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. 4 5 #include "chrome/browser/media_galleries/media_scan_manager.h" 6 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" 22 23 using extensions::ExtensionRegistry; 24 25 namespace media_galleries = extensions::api::media_galleries; 26 27 namespace { 28 29 typedef std::set<std::string /*extension id*/> ScanningExtensionIdSet; 30 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; 36 37 // How long after a completed media scan can we provide the cached results. 38 const int kScanResultsExpiryTimeInHours = 24; 39 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; 53 } 54 return rhs.type == MediaGalleryPrefInfo::kScanResult; 55 } 56 return path.value() < rhs.path.value(); 57 } 58 59 MediaGalleryPrefId pref_id; 60 MediaGalleryPrefInfo::Type type; 61 base::FilePath path; 62 MediaGalleryScanResult file_counts; 63 }; 64 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; 81 } 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())); 89 } 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()); 93 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; 107 } 108 } 109 } 110 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; 121 } 122 } 123 return results; 124 } 125 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); 141 } 142 } 143 for (MediaGalleryPrefIdSet::const_iterator it = to_remove.begin(); 144 it != to_remove.end(); 145 ++it) { 146 preferences->EraseGalleryById(*it); 147 } 148 149 MediaFolderFinder::MediaFolderFinderResults child_folders; 150 MediaFolderFinder::MediaFolderFinderResults 151 unique_found_folders(found_folders); 152 PartitionChildScanResults(preferences, &unique_found_folders, &child_folders); 153 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; 168 } 169 } 170 } 171 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); 185 } 186 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; 198 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); 208 } 209 UMA_HISTOGRAM_COUNTS_10000("MediaGalleries.ScanGalleriesPopulated", 210 unique_found_folders.size() + to_update.size()); 211 } 212 213 int CountScanResultsForExtension(MediaGalleriesPreferences* preferences, 214 const extensions::Extension* extension, 215 MediaGalleryScanResult* file_counts) { 216 int gallery_count = 0; 217 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; 231 } 232 } 233 return gallery_count; 234 } 235 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; 245 } 246 return count; 247 } 248 249 struct ContainerCount { 250 int seen_count, entries_count; 251 bool is_qualified; 252 253 ContainerCount() : seen_count(0), entries_count(-1), is_qualified(false) {} 254 }; 255 256 typedef std::map<base::FilePath, ContainerCount> ContainerCandidates; 257 258 } // namespace 259 260 MediaScanManager::MediaScanManager() 261 : scoped_extension_registry_observer_(this), 262 weak_factory_(this) { 263 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 264 } 265 266 MediaScanManager::~MediaScanManager() { 267 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 268 } 269 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; 275 } 276 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()); 282 } 283 284 void MediaScanManager::CancelScansForProfile(Profile* profile) { 285 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 286 observers_[profile].scanning_extensions.clear(); 287 288 if (!ScanInProgress()) 289 folder_finder_.reset(); 290 } 291 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); 297 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; 307 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; 324 } 325 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)); 329 330 scanning_extensions->insert(extension->id()); 331 scans_for_profile->second.observer->OnScanStarted(extension->id()); 332 333 if (folder_finder_) 334 return; 335 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)); 343 } 344 scan_start_time_ = base::Time::Now(); 345 folder_finder_->StartScan(); 346 } 347 348 void MediaScanManager::CancelScan(Profile* profile, 349 const extensions::Extension* extension) { 350 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); 351 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; 357 } 358 359 scans_for_profile->second.observer->OnScanCancelled(extension->id()); 360 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)); 364 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(); 371 } 372 } 373 374 void MediaScanManager::SetMediaFolderFinderFactory( 375 const MediaFolderFinderFactory& factory) { 376 testing_folder_finder_factory_ = factory; 377 } 378 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); 393 } 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(); 406 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; 420 } 421 } 422 if (is_sensitive) 423 break; 424 425 // Don't bother with ones we already have. 426 if (found_folders.find(parent_directory) != found_folders.end()) 427 continue; 428 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; 439 } 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; 445 } 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(); 450 } 451 } 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(); 459 } 460 return result; 461 } 462 463 MediaScanManager::ScanObservers::ScanObservers() : observer(NULL) {} 464 MediaScanManager::ScanObservers::~ScanObservers() {} 465 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); 472 } 473 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; 480 } 481 return false; 482 } 483 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; 491 } 492 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(); 499 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)); 508 } 509 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()); 516 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; 529 530 AddScanResultsForProfile(preferences, folders); 531 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); 547 } 548 } 549 scanning_extensions->clear(); 550 preferences->SetLastScanCompletionTime(base::Time::Now()); 551 } 552 scoped_extension_registry_observer_.RemoveAll(); 553 folder_finder_.reset(); 554 } 555