blob: d86bd8c7f32dd9cc233580d042aba775ef9d1883 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/media_galleries/media_scan_manager.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/metrics/histogram.h"
#include "base/time/time.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/media_galleries/media_galleries_preferences.h"
#include "chrome/browser/media_galleries/media_galleries_preferences_factory.h"
#include "chrome/browser/media_galleries/media_scan_manager_observer.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/extensions/api/media_galleries.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension.h"
using extensions::ExtensionRegistry;
namespace media_galleries = extensions::api::media_galleries;
namespace {
typedef std::set<std::string /*extension id*/> ScanningExtensionIdSet;
// When multiple scan results have the same parent, sometimes it makes sense
// to combine them into a single scan result at the parent. This constant
// governs when that happens; kContainerDirectoryMinimumPercent percent of the
// directories in the parent directory must be scan results.
const int kContainerDirectoryMinimumPercent = 80;
// How long after a completed media scan can we provide the cached results.
const int kScanResultsExpiryTimeInHours = 24;
struct LocationInfo {
LocationInfo()
: pref_id(kInvalidMediaGalleryPrefId),
type(MediaGalleryPrefInfo::kInvalidType) {}
LocationInfo(MediaGalleryPrefId pref_id, MediaGalleryPrefInfo::Type type,
base::FilePath path)
: pref_id(pref_id), type(type), path(path) {}
// Highest priority comparison by path, next by type (scan result last),
// then by pref id (invalid last).
bool operator<(const LocationInfo& rhs) const {
if (path.value() == rhs.path.value()) {
if (type == rhs.type) {
return pref_id > rhs.pref_id;
}
return rhs.type == MediaGalleryPrefInfo::kScanResult;
}
return path.value() < rhs.path.value();
}
MediaGalleryPrefId pref_id;
MediaGalleryPrefInfo::Type type;
base::FilePath path;
MediaGalleryScanResult file_counts;
};
// Finds new scan results that are shadowed (the same location, or a child) by
// existing locations and moves them from |found_folders| to |child_folders|.
// Also moves new scan results that are shadowed by other new scan results
// to |child_folders|.
void PartitionChildScanResults(
MediaGalleriesPreferences* preferences,
MediaFolderFinder::MediaFolderFinderResults* found_folders,
MediaFolderFinder::MediaFolderFinderResults* child_folders) {
// Construct a list with everything in it.
std::vector<LocationInfo> all_locations;
for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it =
found_folders->begin(); it != found_folders->end(); ++it) {
all_locations.push_back(LocationInfo(kInvalidMediaGalleryPrefId,
MediaGalleryPrefInfo::kScanResult,
it->first));
all_locations.back().file_counts = it->second;
}
const MediaGalleriesPrefInfoMap& known_galleries =
preferences->known_galleries();
for (MediaGalleriesPrefInfoMap::const_iterator it = known_galleries.begin();
it != known_galleries.end();
++it) {
all_locations.push_back(LocationInfo(it->second.pref_id, it->second.type,
it->second.AbsolutePath()));
}
// Sorting on path should put all paths that are prefixes of other paths
// next to each other, with the shortest one first.
std::sort(all_locations.begin(), all_locations.end());
size_t previous_parent_index = 0;
for (size_t i = 1; i < all_locations.size(); i++) {
const LocationInfo& current = all_locations[i];
const LocationInfo& previous_parent = all_locations[previous_parent_index];
bool is_child = previous_parent.path.IsParent(current.path);
if (current.type == MediaGalleryPrefInfo::kScanResult &&
current.pref_id == kInvalidMediaGalleryPrefId &&
(is_child || previous_parent.path == current.path)) {
// Move new scan results that are shadowed.
(*child_folders)[current.path] = current.file_counts;
found_folders->erase(current.path);
} else if (!is_child) {
previous_parent_index = i;
}
}
}
MediaGalleryScanResult SumFilesUnderPath(
const base::FilePath& path,
const MediaFolderFinder::MediaFolderFinderResults& candidates) {
MediaGalleryScanResult results;
for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it =
candidates.begin(); it != candidates.end(); ++it) {
if (it->first == path || path.IsParent(it->first)) {
results.audio_count += it->second.audio_count;
results.image_count += it->second.image_count;
results.video_count += it->second.video_count;
}
}
return results;
}
void AddScanResultsForProfile(
MediaGalleriesPreferences* preferences,
const MediaFolderFinder::MediaFolderFinderResults& found_folders) {
// First, remove any existing scan results where no app has been granted
// permission - either it is gone, or is already in the new scan results.
// This burns some pref ids, but not at an appreciable rate.
MediaGalleryPrefIdSet to_remove;
const MediaGalleriesPrefInfoMap& known_galleries =
preferences->known_galleries();
for (MediaGalleriesPrefInfoMap::const_iterator it = known_galleries.begin();
it != known_galleries.end();
++it) {
if (it->second.type == MediaGalleryPrefInfo::kScanResult &&
!preferences->NonAutoGalleryHasPermission(it->first)) {
to_remove.insert(it->first);
}
}
for (MediaGalleryPrefIdSet::const_iterator it = to_remove.begin();
it != to_remove.end();
++it) {
preferences->EraseGalleryById(*it);
}
MediaFolderFinder::MediaFolderFinderResults child_folders;
MediaFolderFinder::MediaFolderFinderResults
unique_found_folders(found_folders);
PartitionChildScanResults(preferences, &unique_found_folders, &child_folders);
// Updating prefs while iterating them will invalidate the pointer, so
// calculate the changes first and then apply them.
std::map<MediaGalleryPrefId, MediaGalleryScanResult> to_update;
for (MediaGalleriesPrefInfoMap::const_iterator it = known_galleries.begin();
it != known_galleries.end();
++it) {
const MediaGalleryPrefInfo& gallery = it->second;
if (!gallery.IsBlackListedType()) {
MediaGalleryScanResult file_counts =
SumFilesUnderPath(gallery.AbsolutePath(), child_folders);
if (gallery.audio_count != file_counts.audio_count ||
gallery.image_count != file_counts.image_count ||
gallery.video_count != file_counts.video_count) {
to_update[it->first] = file_counts;
}
}
}
for (std::map<MediaGalleryPrefId,
MediaGalleryScanResult>::const_iterator it = to_update.begin();
it != to_update.end();
++it) {
const MediaGalleryPrefInfo& gallery =
preferences->known_galleries().find(it->first)->second;
preferences->AddGallery(gallery.device_id, gallery.path, gallery.type,
gallery.volume_label, gallery.vendor_name,
gallery.model_name, gallery.total_size_in_bytes,
gallery.last_attach_time,
it->second.audio_count,
it->second.image_count,
it->second.video_count);
}
// Add new scan results.
for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it =
unique_found_folders.begin();
it != unique_found_folders.end();
++it) {
MediaGalleryScanResult file_counts =
SumFilesUnderPath(it->first, child_folders);
// The top level scan result is not in |child_folders|. Add it in as well.
file_counts.audio_count += it->second.audio_count;
file_counts.image_count += it->second.image_count;
file_counts.video_count += it->second.video_count;
MediaGalleryPrefInfo gallery;
bool existing = preferences->LookUpGalleryByPath(it->first, &gallery);
DCHECK(!existing);
preferences->AddGallery(gallery.device_id, gallery.path,
MediaGalleryPrefInfo::kScanResult,
gallery.volume_label, gallery.vendor_name,
gallery.model_name, gallery.total_size_in_bytes,
gallery.last_attach_time, file_counts.audio_count,
file_counts.image_count, file_counts.video_count);
}
UMA_HISTOGRAM_COUNTS_10000("MediaGalleries.ScanGalleriesPopulated",
unique_found_folders.size() + to_update.size());
}
int CountScanResultsForExtension(MediaGalleriesPreferences* preferences,
const extensions::Extension* extension,
MediaGalleryScanResult* file_counts) {
int gallery_count = 0;
MediaGalleryPrefIdSet permitted_galleries =
preferences->GalleriesForExtension(*extension);
const MediaGalleriesPrefInfoMap& known_galleries =
preferences->known_galleries();
for (MediaGalleriesPrefInfoMap::const_iterator it = known_galleries.begin();
it != known_galleries.end();
++it) {
if (it->second.type == MediaGalleryPrefInfo::kScanResult &&
!ContainsKey(permitted_galleries, it->first)) {
gallery_count++;
file_counts->audio_count += it->second.audio_count;
file_counts->image_count += it->second.image_count;
file_counts->video_count += it->second.video_count;
}
}
return gallery_count;
}
int CountDirectoryEntries(const base::FilePath& path) {
base::FileEnumerator dir_counter(
path, false /*recursive*/, base::FileEnumerator::DIRECTORIES);
int count = 0;
base::FileEnumerator::FileInfo info;
for (base::FilePath name = dir_counter.Next(); !name.empty();
name = dir_counter.Next()) {
if (!base::IsLink(name))
++count;
}
return count;
}
struct ContainerCount {
int seen_count, entries_count;
bool is_qualified;
ContainerCount() : seen_count(0), entries_count(-1), is_qualified(false) {}
};
typedef std::map<base::FilePath, ContainerCount> ContainerCandidates;
} // namespace
MediaScanManager::MediaScanManager()
: scoped_extension_registry_observer_(this),
weak_factory_(this) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
MediaScanManager::~MediaScanManager() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
void MediaScanManager::AddObserver(Profile* profile,
MediaScanManagerObserver* observer) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(!ContainsKey(observers_, profile));
observers_[profile].observer = observer;
}
void MediaScanManager::RemoveObserver(Profile* profile) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
bool scan_in_progress = ScanInProgress();
observers_.erase(profile);
DCHECK_EQ(scan_in_progress, ScanInProgress());
}
void MediaScanManager::CancelScansForProfile(Profile* profile) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
observers_[profile].scanning_extensions.clear();
if (!ScanInProgress())
folder_finder_.reset();
}
void MediaScanManager::StartScan(Profile* profile,
const extensions::Extension* extension,
bool user_gesture) {
DCHECK(extension);
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
ScanObserverMap::iterator scans_for_profile = observers_.find(profile);
// We expect that an MediaScanManagerObserver has already been registered.
DCHECK(scans_for_profile != observers_.end());
bool scan_in_progress = ScanInProgress();
// Ignore requests for extensions that are already scanning.
ScanningExtensionIdSet* scanning_extensions;
scanning_extensions = &scans_for_profile->second.scanning_extensions;
if (scan_in_progress && ContainsKey(*scanning_extensions, extension->id()))
return;
// Provide cached result if there is not already a scan in progress,
// there is no user gesture, and the previous results are unexpired.
MediaGalleriesPreferences* preferences =
MediaGalleriesPreferencesFactory::GetForProfile(profile);
base::TimeDelta time_since_last_scan =
base::Time::Now() - preferences->GetLastScanCompletionTime();
if (!scan_in_progress && !user_gesture && time_since_last_scan <
base::TimeDelta::FromHours(kScanResultsExpiryTimeInHours)) {
MediaGalleryScanResult file_counts;
int gallery_count =
CountScanResultsForExtension(preferences, extension, &file_counts);
scans_for_profile->second.observer->OnScanStarted(extension->id());
scans_for_profile->second.observer->OnScanFinished(extension->id(),
gallery_count,
file_counts);
return;
}
// On first scan for the |profile|, register to listen for extension unload.
if (scanning_extensions->empty())
scoped_extension_registry_observer_.Add(ExtensionRegistry::Get(profile));
scanning_extensions->insert(extension->id());
scans_for_profile->second.observer->OnScanStarted(extension->id());
if (folder_finder_)
return;
MediaFolderFinder::MediaFolderFinderResultsCallback callback =
base::Bind(&MediaScanManager::OnScanCompleted,
weak_factory_.GetWeakPtr());
if (testing_folder_finder_factory_.is_null()) {
folder_finder_.reset(new MediaFolderFinder(callback));
} else {
folder_finder_.reset(testing_folder_finder_factory_.Run(callback));
}
scan_start_time_ = base::Time::Now();
folder_finder_->StartScan();
}
void MediaScanManager::CancelScan(Profile* profile,
const extensions::Extension* extension) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Erases the logical scan if found, early exit otherwise.
ScanObserverMap::iterator scans_for_profile = observers_.find(profile);
if (scans_for_profile == observers_.end() ||
!scans_for_profile->second.scanning_extensions.erase(extension->id())) {
return;
}
scans_for_profile->second.observer->OnScanCancelled(extension->id());
// No more scanning extensions for |profile|, so stop listening for unloads.
if (scans_for_profile->second.scanning_extensions.empty())
scoped_extension_registry_observer_.Remove(ExtensionRegistry::Get(profile));
if (!ScanInProgress()) {
folder_finder_.reset();
DCHECK(!scan_start_time_.is_null());
UMA_HISTOGRAM_LONG_TIMES("MediaGalleries.ScanCancelTime",
base::Time::Now() - scan_start_time_);
scan_start_time_ = base::Time();
}
}
void MediaScanManager::SetMediaFolderFinderFactory(
const MediaFolderFinderFactory& factory) {
testing_folder_finder_factory_ = factory;
}
// A single directory may contain many folders with media in them, without
// containing any media itself. In fact, the primary purpose of that directory
// may be to contain media directories. This function tries to find those
// container directories.
MediaFolderFinder::MediaFolderFinderResults
MediaScanManager::FindContainerScanResults(
const MediaFolderFinder::MediaFolderFinderResults& found_folders,
const std::vector<base::FilePath>& sensitive_locations) {
DCHECK_CURRENTLY_ON(content::BrowserThread::FILE);
std::vector<base::FilePath> abs_sensitive_locations;
for (size_t i = 0; i < sensitive_locations.size(); ++i) {
base::FilePath path = base::MakeAbsoluteFilePath(sensitive_locations[i]);
if (!path.empty())
abs_sensitive_locations.push_back(path);
}
// Recursively find parent directories with majority of media directories,
// or container directories.
// |candidates| keeps track of directories which might have enough
// such directories to have us return them.
typedef std::map<base::FilePath, ContainerCount> ContainerCandidates;
ContainerCandidates candidates;
for (MediaFolderFinder::MediaFolderFinderResults::const_iterator it =
found_folders.begin();
it != found_folders.end();
++it) {
base::FilePath child_directory = it->first;
base::FilePath parent_directory = child_directory.DirName();
// Parent of root is root.
while (!parent_directory.empty() && child_directory != parent_directory) {
// Skip sensitive folders and their ancestors.
base::FilePath abs_parent_directory =
base::MakeAbsoluteFilePath(parent_directory);
if (abs_parent_directory.empty())
break;
bool is_sensitive = false;
for (size_t i = 0; i < abs_sensitive_locations.size(); ++i) {
if (abs_parent_directory == abs_sensitive_locations[i] ||
abs_parent_directory.IsParent(abs_sensitive_locations[i])) {
is_sensitive = true;
break;
}
}
if (is_sensitive)
break;
// Don't bother with ones we already have.
if (found_folders.find(parent_directory) != found_folders.end())
continue;
ContainerCandidates::iterator parent_it =
candidates.find(parent_directory);
if (parent_it == candidates.end()) {
ContainerCount count;
count.seen_count = 1;
count.entries_count = CountDirectoryEntries(parent_directory);
parent_it =
candidates.insert(std::make_pair(parent_directory, count)).first;
} else {
++candidates[parent_directory].seen_count;
}
// If previously sufficient, or not sufficient, bail.
if (parent_it->second.is_qualified ||
parent_it->second.seen_count * 100 / parent_it->second.entries_count <
kContainerDirectoryMinimumPercent) {
break;
}
// Otherwise, mark qualified and check parent.
parent_it->second.is_qualified = true;
child_directory = parent_directory;
parent_directory = child_directory.DirName();
}
}
MediaFolderFinder::MediaFolderFinderResults result;
// Copy and return worthy results.
for (ContainerCandidates::const_iterator it = candidates.begin();
it != candidates.end();
++it) {
if (it->second.is_qualified && it->second.seen_count >= 2)
result[it->first] = MediaGalleryScanResult();
}
return result;
}
MediaScanManager::ScanObservers::ScanObservers() : observer(NULL) {}
MediaScanManager::ScanObservers::~ScanObservers() {}
void MediaScanManager::OnExtensionUnloaded(
content::BrowserContext* browser_context,
const extensions::Extension* extension,
extensions::UnloadedExtensionInfo::Reason reason) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
CancelScan(Profile::FromBrowserContext(browser_context), extension);
}
bool MediaScanManager::ScanInProgress() const {
for (ScanObserverMap::const_iterator it = observers_.begin();
it != observers_.end();
++it) {
if (!it->second.scanning_extensions.empty())
return true;
}
return false;
}
void MediaScanManager::OnScanCompleted(
bool success,
const MediaFolderFinder::MediaFolderFinderResults& found_folders) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!folder_finder_ || !success) {
folder_finder_.reset();
return;
}
UMA_HISTOGRAM_COUNTS_10000("MediaGalleries.ScanDirectoriesFound",
found_folders.size());
DCHECK(!scan_start_time_.is_null());
UMA_HISTOGRAM_LONG_TIMES("MediaGalleries.ScanFinishedTime",
base::Time::Now() - scan_start_time_);
scan_start_time_ = base::Time();
content::BrowserThread::PostTaskAndReplyWithResult(
content::BrowserThread::FILE, FROM_HERE,
base::Bind(FindContainerScanResults,
found_folders,
folder_finder_->graylisted_folders()),
base::Bind(&MediaScanManager::OnFoundContainerDirectories,
weak_factory_.GetWeakPtr(),
found_folders));
}
void MediaScanManager::OnFoundContainerDirectories(
const MediaFolderFinder::MediaFolderFinderResults& found_folders,
const MediaFolderFinder::MediaFolderFinderResults& container_folders) {
MediaFolderFinder::MediaFolderFinderResults folders;
folders.insert(found_folders.begin(), found_folders.end());
folders.insert(container_folders.begin(), container_folders.end());
for (ScanObserverMap::iterator scans_for_profile = observers_.begin();
scans_for_profile != observers_.end();
++scans_for_profile) {
if (scans_for_profile->second.scanning_extensions.empty())
continue;
Profile* profile = scans_for_profile->first;
MediaGalleriesPreferences* preferences =
MediaGalleriesPreferencesFactory::GetForProfile(profile);
ExtensionService* extension_service =
extensions::ExtensionSystem::Get(profile)->extension_service();
if (!extension_service)
continue;
AddScanResultsForProfile(preferences, folders);
ScanningExtensionIdSet* scanning_extensions =
&scans_for_profile->second.scanning_extensions;
for (ScanningExtensionIdSet::const_iterator extension_id_it =
scanning_extensions->begin();
extension_id_it != scanning_extensions->end();
++extension_id_it) {
const extensions::Extension* extension =
extension_service->GetExtensionById(*extension_id_it, false);
if (extension) {
MediaGalleryScanResult file_counts;
int gallery_count = CountScanResultsForExtension(preferences, extension,
&file_counts);
scans_for_profile->second.observer->OnScanFinished(*extension_id_it,
gallery_count,
file_counts);
}
}
scanning_extensions->clear();
preferences->SetLastScanCompletionTime(base::Time::Now());
}
scoped_extension_registry_observer_.RemoveAll();
folder_finder_.reset();
}