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
;
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;
42 : pref_id(kInvalidMediaGalleryPrefId
),
43 type(MediaGalleryPrefInfo::kInvalidType
) {}
44 LocationInfo(MediaGalleryPrefId pref_id
, MediaGalleryPrefInfo::Type type
,
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
;
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
,
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();
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
;
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();
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();
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();
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();
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();
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
);
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();
225 if (it
->second
.type
== MediaGalleryPrefInfo::kScanResult
&&
226 !ContainsKey(permitted_galleries
, it
->first
)) {
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
);
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
))
249 struct ContainerCount
{
250 int seen_count
, entries_count
;
253 ContainerCount() : seen_count(0), entries_count(-1), is_qualified(false) {}
256 typedef std::map
<base::FilePath
, ContainerCount
> ContainerCandidates
;
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
,
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()))
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
;
318 CountScanResultsForExtension(preferences
, extension
, &file_counts
);
319 scans_for_profile
->second
.observer
->OnScanStarted(extension
->id());
320 scans_for_profile
->second
.observer
->OnScanFinished(extension
->id(),
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());
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
));
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())) {
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
]);
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();
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())
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
])) {
425 // Don't bother with ones we already have.
426 if (found_folders
.find(parent_directory
) != found_folders
.end())
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
);
436 candidates
.insert(std::make_pair(parent_directory
, count
)).first
;
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
) {
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();
457 if (it
->second
.is_qualified
&& it
->second
.seen_count
>= 2)
458 result
[it
->first
] = MediaGalleryScanResult();
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();
478 if (!it
->second
.scanning_extensions
.empty())
484 void MediaScanManager::OnScanCompleted(
486 const MediaFolderFinder::MediaFolderFinderResults
& found_folders
) {
487 DCHECK_CURRENTLY_ON(content::BrowserThread::UI
);
488 if (!folder_finder_
|| !success
) {
489 folder_finder_
.reset();
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
,
504 folder_finder_
->graylisted_folders()),
505 base::Bind(&MediaScanManager::OnFoundContainerDirectories
,
506 weak_factory_
.GetWeakPtr(),
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())
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
)
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();
538 const extensions::Extension
* extension
=
539 extension_service
->GetExtensionById(*extension_id_it
, false);
541 MediaGalleryScanResult file_counts
;
542 int gallery_count
= CountScanResultsForExtension(preferences
, extension
,
544 scans_for_profile
->second
.observer
->OnScanFinished(*extension_id_it
,
549 scanning_extensions
->clear();
550 preferences
->SetLastScanCompletionTime(base::Time::Now());
552 scoped_extension_registry_observer_
.RemoveAll();
553 folder_finder_
.reset();