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/file_util.h"
8 #include "base/files/file_enumerator.h"
9 #include "base/logging.h"
10 #include "base/time/time.h"
11 #include "chrome/browser/chrome_notification_types.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 "content/public/browser/notification_details.h"
20 #include "content/public/browser/notification_source.h"
21 #include "extensions/browser/extension_system.h"
22 #include "extensions/common/extension.h"
24 namespace media_galleries
= extensions::api::media_galleries
;
28 typedef std::set
<std::string
/*extension id*/> ScanningExtensionIdSet
;
30 // When multiple scan results have the same parent, sometimes it makes sense
31 // to combine them into a single scan result at the parent. This constant
32 // governs when that happens; kContainerDirectoryMinimumPercent percent of the
33 // directories in the parent directory must be scan results.
34 const int kContainerDirectoryMinimumPercent
= 80;
36 // How long after a completed media scan can we provide the cached results.
37 const int kScanResultsExpiryTimeInHours
= 24;
41 : pref_id(kInvalidMediaGalleryPrefId
),
42 type(MediaGalleryPrefInfo::kInvalidType
) {}
43 LocationInfo(MediaGalleryPrefId pref_id
, MediaGalleryPrefInfo::Type type
,
45 : pref_id(pref_id
), type(type
), path(path
) {}
46 // Highest priority comparison by path, next by type (scan result last),
47 // then by pref id (invalid last).
48 bool operator<(const LocationInfo
& rhs
) const {
49 if (path
.value() == rhs
.path
.value()) {
50 if (type
== rhs
.type
) {
51 return pref_id
> rhs
.pref_id
;
53 return rhs
.type
== MediaGalleryPrefInfo::kScanResult
;
55 return path
.value() < rhs
.path
.value();
58 MediaGalleryPrefId pref_id
;
59 MediaGalleryPrefInfo::Type type
;
61 MediaGalleryScanResult file_counts
;
64 // Finds new scan results that are shadowed (the same location, or a child) by
65 // existing locations and moves them from |found_folders| to |child_folders|.
66 // Also moves new scan results that are shadowed by other new scan results
67 // to |child_folders|.
68 void PartitionChildScanResults(
69 MediaGalleriesPreferences
* preferences
,
70 MediaFolderFinder::MediaFolderFinderResults
* found_folders
,
71 MediaFolderFinder::MediaFolderFinderResults
* child_folders
) {
72 // Construct a list with everything in it.
73 std::vector
<LocationInfo
> all_locations
;
74 for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it
=
75 found_folders
->begin(); it
!= found_folders
->end(); ++it
) {
76 all_locations
.push_back(LocationInfo(kInvalidMediaGalleryPrefId
,
77 MediaGalleryPrefInfo::kScanResult
,
79 all_locations
.back().file_counts
= it
->second
;
81 const MediaGalleriesPrefInfoMap
& known_galleries
=
82 preferences
->known_galleries();
83 for (MediaGalleriesPrefInfoMap::const_iterator it
= known_galleries
.begin();
84 it
!= known_galleries
.end();
86 all_locations
.push_back(LocationInfo(it
->second
.pref_id
, it
->second
.type
,
87 it
->second
.AbsolutePath()));
89 // Sorting on path should put all paths that are prefixes of other paths
90 // next to each other, with the shortest one first.
91 std::sort(all_locations
.begin(), all_locations
.end());
93 size_t previous_parent_index
= 0;
94 for (size_t i
= 1; i
< all_locations
.size(); i
++) {
95 const LocationInfo
& current
= all_locations
[i
];
96 const LocationInfo
& previous_parent
= all_locations
[previous_parent_index
];
97 bool is_child
= previous_parent
.path
.IsParent(current
.path
);
98 if (current
.type
== MediaGalleryPrefInfo::kScanResult
&&
99 current
.pref_id
== kInvalidMediaGalleryPrefId
&&
100 (is_child
|| previous_parent
.path
== current
.path
)) {
101 // Move new scan results that are shadowed.
102 (*child_folders
)[current
.path
] = current
.file_counts
;
103 found_folders
->erase(current
.path
);
104 } else if (!is_child
) {
105 previous_parent_index
= i
;
110 MediaGalleryScanResult
SumFilesUnderPath(
111 const base::FilePath
& path
,
112 const MediaFolderFinder::MediaFolderFinderResults
& candidates
) {
113 MediaGalleryScanResult results
;
114 for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it
=
115 candidates
.begin(); it
!= candidates
.end(); ++it
) {
116 if (it
->first
== path
|| path
.IsParent(it
->first
)) {
117 results
.audio_count
+= it
->second
.audio_count
;
118 results
.image_count
+= it
->second
.image_count
;
119 results
.video_count
+= it
->second
.video_count
;
125 void AddScanResultsForProfile(
126 MediaGalleriesPreferences
* preferences
,
127 const MediaFolderFinder::MediaFolderFinderResults
& found_folders
) {
128 // First, remove any existing scan results where no app has been granted
129 // permission - either it is gone, or is already in the new scan results.
130 // This burns some pref ids, but not at an appreciable rate.
131 MediaGalleryPrefIdSet to_remove
;
132 const MediaGalleriesPrefInfoMap
& known_galleries
=
133 preferences
->known_galleries();
134 for (MediaGalleriesPrefInfoMap::const_iterator it
= known_galleries
.begin();
135 it
!= known_galleries
.end();
137 if (it
->second
.type
== MediaGalleryPrefInfo::kScanResult
&&
138 !preferences
->NonAutoGalleryHasPermission(it
->first
)) {
139 to_remove
.insert(it
->first
);
142 for (MediaGalleryPrefIdSet::const_iterator it
= to_remove
.begin();
143 it
!= to_remove
.end();
145 preferences
->EraseGalleryById(*it
);
148 MediaFolderFinder::MediaFolderFinderResults child_folders
;
149 MediaFolderFinder::MediaFolderFinderResults
150 unique_found_folders(found_folders
);
151 PartitionChildScanResults(preferences
, &unique_found_folders
, &child_folders
);
153 // Updating prefs while iterating them will invalidate the pointer, so
154 // calculate the changes first and then apply them.
155 std::map
<MediaGalleryPrefId
, MediaGalleryScanResult
> to_update
;
156 for (MediaGalleriesPrefInfoMap::const_iterator it
= known_galleries
.begin();
157 it
!= known_galleries
.end();
159 const MediaGalleryPrefInfo
& gallery
= it
->second
;
160 if (!gallery
.IsBlackListedType()) {
161 MediaGalleryScanResult file_counts
=
162 SumFilesUnderPath(gallery
.AbsolutePath(), child_folders
);
163 if (!IsEmptyScanResult(file_counts
) && !gallery
.IsBlackListedType()) {
164 to_update
[it
->first
] = file_counts
;
169 for (std::map
<MediaGalleryPrefId
,
170 MediaGalleryScanResult
>::const_iterator it
= to_update
.begin();
171 it
!= to_update
.end();
173 const MediaGalleryPrefInfo
& gallery
=
174 preferences
->known_galleries().find(it
->first
)->second
;
175 preferences
->AddGallery(gallery
.device_id
, gallery
.path
, gallery
.type
,
176 gallery
.volume_label
, gallery
.vendor_name
,
177 gallery
.model_name
, gallery
.total_size_in_bytes
,
178 gallery
.last_attach_time
,
179 it
->second
.audio_count
,
180 it
->second
.image_count
,
181 it
->second
.video_count
);
184 // Add new scan results.
185 for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it
=
186 unique_found_folders
.begin();
187 it
!= unique_found_folders
.end();
189 MediaGalleryScanResult file_counts
=
190 SumFilesUnderPath(it
->first
, child_folders
);
191 // The top level scan result is not in |child_folders|. Add it in as well.
192 file_counts
.audio_count
+= it
->second
.audio_count
;
193 file_counts
.image_count
+= it
->second
.image_count
;
194 file_counts
.video_count
+= it
->second
.video_count
;
196 MediaGalleryPrefInfo gallery
;
197 bool existing
= preferences
->LookUpGalleryByPath(it
->first
, &gallery
);
199 preferences
->AddGallery(gallery
.device_id
, gallery
.path
,
200 MediaGalleryPrefInfo::kScanResult
,
201 gallery
.volume_label
, gallery
.vendor_name
,
202 gallery
.model_name
, gallery
.total_size_in_bytes
,
203 gallery
.last_attach_time
, file_counts
.audio_count
,
204 file_counts
.image_count
, file_counts
.video_count
);
208 // A single directory may contain many folders with media in them, without
209 // containing any media itself. In fact, the primary purpose of that directory
210 // may be to contain media directories. This function tries to find those
211 // immediate container directories.
212 MediaFolderFinder::MediaFolderFinderResults
FindContainerScanResults(
213 const MediaFolderFinder::MediaFolderFinderResults
& found_folders
) {
214 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE));
215 // Count the number of scan results with the same parent directory.
216 typedef std::map
<base::FilePath
, int /*count*/> ContainerCandidates
;
217 ContainerCandidates candidates
;
218 for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it
=
219 found_folders
.begin(); it
!= found_folders
.end(); ++it
) {
220 base::FilePath parent_directory
= it
->first
.DirName();
221 ContainerCandidates::iterator existing
= candidates
.find(parent_directory
);
222 if (existing
== candidates
.end()) {
223 candidates
[parent_directory
] = 1;
229 // If a parent directory has more than one scan result, consider it.
230 MediaFolderFinder::MediaFolderFinderResults result
;
231 for (ContainerCandidates::const_iterator it
= candidates
.begin();
232 it
!= candidates
.end();
237 base::FileEnumerator
dir_counter(it
->first
, false /*recursive*/,
238 base::FileEnumerator::DIRECTORIES
);
239 base::FileEnumerator::FileInfo info
;
241 for (base::FilePath name
= dir_counter
.Next();
243 name
= dir_counter
.Next()) {
244 if (!base::IsLink(name
))
247 if (it
->second
* 100 / count
>= kContainerDirectoryMinimumPercent
)
248 result
[it
->first
] = MediaGalleryScanResult();
253 void RemoveSensitiveLocations(
254 MediaFolderFinder::MediaFolderFinderResults
* found_folders
) {
255 // TODO(vandebo) Use the greylist from filesystem api.
258 int CountScanResultsForExtension(MediaGalleriesPreferences
* preferences
,
259 const extensions::Extension
* extension
,
260 MediaGalleryScanResult
* file_counts
) {
261 int gallery_count
= 0;
263 MediaGalleryPrefIdSet permitted_galleries
=
264 preferences
->GalleriesForExtension(*extension
);
265 const MediaGalleriesPrefInfoMap
& known_galleries
=
266 preferences
->known_galleries();
267 for (MediaGalleriesPrefInfoMap::const_iterator it
= known_galleries
.begin();
268 it
!= known_galleries
.end();
270 if (it
->second
.type
== MediaGalleryPrefInfo::kScanResult
&&
271 !ContainsKey(permitted_galleries
, it
->first
)) {
273 file_counts
->audio_count
+= it
->second
.audio_count
;
274 file_counts
->image_count
+= it
->second
.image_count
;
275 file_counts
->video_count
+= it
->second
.video_count
;
278 return gallery_count
;
283 MediaScanManager::MediaScanManager() : weak_factory_(this) {
284 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
287 MediaScanManager::~MediaScanManager() {
288 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
291 void MediaScanManager::AddObserver(Profile
* profile
,
292 MediaScanManagerObserver
* observer
) {
293 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
294 DCHECK(!ContainsKey(observers_
, profile
));
295 observers_
[profile
].observer
= observer
;
298 void MediaScanManager::RemoveObserver(Profile
* profile
) {
299 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
300 bool scan_in_progress
= ScanInProgress();
301 observers_
.erase(profile
);
302 DCHECK_EQ(scan_in_progress
, ScanInProgress());
305 void MediaScanManager::CancelScansForProfile(Profile
* profile
) {
306 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
307 observers_
[profile
].scanning_extensions
.clear();
309 if (!ScanInProgress())
310 folder_finder_
.reset();
313 void MediaScanManager::StartScan(Profile
* profile
,
314 const extensions::Extension
* extension
,
317 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
319 ScanObserverMap::iterator scans_for_profile
= observers_
.find(profile
);
320 // We expect that an MediaScanManagerObserver has already been registered.
321 DCHECK(scans_for_profile
!= observers_
.end());
322 bool scan_in_progress
= ScanInProgress();
323 // Ignore requests for extensions that are already scanning.
324 ScanningExtensionIdSet
* scanning_extensions
;
325 scanning_extensions
= &scans_for_profile
->second
.scanning_extensions
;
326 if (scan_in_progress
&& ContainsKey(*scanning_extensions
, extension
->id()))
329 // Provide cached result if there is not already a scan in progress,
330 // there is no user gesture, and the previous results are unexpired.
331 MediaGalleriesPreferences
* preferences
=
332 MediaGalleriesPreferencesFactory::GetForProfile(profile
);
333 base::TimeDelta time_since_last_scan
=
334 base::Time::Now() - preferences
->GetLastScanCompletionTime();
335 if (!scan_in_progress
&& !user_gesture
&& time_since_last_scan
<
336 base::TimeDelta::FromHours(kScanResultsExpiryTimeInHours
)) {
337 MediaGalleryScanResult file_counts
;
339 CountScanResultsForExtension(preferences
, extension
, &file_counts
);
340 scans_for_profile
->second
.observer
->OnScanStarted(extension
->id());
341 scans_for_profile
->second
.observer
->OnScanFinished(extension
->id(),
347 // On first scan for the |profile|, register to listen for extension unload.
348 if (scanning_extensions
->empty()) {
351 chrome::NOTIFICATION_EXTENSION_UNLOADED
,
352 content::Source
<Profile
>(profile
));
355 scanning_extensions
->insert(extension
->id());
356 scans_for_profile
->second
.observer
->OnScanStarted(extension
->id());
361 MediaFolderFinder::MediaFolderFinderResultsCallback callback
=
362 base::Bind(&MediaScanManager::OnScanCompleted
,
363 weak_factory_
.GetWeakPtr());
364 if (testing_folder_finder_factory_
.is_null()) {
365 folder_finder_
.reset(new MediaFolderFinder(callback
));
367 folder_finder_
.reset(testing_folder_finder_factory_
.Run(callback
));
369 folder_finder_
->StartScan();
372 void MediaScanManager::CancelScan(Profile
* profile
,
373 const extensions::Extension
* extension
) {
374 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
376 // Erases the logical scan if found, early exit otherwise.
377 ScanObserverMap::iterator scans_for_profile
= observers_
.find(profile
);
378 if (scans_for_profile
== observers_
.end() ||
379 !scans_for_profile
->second
.scanning_extensions
.erase(extension
->id())) {
383 scans_for_profile
->second
.observer
->OnScanCancelled(extension
->id());
385 // No more scanning extensions for |profile|, so stop listening for unloads.
386 if (scans_for_profile
->second
.scanning_extensions
.empty()) {
389 chrome::NOTIFICATION_EXTENSION_UNLOADED
,
390 content::Source
<Profile
>(profile
));
393 if (!ScanInProgress())
394 folder_finder_
.reset();
397 void MediaScanManager::SetMediaFolderFinderFactory(
398 const MediaFolderFinderFactory
& factory
) {
399 testing_folder_finder_factory_
= factory
;
402 MediaScanManager::ScanObservers::ScanObservers() : observer(NULL
) {}
403 MediaScanManager::ScanObservers::~ScanObservers() {}
405 void MediaScanManager::Observe(
406 int type
, const content::NotificationSource
& source
,
407 const content::NotificationDetails
& details
) {
408 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
410 case chrome::NOTIFICATION_EXTENSION_UNLOADED
: {
411 Profile
* profile
= content::Source
<Profile
>(source
).ptr();
412 extensions::Extension
* extension
= const_cast<extensions::Extension
*>(
413 content::Details
<extensions::UnloadedExtensionInfo
>(
414 details
)->extension
);
416 CancelScan(profile
, extension
);
424 bool MediaScanManager::ScanInProgress() const {
425 for (ScanObserverMap::const_iterator it
= observers_
.begin();
426 it
!= observers_
.end();
428 if (!it
->second
.scanning_extensions
.empty())
434 void MediaScanManager::OnScanCompleted(
436 const MediaFolderFinder::MediaFolderFinderResults
& found_folders
) {
437 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
438 if (!folder_finder_
|| !success
) {
439 folder_finder_
.reset();
443 content::BrowserThread::PostTaskAndReplyWithResult(
444 content::BrowserThread::FILE, FROM_HERE
,
445 base::Bind(FindContainerScanResults
, found_folders
),
446 base::Bind(&MediaScanManager::OnFoundContainerDirectories
,
447 weak_factory_
.GetWeakPtr(), found_folders
));
450 void MediaScanManager::OnFoundContainerDirectories(
451 const MediaFolderFinder::MediaFolderFinderResults
& found_folders
,
452 const MediaFolderFinder::MediaFolderFinderResults
& container_folders
) {
453 MediaFolderFinder::MediaFolderFinderResults folders
;
454 folders
.insert(found_folders
.begin(), found_folders
.end());
455 folders
.insert(container_folders
.begin(), container_folders
.end());
457 RemoveSensitiveLocations(&folders
);
459 for (ScanObserverMap::iterator scans_for_profile
= observers_
.begin();
460 scans_for_profile
!= observers_
.end();
461 ++scans_for_profile
) {
462 if (scans_for_profile
->second
.scanning_extensions
.empty())
464 Profile
* profile
= scans_for_profile
->first
;
465 MediaGalleriesPreferences
* preferences
=
466 MediaGalleriesPreferencesFactory::GetForProfile(profile
);
467 ExtensionService
* extension_service
=
468 extensions::ExtensionSystem::Get(profile
)->extension_service();
469 if (!extension_service
)
472 AddScanResultsForProfile(preferences
, folders
);
474 ScanningExtensionIdSet
* scanning_extensions
=
475 &scans_for_profile
->second
.scanning_extensions
;
476 for (ScanningExtensionIdSet::const_iterator extension_id_it
=
477 scanning_extensions
->begin();
478 extension_id_it
!= scanning_extensions
->end();
480 const extensions::Extension
* extension
=
481 extension_service
->GetExtensionById(*extension_id_it
, false);
483 MediaGalleryScanResult file_counts
;
484 int gallery_count
= CountScanResultsForExtension(preferences
, extension
,
486 scans_for_profile
->second
.observer
->OnScanFinished(*extension_id_it
,
491 scanning_extensions
->clear();
492 preferences
->SetLastScanCompletionTime(base::Time::Now());
494 registrar_
.RemoveAll();
495 folder_finder_
.reset();