Media Galleries API Scanning: Give cached scan results if no user gesture.
[chromium-blink-merge.git] / chrome / browser / media_galleries / media_scan_manager.cc
blob338fe00201f6ceefbfbc5ce926860a14a7484d6d
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;
26 namespace {
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;
39 struct LocationInfo {
40 LocationInfo()
41 : pref_id(kInvalidMediaGalleryPrefId),
42 type(MediaGalleryPrefInfo::kInvalidType) {}
43 LocationInfo(MediaGalleryPrefId pref_id, MediaGalleryPrefInfo::Type type,
44 base::FilePath path)
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;
60 base::FilePath path;
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,
78 it->first));
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();
85 ++it) {
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;
122 return results;
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();
136 ++it) {
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();
144 ++it) {
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();
158 ++it) {
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();
172 ++it) {
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();
188 ++it) {
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);
198 DCHECK(!existing);
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;
224 } else {
225 existing->second++;
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();
233 ++it) {
234 if (it->second <= 1)
235 continue;
237 base::FileEnumerator dir_counter(it->first, false /*recursive*/,
238 base::FileEnumerator::DIRECTORIES);
239 base::FileEnumerator::FileInfo info;
240 int count = 0;
241 for (base::FilePath name = dir_counter.Next();
242 !name.empty();
243 name = dir_counter.Next()) {
244 if (!base::IsLink(name))
245 count++;
247 if (it->second * 100 / count >= kContainerDirectoryMinimumPercent)
248 result[it->first] = MediaGalleryScanResult();
250 return result;
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();
269 ++it) {
270 if (it->second.type == MediaGalleryPrefInfo::kScanResult &&
271 !ContainsKey(permitted_galleries, it->first)) {
272 gallery_count++;
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;
281 } // namespace
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,
315 bool user_gesture) {
316 DCHECK(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()))
327 return;
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;
338 int gallery_count =
339 CountScanResultsForExtension(preferences, extension, &file_counts);
340 scans_for_profile->second.observer->OnScanStarted(extension->id());
341 scans_for_profile->second.observer->OnScanFinished(extension->id(),
342 gallery_count,
343 file_counts);
344 return;
347 // On first scan for the |profile|, register to listen for extension unload.
348 if (scanning_extensions->empty()) {
349 registrar_.Add(
350 this,
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());
358 if (folder_finder_)
359 return;
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));
366 } else {
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())) {
380 return;
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()) {
387 registrar_.Remove(
388 this,
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));
409 switch (type) {
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);
415 DCHECK(extension);
416 CancelScan(profile, extension);
417 break;
419 default:
420 NOTREACHED();
424 bool MediaScanManager::ScanInProgress() const {
425 for (ScanObserverMap::const_iterator it = observers_.begin();
426 it != observers_.end();
427 ++it) {
428 if (!it->second.scanning_extensions.empty())
429 return true;
431 return false;
434 void MediaScanManager::OnScanCompleted(
435 bool success,
436 const MediaFolderFinder::MediaFolderFinderResults& found_folders) {
437 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
438 if (!folder_finder_ || !success) {
439 folder_finder_.reset();
440 return;
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())
463 continue;
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)
470 continue;
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();
479 ++extension_id_it) {
480 const extensions::Extension* extension =
481 extension_service->GetExtensionById(*extension_id_it, false);
482 if (extension) {
483 MediaGalleryScanResult file_counts;
484 int gallery_count = CountScanResultsForExtension(preferences, extension,
485 &file_counts);
486 scans_for_profile->second.observer->OnScanFinished(*extension_id_it,
487 gallery_count,
488 file_counts);
491 scanning_extensions->clear();
492 preferences->SetLastScanCompletionTime(base::Time::Now());
494 registrar_.RemoveAll();
495 folder_finder_.reset();