Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / browser / media_galleries / media_scan_manager.cc
blobd86bd8c7f32dd9cc233580d042aba775ef9d1883
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/media_galleries/media_scan_manager.h"
7 #include "base/files/file_enumerator.h"
8 #include "base/files/file_util.h"
9 #include "base/logging.h"
10 #include "base/metrics/histogram.h"
11 #include "base/time/time.h"
12 #include "chrome/browser/extensions/extension_service.h"
13 #include "chrome/browser/media_galleries/media_galleries_preferences.h"
14 #include "chrome/browser/media_galleries/media_galleries_preferences_factory.h"
15 #include "chrome/browser/media_galleries/media_scan_manager_observer.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/common/extensions/api/media_galleries.h"
18 #include "content/public/browser/browser_thread.h"
19 #include "extensions/browser/extension_registry.h"
20 #include "extensions/browser/extension_system.h"
21 #include "extensions/common/extension.h"
23 using extensions::ExtensionRegistry;
25 namespace media_galleries = extensions::api::media_galleries;
27 namespace {
29 typedef std::set<std::string /*extension id*/> ScanningExtensionIdSet;
31 // When multiple scan results have the same parent, sometimes it makes sense
32 // to combine them into a single scan result at the parent. This constant
33 // governs when that happens; kContainerDirectoryMinimumPercent percent of the
34 // directories in the parent directory must be scan results.
35 const int kContainerDirectoryMinimumPercent = 80;
37 // How long after a completed media scan can we provide the cached results.
38 const int kScanResultsExpiryTimeInHours = 24;
40 struct LocationInfo {
41 LocationInfo()
42 : pref_id(kInvalidMediaGalleryPrefId),
43 type(MediaGalleryPrefInfo::kInvalidType) {}
44 LocationInfo(MediaGalleryPrefId pref_id, MediaGalleryPrefInfo::Type type,
45 base::FilePath path)
46 : pref_id(pref_id), type(type), path(path) {}
47 // Highest priority comparison by path, next by type (scan result last),
48 // then by pref id (invalid last).
49 bool operator<(const LocationInfo& rhs) const {
50 if (path.value() == rhs.path.value()) {
51 if (type == rhs.type) {
52 return pref_id > rhs.pref_id;
54 return rhs.type == MediaGalleryPrefInfo::kScanResult;
56 return path.value() < rhs.path.value();
59 MediaGalleryPrefId pref_id;
60 MediaGalleryPrefInfo::Type type;
61 base::FilePath path;
62 MediaGalleryScanResult file_counts;
65 // Finds new scan results that are shadowed (the same location, or a child) by
66 // existing locations and moves them from |found_folders| to |child_folders|.
67 // Also moves new scan results that are shadowed by other new scan results
68 // to |child_folders|.
69 void PartitionChildScanResults(
70 MediaGalleriesPreferences* preferences,
71 MediaFolderFinder::MediaFolderFinderResults* found_folders,
72 MediaFolderFinder::MediaFolderFinderResults* child_folders) {
73 // Construct a list with everything in it.
74 std::vector<LocationInfo> all_locations;
75 for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it =
76 found_folders->begin(); it != found_folders->end(); ++it) {
77 all_locations.push_back(LocationInfo(kInvalidMediaGalleryPrefId,
78 MediaGalleryPrefInfo::kScanResult,
79 it->first));
80 all_locations.back().file_counts = it->second;
82 const MediaGalleriesPrefInfoMap& known_galleries =
83 preferences->known_galleries();
84 for (MediaGalleriesPrefInfoMap::const_iterator it = known_galleries.begin();
85 it != known_galleries.end();
86 ++it) {
87 all_locations.push_back(LocationInfo(it->second.pref_id, it->second.type,
88 it->second.AbsolutePath()));
90 // Sorting on path should put all paths that are prefixes of other paths
91 // next to each other, with the shortest one first.
92 std::sort(all_locations.begin(), all_locations.end());
94 size_t previous_parent_index = 0;
95 for (size_t i = 1; i < all_locations.size(); i++) {
96 const LocationInfo& current = all_locations[i];
97 const LocationInfo& previous_parent = all_locations[previous_parent_index];
98 bool is_child = previous_parent.path.IsParent(current.path);
99 if (current.type == MediaGalleryPrefInfo::kScanResult &&
100 current.pref_id == kInvalidMediaGalleryPrefId &&
101 (is_child || previous_parent.path == current.path)) {
102 // Move new scan results that are shadowed.
103 (*child_folders)[current.path] = current.file_counts;
104 found_folders->erase(current.path);
105 } else if (!is_child) {
106 previous_parent_index = i;
111 MediaGalleryScanResult SumFilesUnderPath(
112 const base::FilePath& path,
113 const MediaFolderFinder::MediaFolderFinderResults& candidates) {
114 MediaGalleryScanResult results;
115 for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it =
116 candidates.begin(); it != candidates.end(); ++it) {
117 if (it->first == path || path.IsParent(it->first)) {
118 results.audio_count += it->second.audio_count;
119 results.image_count += it->second.image_count;
120 results.video_count += it->second.video_count;
123 return results;
126 void AddScanResultsForProfile(
127 MediaGalleriesPreferences* preferences,
128 const MediaFolderFinder::MediaFolderFinderResults& found_folders) {
129 // First, remove any existing scan results where no app has been granted
130 // permission - either it is gone, or is already in the new scan results.
131 // This burns some pref ids, but not at an appreciable rate.
132 MediaGalleryPrefIdSet to_remove;
133 const MediaGalleriesPrefInfoMap& known_galleries =
134 preferences->known_galleries();
135 for (MediaGalleriesPrefInfoMap::const_iterator it = known_galleries.begin();
136 it != known_galleries.end();
137 ++it) {
138 if (it->second.type == MediaGalleryPrefInfo::kScanResult &&
139 !preferences->NonAutoGalleryHasPermission(it->first)) {
140 to_remove.insert(it->first);
143 for (MediaGalleryPrefIdSet::const_iterator it = to_remove.begin();
144 it != to_remove.end();
145 ++it) {
146 preferences->EraseGalleryById(*it);
149 MediaFolderFinder::MediaFolderFinderResults child_folders;
150 MediaFolderFinder::MediaFolderFinderResults
151 unique_found_folders(found_folders);
152 PartitionChildScanResults(preferences, &unique_found_folders, &child_folders);
154 // Updating prefs while iterating them will invalidate the pointer, so
155 // calculate the changes first and then apply them.
156 std::map<MediaGalleryPrefId, MediaGalleryScanResult> to_update;
157 for (MediaGalleriesPrefInfoMap::const_iterator it = known_galleries.begin();
158 it != known_galleries.end();
159 ++it) {
160 const MediaGalleryPrefInfo& gallery = it->second;
161 if (!gallery.IsBlackListedType()) {
162 MediaGalleryScanResult file_counts =
163 SumFilesUnderPath(gallery.AbsolutePath(), child_folders);
164 if (gallery.audio_count != file_counts.audio_count ||
165 gallery.image_count != file_counts.image_count ||
166 gallery.video_count != file_counts.video_count) {
167 to_update[it->first] = file_counts;
172 for (std::map<MediaGalleryPrefId,
173 MediaGalleryScanResult>::const_iterator it = to_update.begin();
174 it != to_update.end();
175 ++it) {
176 const MediaGalleryPrefInfo& gallery =
177 preferences->known_galleries().find(it->first)->second;
178 preferences->AddGallery(gallery.device_id, gallery.path, gallery.type,
179 gallery.volume_label, gallery.vendor_name,
180 gallery.model_name, gallery.total_size_in_bytes,
181 gallery.last_attach_time,
182 it->second.audio_count,
183 it->second.image_count,
184 it->second.video_count);
187 // Add new scan results.
188 for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it =
189 unique_found_folders.begin();
190 it != unique_found_folders.end();
191 ++it) {
192 MediaGalleryScanResult file_counts =
193 SumFilesUnderPath(it->first, child_folders);
194 // The top level scan result is not in |child_folders|. Add it in as well.
195 file_counts.audio_count += it->second.audio_count;
196 file_counts.image_count += it->second.image_count;
197 file_counts.video_count += it->second.video_count;
199 MediaGalleryPrefInfo gallery;
200 bool existing = preferences->LookUpGalleryByPath(it->first, &gallery);
201 DCHECK(!existing);
202 preferences->AddGallery(gallery.device_id, gallery.path,
203 MediaGalleryPrefInfo::kScanResult,
204 gallery.volume_label, gallery.vendor_name,
205 gallery.model_name, gallery.total_size_in_bytes,
206 gallery.last_attach_time, file_counts.audio_count,
207 file_counts.image_count, file_counts.video_count);
209 UMA_HISTOGRAM_COUNTS_10000("MediaGalleries.ScanGalleriesPopulated",
210 unique_found_folders.size() + to_update.size());
213 int CountScanResultsForExtension(MediaGalleriesPreferences* preferences,
214 const extensions::Extension* extension,
215 MediaGalleryScanResult* file_counts) {
216 int gallery_count = 0;
218 MediaGalleryPrefIdSet permitted_galleries =
219 preferences->GalleriesForExtension(*extension);
220 const MediaGalleriesPrefInfoMap& known_galleries =
221 preferences->known_galleries();
222 for (MediaGalleriesPrefInfoMap::const_iterator it = known_galleries.begin();
223 it != known_galleries.end();
224 ++it) {
225 if (it->second.type == MediaGalleryPrefInfo::kScanResult &&
226 !ContainsKey(permitted_galleries, it->first)) {
227 gallery_count++;
228 file_counts->audio_count += it->second.audio_count;
229 file_counts->image_count += it->second.image_count;
230 file_counts->video_count += it->second.video_count;
233 return gallery_count;
236 int CountDirectoryEntries(const base::FilePath& path) {
237 base::FileEnumerator dir_counter(
238 path, false /*recursive*/, base::FileEnumerator::DIRECTORIES);
239 int count = 0;
240 base::FileEnumerator::FileInfo info;
241 for (base::FilePath name = dir_counter.Next(); !name.empty();
242 name = dir_counter.Next()) {
243 if (!base::IsLink(name))
244 ++count;
246 return count;
249 struct ContainerCount {
250 int seen_count, entries_count;
251 bool is_qualified;
253 ContainerCount() : seen_count(0), entries_count(-1), is_qualified(false) {}
256 typedef std::map<base::FilePath, ContainerCount> ContainerCandidates;
258 } // namespace
260 MediaScanManager::MediaScanManager()
261 : scoped_extension_registry_observer_(this),
262 weak_factory_(this) {
263 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
266 MediaScanManager::~MediaScanManager() {
267 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
270 void MediaScanManager::AddObserver(Profile* profile,
271 MediaScanManagerObserver* observer) {
272 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
273 DCHECK(!ContainsKey(observers_, profile));
274 observers_[profile].observer = observer;
277 void MediaScanManager::RemoveObserver(Profile* profile) {
278 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
279 bool scan_in_progress = ScanInProgress();
280 observers_.erase(profile);
281 DCHECK_EQ(scan_in_progress, ScanInProgress());
284 void MediaScanManager::CancelScansForProfile(Profile* profile) {
285 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
286 observers_[profile].scanning_extensions.clear();
288 if (!ScanInProgress())
289 folder_finder_.reset();
292 void MediaScanManager::StartScan(Profile* profile,
293 const extensions::Extension* extension,
294 bool user_gesture) {
295 DCHECK(extension);
296 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
298 ScanObserverMap::iterator scans_for_profile = observers_.find(profile);
299 // We expect that an MediaScanManagerObserver has already been registered.
300 DCHECK(scans_for_profile != observers_.end());
301 bool scan_in_progress = ScanInProgress();
302 // Ignore requests for extensions that are already scanning.
303 ScanningExtensionIdSet* scanning_extensions;
304 scanning_extensions = &scans_for_profile->second.scanning_extensions;
305 if (scan_in_progress && ContainsKey(*scanning_extensions, extension->id()))
306 return;
308 // Provide cached result if there is not already a scan in progress,
309 // there is no user gesture, and the previous results are unexpired.
310 MediaGalleriesPreferences* preferences =
311 MediaGalleriesPreferencesFactory::GetForProfile(profile);
312 base::TimeDelta time_since_last_scan =
313 base::Time::Now() - preferences->GetLastScanCompletionTime();
314 if (!scan_in_progress && !user_gesture && time_since_last_scan <
315 base::TimeDelta::FromHours(kScanResultsExpiryTimeInHours)) {
316 MediaGalleryScanResult file_counts;
317 int gallery_count =
318 CountScanResultsForExtension(preferences, extension, &file_counts);
319 scans_for_profile->second.observer->OnScanStarted(extension->id());
320 scans_for_profile->second.observer->OnScanFinished(extension->id(),
321 gallery_count,
322 file_counts);
323 return;
326 // On first scan for the |profile|, register to listen for extension unload.
327 if (scanning_extensions->empty())
328 scoped_extension_registry_observer_.Add(ExtensionRegistry::Get(profile));
330 scanning_extensions->insert(extension->id());
331 scans_for_profile->second.observer->OnScanStarted(extension->id());
333 if (folder_finder_)
334 return;
336 MediaFolderFinder::MediaFolderFinderResultsCallback callback =
337 base::Bind(&MediaScanManager::OnScanCompleted,
338 weak_factory_.GetWeakPtr());
339 if (testing_folder_finder_factory_.is_null()) {
340 folder_finder_.reset(new MediaFolderFinder(callback));
341 } else {
342 folder_finder_.reset(testing_folder_finder_factory_.Run(callback));
344 scan_start_time_ = base::Time::Now();
345 folder_finder_->StartScan();
348 void MediaScanManager::CancelScan(Profile* profile,
349 const extensions::Extension* extension) {
350 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
352 // Erases the logical scan if found, early exit otherwise.
353 ScanObserverMap::iterator scans_for_profile = observers_.find(profile);
354 if (scans_for_profile == observers_.end() ||
355 !scans_for_profile->second.scanning_extensions.erase(extension->id())) {
356 return;
359 scans_for_profile->second.observer->OnScanCancelled(extension->id());
361 // No more scanning extensions for |profile|, so stop listening for unloads.
362 if (scans_for_profile->second.scanning_extensions.empty())
363 scoped_extension_registry_observer_.Remove(ExtensionRegistry::Get(profile));
365 if (!ScanInProgress()) {
366 folder_finder_.reset();
367 DCHECK(!scan_start_time_.is_null());
368 UMA_HISTOGRAM_LONG_TIMES("MediaGalleries.ScanCancelTime",
369 base::Time::Now() - scan_start_time_);
370 scan_start_time_ = base::Time();
374 void MediaScanManager::SetMediaFolderFinderFactory(
375 const MediaFolderFinderFactory& factory) {
376 testing_folder_finder_factory_ = factory;
379 // A single directory may contain many folders with media in them, without
380 // containing any media itself. In fact, the primary purpose of that directory
381 // may be to contain media directories. This function tries to find those
382 // container directories.
383 MediaFolderFinder::MediaFolderFinderResults
384 MediaScanManager::FindContainerScanResults(
385 const MediaFolderFinder::MediaFolderFinderResults& found_folders,
386 const std::vector<base::FilePath>& sensitive_locations) {
387 DCHECK_CURRENTLY_ON(content::BrowserThread::FILE);
388 std::vector<base::FilePath> abs_sensitive_locations;
389 for (size_t i = 0; i < sensitive_locations.size(); ++i) {
390 base::FilePath path = base::MakeAbsoluteFilePath(sensitive_locations[i]);
391 if (!path.empty())
392 abs_sensitive_locations.push_back(path);
394 // Recursively find parent directories with majority of media directories,
395 // or container directories.
396 // |candidates| keeps track of directories which might have enough
397 // such directories to have us return them.
398 typedef std::map<base::FilePath, ContainerCount> ContainerCandidates;
399 ContainerCandidates candidates;
400 for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it =
401 found_folders.begin();
402 it != found_folders.end();
403 ++it) {
404 base::FilePath child_directory = it->first;
405 base::FilePath parent_directory = child_directory.DirName();
407 // Parent of root is root.
408 while (!parent_directory.empty() && child_directory != parent_directory) {
409 // Skip sensitive folders and their ancestors.
410 base::FilePath abs_parent_directory =
411 base::MakeAbsoluteFilePath(parent_directory);
412 if (abs_parent_directory.empty())
413 break;
414 bool is_sensitive = false;
415 for (size_t i = 0; i < abs_sensitive_locations.size(); ++i) {
416 if (abs_parent_directory == abs_sensitive_locations[i] ||
417 abs_parent_directory.IsParent(abs_sensitive_locations[i])) {
418 is_sensitive = true;
419 break;
422 if (is_sensitive)
423 break;
425 // Don't bother with ones we already have.
426 if (found_folders.find(parent_directory) != found_folders.end())
427 continue;
429 ContainerCandidates::iterator parent_it =
430 candidates.find(parent_directory);
431 if (parent_it == candidates.end()) {
432 ContainerCount count;
433 count.seen_count = 1;
434 count.entries_count = CountDirectoryEntries(parent_directory);
435 parent_it =
436 candidates.insert(std::make_pair(parent_directory, count)).first;
437 } else {
438 ++candidates[parent_directory].seen_count;
440 // If previously sufficient, or not sufficient, bail.
441 if (parent_it->second.is_qualified ||
442 parent_it->second.seen_count * 100 / parent_it->second.entries_count <
443 kContainerDirectoryMinimumPercent) {
444 break;
446 // Otherwise, mark qualified and check parent.
447 parent_it->second.is_qualified = true;
448 child_directory = parent_directory;
449 parent_directory = child_directory.DirName();
452 MediaFolderFinder::MediaFolderFinderResults result;
453 // Copy and return worthy results.
454 for (ContainerCandidates::const_iterator it = candidates.begin();
455 it != candidates.end();
456 ++it) {
457 if (it->second.is_qualified && it->second.seen_count >= 2)
458 result[it->first] = MediaGalleryScanResult();
460 return result;
463 MediaScanManager::ScanObservers::ScanObservers() : observer(NULL) {}
464 MediaScanManager::ScanObservers::~ScanObservers() {}
466 void MediaScanManager::OnExtensionUnloaded(
467 content::BrowserContext* browser_context,
468 const extensions::Extension* extension,
469 extensions::UnloadedExtensionInfo::Reason reason) {
470 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
471 CancelScan(Profile::FromBrowserContext(browser_context), extension);
474 bool MediaScanManager::ScanInProgress() const {
475 for (ScanObserverMap::const_iterator it = observers_.begin();
476 it != observers_.end();
477 ++it) {
478 if (!it->second.scanning_extensions.empty())
479 return true;
481 return false;
484 void MediaScanManager::OnScanCompleted(
485 bool success,
486 const MediaFolderFinder::MediaFolderFinderResults& found_folders) {
487 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
488 if (!folder_finder_ || !success) {
489 folder_finder_.reset();
490 return;
493 UMA_HISTOGRAM_COUNTS_10000("MediaGalleries.ScanDirectoriesFound",
494 found_folders.size());
495 DCHECK(!scan_start_time_.is_null());
496 UMA_HISTOGRAM_LONG_TIMES("MediaGalleries.ScanFinishedTime",
497 base::Time::Now() - scan_start_time_);
498 scan_start_time_ = base::Time();
500 content::BrowserThread::PostTaskAndReplyWithResult(
501 content::BrowserThread::FILE, FROM_HERE,
502 base::Bind(FindContainerScanResults,
503 found_folders,
504 folder_finder_->graylisted_folders()),
505 base::Bind(&MediaScanManager::OnFoundContainerDirectories,
506 weak_factory_.GetWeakPtr(),
507 found_folders));
510 void MediaScanManager::OnFoundContainerDirectories(
511 const MediaFolderFinder::MediaFolderFinderResults& found_folders,
512 const MediaFolderFinder::MediaFolderFinderResults& container_folders) {
513 MediaFolderFinder::MediaFolderFinderResults folders;
514 folders.insert(found_folders.begin(), found_folders.end());
515 folders.insert(container_folders.begin(), container_folders.end());
517 for (ScanObserverMap::iterator scans_for_profile = observers_.begin();
518 scans_for_profile != observers_.end();
519 ++scans_for_profile) {
520 if (scans_for_profile->second.scanning_extensions.empty())
521 continue;
522 Profile* profile = scans_for_profile->first;
523 MediaGalleriesPreferences* preferences =
524 MediaGalleriesPreferencesFactory::GetForProfile(profile);
525 ExtensionService* extension_service =
526 extensions::ExtensionSystem::Get(profile)->extension_service();
527 if (!extension_service)
528 continue;
530 AddScanResultsForProfile(preferences, folders);
532 ScanningExtensionIdSet* scanning_extensions =
533 &scans_for_profile->second.scanning_extensions;
534 for (ScanningExtensionIdSet::const_iterator extension_id_it =
535 scanning_extensions->begin();
536 extension_id_it != scanning_extensions->end();
537 ++extension_id_it) {
538 const extensions::Extension* extension =
539 extension_service->GetExtensionById(*extension_id_it, false);
540 if (extension) {
541 MediaGalleryScanResult file_counts;
542 int gallery_count = CountScanResultsForExtension(preferences, extension,
543 &file_counts);
544 scans_for_profile->second.observer->OnScanFinished(*extension_id_it,
545 gallery_count,
546 file_counts);
549 scanning_extensions->clear();
550 preferences->SetLastScanCompletionTime(base::Time::Now());
552 scoped_extension_registry_observer_.RemoveAll();
553 folder_finder_.reset();