diff options
author | vandebo@chromium.org <vandebo@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-01-27 02:42:00 +0000 |
---|---|---|
committer | vandebo@chromium.org <vandebo@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-01-27 02:42:00 +0000 |
commit | 617d2c4035bfea87ed9adeab976f648c8a4eb8a6 (patch) | |
tree | 664bb8fcd69d50982fc21d307274ed832b1127e1 | |
parent | b756ed998c006bbec9564a22f47637e5faa75136 (diff) | |
download | chromium_src-617d2c4035bfea87ed9adeab976f648c8a4eb8a6.zip chromium_src-617d2c4035bfea87ed9adeab976f648c8a4eb8a6.tar.gz chromium_src-617d2c4035bfea87ed9adeab976f648c8a4eb8a6.tar.bz2 |
Media galleries scan result dialog
Used to show locations where media files where found during a disk scan.
BUG=161119
Review URL: https://codereview.chromium.org/137783027
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@247182 0039d316-1c4b-4281-b951-d872f2087c98
13 files changed, 1445 insertions, 10 deletions
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd index b7c3a2d..20209c1 100644 --- a/chrome/app/generated_resources.grd +++ b/chrome/app/generated_resources.grd @@ -15099,15 +15099,30 @@ Do you accept? <message name="IDS_MEDIA_GALLERIES_DIALOG_HEADER" desc="Header for media gallery permissions dialog."> Media-File Permissions for "<ph name="EXTENSION">$1<ex>Photo Editor</ex></ph>" </message> - <message name="IDS_MEDIA_GALLERIES_DIALOG_SUBTEXT_READ_WRITE" desc="Explanatory text for the media gallery access permission. Indicates than the specified (by name) extension has read and write access to the listed folders."> + <message name="IDS_MEDIA_GALLERIES_DIALOG_SUBTEXT_READ_WRITE" desc="Explanatory text for the media gallery access permission. Indicates that the specified (by name) extension has read and write access to the listed folders."> "<ph name="EXTENSION">$1<ex>PhotoEditor</ex></ph>" can read and write images, video, and sound files in the checked locations. </message> - <message name="IDS_MEDIA_GALLERIES_DIALOG_SUBTEXT_READ_DELETE" desc="Explanatory text for the media gallery access permission. Indicates than the specified (by name) extension has read and delete access to the listed folders."> + <message name="IDS_MEDIA_GALLERIES_DIALOG_SUBTEXT_READ_DELETE" desc="Explanatory text for the media gallery access permission. Indicates that the specified (by name) extension has read and delete access to the listed folders."> "<ph name="EXTENSION">$1<ex>PhotoEditor</ex></ph>" can read and delete images, video, and sound files in the checked locations. </message> - <message name="IDS_MEDIA_GALLERIES_DIALOG_SUBTEXT_READ_ONLY" desc="Explanatory text for the media gallery access permission. Indicates than the specified (by name) extension has read-only access to the listed folders."> + <message name="IDS_MEDIA_GALLERIES_DIALOG_SUBTEXT_READ_ONLY" desc="Explanatory text for the media gallery access permission. Indicates that the specified (by name) extension has read-only access to the listed folders."> "<ph name="EXTENSION">$1<ex>PhotoEditor</ex></ph>" can read images, video, and sound files in the checked locations. </message> + <message name="IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_HEADER" desc="Header for media gallery scan result dialog."> + Add folders to "<ph name="EXTENSION">$1<ex>Photo Editor</ex></ph>"? + </message> + <message name="IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_SUBTEXT_READ_WRITE" desc="Explanatory text for the media gallery scan result dialog. Indicates that the specified (by name) extension will have read and write access to the selected folders."> + "<ph name="EXTENSION">$1<ex>PhotoEditor</ex></ph>" will be able to read and write images, video, and sound files in the checked folders. + </message> + <message name="IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_SUBTEXT_READ_DELETE" desc="Explanatory text for the media gallery scan result dialog. Indicates that the specified (by name) extension will have read and delete access to the selected folders."> + "<ph name="EXTENSION">$1<ex>PhotoEditor</ex></ph>" will be able to read and delete images, video, and sound files in the checked folders. + </message> + <message name="IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_SUBTEXT_READ_ONLY" desc="Explanatory text for the media gallery scan result dialog. Indicates that the specified (by name) extension will have read-only access to the selected folders."> + "<ph name="EXTENSION">$1<ex>PhotoEditor</ex></ph>" will be able to read images, video, and sound files in the checked folders. + </message> + <message name="IDS_MEDIA_GALLERIES_SCAN_RESULT_OPEN_FOLDER_VIEW_ACCESSIBILITY_NAME" desc="Accessibility name for an icon that opens the folder view of a gallery from the media galleries scan result dialog."> + Show in folder + </message> <message name="IDS_MEDIA_GALLERIES_UNATTACHED_LOCATIONS" desc="Header for media gallery permissions to locations not currently attached, i.e. removable devices which aren't plugged in"> Unattached locations </message> diff --git a/chrome/browser/extensions/api/media_galleries/media_galleries_api.cc b/chrome/browser/extensions/api/media_galleries/media_galleries_api.cc index cff29b6..8f1c48d 100644 --- a/chrome/browser/extensions/api/media_galleries/media_galleries_api.cc +++ b/chrome/browser/extensions/api/media_galleries/media_galleries_api.cc @@ -25,6 +25,7 @@ #include "chrome/browser/media_galleries/media_galleries_dialog_controller.h" #include "chrome/browser/media_galleries/media_galleries_histograms.h" #include "chrome/browser/media_galleries/media_galleries_preferences.h" +#include "chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller.h" #include "chrome/browser/media_galleries/media_scan_manager.h" #include "chrome/browser/platform_util.h" #include "chrome/browser/profiles/profile.h" @@ -647,7 +648,44 @@ bool MediaGalleriesAddScanResultsFunction::RunImpl() { void MediaGalleriesAddScanResultsFunction::OnPreferencesInit() { DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); + const Extension* extension = GetExtension(); + WebContents* contents = + GetWebContents(render_view_host(), GetProfile(), extension->id()); + if (!contents) { + SendResponse(false); + return; + } + // Controller will delete itself. + base::Closure cb = base::Bind( + &MediaGalleriesAddScanResultsFunction::GetAndReturnGalleries, this); + new MediaGalleriesScanResultDialogController(contents, *extension, cb); +} + +void MediaGalleriesAddScanResultsFunction::GetAndReturnGalleries() { + if (!render_view_host()) { + ReturnGalleries(std::vector<MediaFileSystemInfo>()); + return; + } + MediaFileSystemRegistry* registry = media_file_system_registry(); + DCHECK(registry->GetPreferences(GetProfile())->IsInitialized()); + registry->GetMediaFileSystemsForExtension( + render_view_host(), GetExtension(), + base::Bind(&MediaGalleriesAddScanResultsFunction::ReturnGalleries, + this)); +} + +void MediaGalleriesAddScanResultsFunction::ReturnGalleries( + const std::vector<MediaFileSystemInfo>& filesystems) { + scoped_ptr<base::ListValue> list( + ConstructFileSystemList(render_view_host(), GetExtension(), filesystems)); + if (!list.get()) { + SendResponse(false); + return; + } + + // The custom JS binding will use this list to create DOMFileSystem objects. + SetResult(list.release()); SendResponse(true); } diff --git a/chrome/browser/extensions/api/media_galleries/media_galleries_api.h b/chrome/browser/extensions/api/media_galleries/media_galleries_api.h index d8dde4e..4e85b68 100644 --- a/chrome/browser/extensions/api/media_galleries/media_galleries_api.h +++ b/chrome/browser/extensions/api/media_galleries/media_galleries_api.h @@ -206,6 +206,13 @@ class MediaGalleriesAddScanResultsFunction private: // Bottom half for RunImpl, invoked after the preferences is initialized. void OnPreferencesInit(); + + // Grabs galleries from the media file system registry and passes them to + // ReturnGalleries(). + void GetAndReturnGalleries(); + + // Returns galleries to the caller. + void ReturnGalleries(const std::vector<MediaFileSystemInfo>& filesystems); }; class MediaGalleriesGetMetadataFunction : public ChromeAsyncExtensionFunction { diff --git a/chrome/browser/media_galleries/media_galleries_dialog_controller.cc b/chrome/browser/media_galleries/media_galleries_dialog_controller.cc index 6d425e90..3dfbb27 100644 --- a/chrome/browser/media_galleries/media_galleries_dialog_controller.cc +++ b/chrome/browser/media_galleries/media_galleries_dialog_controller.cc @@ -389,9 +389,8 @@ void MediaGalleriesDialogController::InitializePermissions() { iter != galleries.end(); ++iter) { const MediaGalleryPrefInfo& gallery = iter->second; - if (gallery.type == MediaGalleryPrefInfo::kBlackListed) { + if (gallery.IsBlackListedType()) continue; - } known_galleries_[iter->first] = GalleryPermission(gallery, false); } diff --git a/chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller.cc b/chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller.cc new file mode 100644 index 0000000..befcd8a --- /dev/null +++ b/chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller.cc @@ -0,0 +1,319 @@ +// 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_galleries_scan_result_dialog_controller.h" + +#include <algorithm> +#include <list> + +#include "base/bind.h" +#include "base/logging.h" +#include "base/stl_util.h" +#include "base/strings/utf_string_conversions.h" +#include "chrome/browser/browser_process.h" +#include "chrome/browser/media_galleries/media_file_system_registry.h" +#include "chrome/browser/media_galleries/media_galleries_histograms.h" +#include "chrome/browser/platform_util.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/storage_monitor/storage_info.h" +#include "chrome/browser/storage_monitor/storage_monitor.h" +#include "chrome/common/extensions/permissions/media_galleries_permission.h" +#include "content/public/browser/web_contents.h" +#include "extensions/common/extension.h" +#include "extensions/common/permissions/permissions_data.h" +#include "grit/generated_resources.h" +#include "ui/base/l10n/l10n_util.h" + +namespace { + +// Comparator for sorting OrderedScanResults -- more files first and then sorts +// by absolute path. +bool ScanResultsComparator( + const MediaGalleriesScanResultDialogController::ScanResult& a, + const MediaGalleriesScanResultDialogController::ScanResult& b) { + int a_media_count = a.pref_info.image_count + a.pref_info.music_count + + a.pref_info.video_count; + int b_media_count = b.pref_info.image_count + b.pref_info.music_count + + b.pref_info.video_count; + if (a_media_count == b_media_count) + return a.pref_info.AbsolutePath() < b.pref_info.AbsolutePath(); + return a_media_count > b_media_count; +} + +} // namespace + +MediaGalleriesScanResultDialogController:: +MediaGalleriesScanResultDialogController( + content::WebContents* web_contents, + const extensions::Extension& extension, + const base::Closure& on_finish) + : web_contents_(web_contents), + extension_(&extension), + on_finish_(on_finish) { + // TODO(vandebo): Put this in the intializer list after the cocoa version is + // done. +#if defined(USE_AURA) + create_dialog_callback_ = base::Bind(&MediaGalleriesScanResultDialog::Create); +#endif // USE_AURA + preferences_ = + g_browser_process->media_file_system_registry()->GetPreferences( + GetProfile()); + // Passing unretained pointer is safe, since the dialog controller + // is self-deleting, and so won't be deleted until it can be shown + // and then closed. + preferences_->EnsureInitialized(base::Bind( + &MediaGalleriesScanResultDialogController::OnPreferencesInitialized, + base::Unretained(this))); +} + +MediaGalleriesScanResultDialogController:: +MediaGalleriesScanResultDialogController( + const extensions::Extension& extension, + MediaGalleriesPreferences* preferences, + const CreateDialogCallback& create_dialog_callback, + const base::Closure& on_finish) + : web_contents_(NULL), + extension_(&extension), + on_finish_(on_finish), + preferences_(preferences), + create_dialog_callback_(create_dialog_callback) { + OnPreferencesInitialized(); +} + +MediaGalleriesScanResultDialogController:: +~MediaGalleriesScanResultDialogController() { + preferences_->RemoveGalleryChangeObserver(this); + if (StorageMonitor::GetInstance()) + StorageMonitor::GetInstance()->RemoveObserver(this); +} + +base::string16 MediaGalleriesScanResultDialogController::GetHeader() const { + return l10n_util::GetStringFUTF16( + IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_HEADER, + base::UTF8ToUTF16(extension_->name())); +} + +base::string16 MediaGalleriesScanResultDialogController::GetSubtext() const { + extensions::MediaGalleriesPermission::CheckParam copy_to_param( + extensions::MediaGalleriesPermission::kCopyToPermission); + extensions::MediaGalleriesPermission::CheckParam delete_param( + extensions::MediaGalleriesPermission::kDeletePermission); + bool has_copy_to_permission = + extensions::PermissionsData::CheckAPIPermissionWithParam( + extension_, extensions::APIPermission::kMediaGalleries, + ©_to_param); + bool has_delete_permission = + extensions::PermissionsData::CheckAPIPermissionWithParam( + extension_, extensions::APIPermission::kMediaGalleries, + &delete_param); + + int id; + if (has_copy_to_permission) + id = IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_SUBTEXT_READ_WRITE; + else if (has_delete_permission) + id = IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_SUBTEXT_READ_DELETE; + else + id = IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_SUBTEXT_READ_ONLY; + + return l10n_util::GetStringFUTF16(id, base::UTF8ToUTF16(extension_->name())); +} + +MediaGalleriesScanResultDialogController::OrderedScanResults +MediaGalleriesScanResultDialogController::GetGalleryList() const { + OrderedScanResults result; + result.reserve(scan_results_.size()); + for (ScanResults::const_iterator it = scan_results_.begin(); + it != scan_results_.end(); + ++it) { + result.push_back(it->second); + } + std::sort(result.begin(), result.end(), ScanResultsComparator); + return result; +} + +void MediaGalleriesScanResultDialogController::DidToggleGalleryId( + MediaGalleryPrefId pref_id, bool selected) { + DCHECK(ContainsKey(scan_results_, pref_id)); + ScanResults::iterator entry = scan_results_.find(pref_id); + entry->second.selected = selected; +} + +void MediaGalleriesScanResultDialogController::DidClickOpenFolderViewer( + MediaGalleryPrefId pref_id) const { + ScanResults::const_iterator entry = scan_results_.find(pref_id); + if (entry == scan_results_.end()) { + NOTREACHED(); + return; + } + platform_util::OpenItem(GetProfile(), entry->second.pref_info.AbsolutePath()); +} + +void MediaGalleriesScanResultDialogController::DidForgetGallery( + MediaGalleryPrefId pref_id) { + results_to_remove_.insert(pref_id); + scan_results_.erase(pref_id); + dialog_->UpdateResults(); +} + +void MediaGalleriesScanResultDialogController::DialogFinished(bool accepted) { + // No longer interested in preference updates (and the below code generates + // some). + preferences_->RemoveGalleryChangeObserver(this); + + if (accepted) { + DCHECK(preferences_); + for (ScanResults::const_iterator it = scan_results_.begin(); + it != scan_results_.end(); + ++it) { + if (it->second.selected) { + bool changed = preferences_->SetGalleryPermissionForExtension( + *extension_, it->first, true); + DCHECK(changed); + } + } + for (MediaGalleryPrefIdSet::const_iterator it = results_to_remove_.begin(); + it != results_to_remove_.end(); + ++it) { + preferences_->ForgetGalleryById(*it); + } + } + + on_finish_.Run(); + delete this; +} + +content::WebContents* MediaGalleriesScanResultDialogController::web_contents() { + return web_contents_; +} + +void MediaGalleriesScanResultDialogController::OnPreferencesInitialized() { + preferences_->AddGalleryChangeObserver(this); + StorageMonitor::GetInstance()->AddObserver(this); + UpdateFromPreferences(); + + // TODO(vandebo): Remove the conditional after the cocoa version is done. + if (!create_dialog_callback_.is_null()) + dialog_.reset(create_dialog_callback_.Run(this)); +} + +void MediaGalleriesScanResultDialogController::UpdateFromPreferences() { + const MediaGalleriesPrefInfoMap& galleries = preferences_->known_galleries(); + MediaGalleryPrefIdSet permitted = + preferences_->GalleriesForExtension(*extension_); + + // Add or update any scan results that the extension doesn't already have + // access to or isn't in |results_to_remove_|. + for (MediaGalleriesPrefInfoMap::const_iterator it = galleries.begin(); + it != galleries.end(); + ++it) { + const MediaGalleryPrefInfo& gallery = it->second; + if (gallery.type == MediaGalleryPrefInfo::kScanResult && + !ContainsKey(permitted, gallery.pref_id) && + !ContainsKey(results_to_remove_, gallery.pref_id)) { + ScanResults::iterator existing = scan_results_.find(gallery.pref_id); + if (existing == scan_results_.end()) { + // Default to selected. + scan_results_[gallery.pref_id] = ScanResult(gallery, true); + } else { + // Update pref_info, in case anything has been updated. + existing->second.pref_info = gallery; + } + } + } + + // Remove anything from |scan_results_| that's no longer valid or the user + // already has access to. + std::list<ScanResults::iterator> to_remove; + for (ScanResults::iterator it = scan_results_.begin(); + it != scan_results_.end(); + ++it) { + MediaGalleriesPrefInfoMap::const_iterator pref_gallery = + galleries.find(it->first); + if (pref_gallery == galleries.end() || + pref_gallery->second.type != MediaGalleryPrefInfo::kScanResult || + permitted.find(it->first) != permitted.end()) { + to_remove.push_back(it); + } + } + while (!to_remove.empty()) { + scan_results_.erase(to_remove.front()); + to_remove.pop_front(); + } +} + +void MediaGalleriesScanResultDialogController::OnPreferenceUpdate( + const std::string& extension_id, MediaGalleryPrefId pref_id) { + if (extension_id == extension_->id()) { + const MediaGalleriesPrefInfoMap::const_iterator it = + preferences_->known_galleries().find(pref_id); + if (it == preferences_->known_galleries().end() || + it->second.type == MediaGalleryPrefInfo::kScanResult || + it->second.type == MediaGalleryPrefInfo::kRemovedScan) { + UpdateFromPreferences(); + dialog_->UpdateResults(); + } + } +} + +void MediaGalleriesScanResultDialogController::OnRemovableDeviceUpdate( + const std::string device_id) { + for (ScanResults::const_iterator it = scan_results_.begin(); + it != scan_results_.end(); + ++it) { + if (it->second.pref_info.device_id == device_id) { + dialog_->UpdateResults(); + return; + } + } +} + +Profile* MediaGalleriesScanResultDialogController::GetProfile() const { + return Profile::FromBrowserContext(web_contents_->GetBrowserContext()); +} + +void MediaGalleriesScanResultDialogController::OnRemovableStorageAttached( + const StorageInfo& info) { + OnRemovableDeviceUpdate(info.device_id()); +} + +void MediaGalleriesScanResultDialogController::OnRemovableStorageDetached( + const StorageInfo& info) { + OnRemovableDeviceUpdate(info.device_id()); +} + +void MediaGalleriesScanResultDialogController::OnPermissionAdded( + MediaGalleriesPreferences* /* pref */, + const std::string& extension_id, + MediaGalleryPrefId pref_id) { + OnPreferenceUpdate(extension_id, pref_id); +} + +void MediaGalleriesScanResultDialogController::OnPermissionRemoved( + MediaGalleriesPreferences* /* pref */, + const std::string& extension_id, + MediaGalleryPrefId pref_id) { + OnPreferenceUpdate(extension_id, pref_id); +} + +void MediaGalleriesScanResultDialogController::OnGalleryAdded( + MediaGalleriesPreferences* /* prefs */, + MediaGalleryPrefId pref_id) { + OnPreferenceUpdate(extension_->id(), pref_id); +} + +void MediaGalleriesScanResultDialogController::OnGalleryRemoved( + MediaGalleriesPreferences* /* prefs */, + MediaGalleryPrefId pref_id) { + OnPreferenceUpdate(extension_->id(), pref_id); +} + +void MediaGalleriesScanResultDialogController::OnGalleryInfoUpdated( + MediaGalleriesPreferences* /* prefs */, + MediaGalleryPrefId pref_id) { + OnPreferenceUpdate(extension_->id(), pref_id); +} + +// MediaGalleriesScanResultDialog --------------------------------------------- + +MediaGalleriesScanResultDialog::~MediaGalleriesScanResultDialog() {} diff --git a/chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller.h b/chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller.h new file mode 100644 index 0000000..28754a7 --- /dev/null +++ b/chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller.h @@ -0,0 +1,172 @@ +// 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. + +#ifndef CHROME_BROWSER_MEDIA_GALLERIES_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_CONTROLLER_H_ +#define CHROME_BROWSER_MEDIA_GALLERIES_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_CONTROLLER_H_ + +#include <map> +#include <set> +#include <string> + +#include "base/callback.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string16.h" +#include "chrome/browser/media_galleries/media_galleries_preferences.h" +#include "chrome/browser/storage_monitor/removable_storage_observer.h" + +namespace content { +class WebContents; +} + +namespace extensions { +class Extension; +} + +class MediaGalleriesScanResultDialogController; +class Profile; + +// The view. +class MediaGalleriesScanResultDialog { + public: + virtual ~MediaGalleriesScanResultDialog(); + + // Tell the dialog to update its display list of scan results. + virtual void UpdateResults() = 0; + + // Constructs a platform-specific dialog owned and controlled by |controller|. + static MediaGalleriesScanResultDialog* Create( + MediaGalleriesScanResultDialogController* controller); +}; + +// The controller is responsible for handling the logic of the dialog and +// interfacing with the model (i.e., MediaGalleriesPreferences). It shows +// the dialog and owns itself. +class MediaGalleriesScanResultDialogController + : public RemovableStorageObserver, + public MediaGalleriesPreferences::GalleryChangeObserver { + public: + struct ScanResult { + ScanResult(const MediaGalleryPrefInfo& pref_info, bool selected) + : pref_info(pref_info), + selected(selected) { + } + ScanResult() {} + + MediaGalleryPrefInfo pref_info; + bool selected; + }; + typedef std::vector<ScanResult> OrderedScanResults; + + // The constructor creates a dialog controller which owns itself. + MediaGalleriesScanResultDialogController( + content::WebContents* web_contents, + const extensions::Extension& extension, + const base::Closure& on_finish); + + // The title of the dialog view. + base::string16 GetHeader() const; + + // Explanatory text directly below the title. + base::string16 GetSubtext() const; + + // Get the scan results and their current selection state. + virtual OrderedScanResults GetGalleryList() const; + + // A checkbox beside a scan result was toggled. + virtual void DidToggleGalleryId(MediaGalleryPrefId pref_id, bool selected); + + // A folder viewer icon was clicked. + virtual void DidClickOpenFolderViewer(MediaGalleryPrefId pref_id) const; + + // The forget command in the context menu was selected. + virtual void DidForgetGallery(MediaGalleryPrefId pref_id); + + // The dialog is being deleted. + virtual void DialogFinished(bool accepted); + + virtual content::WebContents* web_contents(); + + private: + friend class MediaGalleriesScanResultDialogControllerTest; + + typedef std::map<MediaGalleryPrefId, ScanResult> ScanResults; + typedef base::Callback<MediaGalleriesScanResultDialog* ( + MediaGalleriesScanResultDialogController*)> CreateDialogCallback; + + // Used for unit tests. + MediaGalleriesScanResultDialogController( + const extensions::Extension& extension, + MediaGalleriesPreferences* preferences_, + const CreateDialogCallback& create_dialog_callback, + const base::Closure& on_finish); + + virtual ~MediaGalleriesScanResultDialogController(); + + // Bottom half of constructor -- called when |preferences_| is initialized. + void OnPreferencesInitialized(); + + // Update the controller state from preferences. + void UpdateFromPreferences(); + + // Used to keep the dialog in sync with the preferences. + void OnPreferenceUpdate(const std::string& extension_id, + MediaGalleryPrefId pref_id); + + // Used to keep the dialog in sync with attached and detached devices. + void OnRemovableDeviceUpdate(const std::string device_id); + + Profile* GetProfile() const; + + // RemovableStorageObserver implementation. + // Used to keep dialog in sync with removable device status. + virtual void OnRemovableStorageAttached(const StorageInfo& info) OVERRIDE; + virtual void OnRemovableStorageDetached(const StorageInfo& info) OVERRIDE; + + // MediaGalleriesPreferences::GalleryChangeObserver implementations. + // Used to keep the dialog in sync when the preferences change. + virtual void OnPermissionAdded(MediaGalleriesPreferences* pref, + const std::string& extension_id, + MediaGalleryPrefId pref_id) OVERRIDE; + virtual void OnPermissionRemoved(MediaGalleriesPreferences* pref, + const std::string& extension_id, + MediaGalleryPrefId pref_id) OVERRIDE; + virtual void OnGalleryAdded(MediaGalleriesPreferences* pref, + MediaGalleryPrefId pref_id) OVERRIDE; + virtual void OnGalleryRemoved(MediaGalleriesPreferences* pref, + MediaGalleryPrefId pref_id) OVERRIDE; + virtual void OnGalleryInfoUpdated(MediaGalleriesPreferences* pref, + MediaGalleryPrefId pref_id) OVERRIDE; + + // The web contents from which the request originated. + content::WebContents* web_contents_; + + // This is just a reference, but it's assumed that it won't become invalid + // while the dialog is showing. + const extensions::Extension* extension_; + + // The scan results that aren't blacklisted and this extension doesn't + // already have access to. + ScanResults scan_results_; + + // The set of scan results which should be removed (blacklisted) - unless + // the user clicks Cancel. + MediaGalleryPrefIdSet results_to_remove_; + + // Callback to run when the dialog closes. + base::Closure on_finish_; + + // The model that tracks galleries and extensions' permissions. + // This is the authoritative source for gallery information. + MediaGalleriesPreferences* preferences_; + + // Creates the dialog. Only changed for unit tests. + CreateDialogCallback create_dialog_callback_; + + // The view that's showing. + scoped_ptr<MediaGalleriesScanResultDialog> dialog_; + + DISALLOW_COPY_AND_ASSIGN(MediaGalleriesScanResultDialogController); +}; + +#endif // CHROME_BROWSER_MEDIA_GALLERIES_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_CONTROLLER_H_ diff --git a/chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller_unittest.cc b/chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller_unittest.cc new file mode 100644 index 0000000..4f48d9c --- /dev/null +++ b/chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller_unittest.cc @@ -0,0 +1,478 @@ +// 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 "base/bind.h" +#include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/memory/scoped_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/run_loop.h" +#include "base/strings/string16.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "chrome/browser/extensions/extension_system.h" +#include "chrome/browser/extensions/test_extension_system.h" +#include "chrome/browser/media_galleries/media_galleries_preferences.h" +#include "chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller.h" +#include "chrome/browser/media_galleries/media_galleries_test_util.h" +#include "chrome/browser/storage_monitor/test_storage_monitor.h" +#include "chrome/common/extensions/permissions/media_galleries_permission.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/test/test_browser_thread_bundle.h" +#include "extensions/common/extension.h" +#include "testing/gtest/include/gtest/gtest.h" + +#if defined(OS_CHROMEOS) +#include "chrome/browser/chromeos/login/user_manager.h" +#include "chrome/browser/chromeos/settings/cros_settings.h" +#include "chrome/browser/chromeos/settings/device_settings_service.h" +#endif + +namespace { + +base::FilePath MakePath(std::string dir) { +#if defined(OS_WIN) + return base::FilePath(FILE_PATH_LITERAL("C:\\")).AppendASCII(dir); +#elif defined(OS_POSIX) + return base::FilePath(FILE_PATH_LITERAL("/")).Append(dir); +#else + NOTIMPLEMENTED(); +#endif +} + +MediaGalleryPrefId AddScanResult(MediaGalleriesPreferences* gallery_prefs, + const std::string& path, int image_count, + int music_count, int video_count) { + MediaGalleryPrefInfo gallery_info; + gallery_prefs->LookUpGalleryByPath(MakePath(path), &gallery_info); + return gallery_prefs->AddGallery( + gallery_info.device_id, + gallery_info.path, + MediaGalleryPrefInfo::kScanResult, + gallery_info.volume_label, + gallery_info.vendor_name, + gallery_info.model_name, + gallery_info.total_size_in_bytes, + gallery_info.last_attach_time, + image_count, music_count, video_count); +} + +class MockMediaGalleriesScanResultDialog + : public MediaGalleriesScanResultDialog { + public: + typedef base::Callback<void(int update_count)> DialogDestroyedCallback; + + explicit MockMediaGalleriesScanResultDialog( + const DialogDestroyedCallback& callback) + : update_count_(0), + dialog_destroyed_callback_(callback) { + } + + virtual ~MockMediaGalleriesScanResultDialog() { + dialog_destroyed_callback_.Run(update_count_); + } + + // MockMediaGalleriesScanResultDialog implementation. + virtual void UpdateResults() OVERRIDE { + update_count_++; + } + + // Number up times UpdateResults has been called. + int update_count() { + return update_count_; + } + + private: + int update_count_; + + DialogDestroyedCallback dialog_destroyed_callback_; + + DISALLOW_COPY_AND_ASSIGN(MockMediaGalleriesScanResultDialog); +}; + +} // namespace + +class MediaGalleriesScanResultDialogControllerTest : public testing::Test { + public: + MediaGalleriesScanResultDialogControllerTest() + : dialog_(NULL), + dialog_update_count_at_destruction_(0), + controller_(NULL), + profile_(new TestingProfile()), + weak_factory_(this) { + } + + virtual ~MediaGalleriesScanResultDialogControllerTest() { + EXPECT_FALSE(controller_); + EXPECT_FALSE(dialog_); + } + + virtual void SetUp() OVERRIDE { + ASSERT_TRUE(TestStorageMonitor::CreateAndInstall()); + + extensions::TestExtensionSystem* extension_system( + static_cast<extensions::TestExtensionSystem*>( + extensions::ExtensionSystem::Get(profile_.get()))); + extension_system->CreateExtensionService( + CommandLine::ForCurrentProcess(), base::FilePath(), false); + + gallery_prefs_.reset(new MediaGalleriesPreferences(profile_.get())); + base::RunLoop loop; + gallery_prefs_->EnsureInitialized(loop.QuitClosure()); + loop.Run(); + + std::vector<std::string> read_permissions; + read_permissions.push_back( + extensions::MediaGalleriesPermission::kReadPermission); + extension_ = AddMediaGalleriesApp("read", read_permissions, profile_.get()); + } + + virtual void TearDown() OVERRIDE { + TestStorageMonitor::RemoveSingleton(); + } + + void StartDialog() { + ASSERT_FALSE(controller_); + controller_ = new MediaGalleriesScanResultDialogController( + *extension_.get(), + gallery_prefs_.get(), + base::Bind( + &MediaGalleriesScanResultDialogControllerTest::CreateMockDialog, + base::Unretained(this)), + base::Bind( + &MediaGalleriesScanResultDialogControllerTest::OnControllerDone, + base::Unretained(this))); + } + + MediaGalleriesScanResultDialogController* controller() { + return controller_; + } + + MockMediaGalleriesScanResultDialog* dialog() { + return dialog_; + } + + int dialog_update_count_at_destruction() { + EXPECT_FALSE(dialog_); + return dialog_update_count_at_destruction_; + } + + extensions::Extension* extension() { + return extension_.get(); + } + + MediaGalleriesPreferences* gallery_prefs() { + return gallery_prefs_.get(); + } + + private: + MediaGalleriesScanResultDialog* CreateMockDialog( + MediaGalleriesScanResultDialogController* controller) { + EXPECT_FALSE(dialog_); + dialog_update_count_at_destruction_ = 0; + dialog_ = new MockMediaGalleriesScanResultDialog(base::Bind( + &MediaGalleriesScanResultDialogControllerTest::OnDialogDestroyed, + weak_factory_.GetWeakPtr())); + return dialog_; + } + + void OnDialogDestroyed(int update_count) { + EXPECT_TRUE(dialog_); + dialog_update_count_at_destruction_ = update_count; + dialog_ = NULL; + } + + void OnControllerDone() { + controller_ = NULL; + } + + // Needed for extension service & friends to work. + content::TestBrowserThreadBundle thread_bundle_; + + // The dialog is owned by the controller, but this pointer should only be + // valid while the dialog is live within the controller. + MockMediaGalleriesScanResultDialog* dialog_; + int dialog_update_count_at_destruction_; + + // The controller owns itself. + MediaGalleriesScanResultDialogController* controller_; + + scoped_refptr<extensions::Extension> extension_; + + EnsureMediaDirectoriesExists mock_gallery_locations_; + +#if defined OS_CHROMEOS + chromeos::ScopedTestDeviceSettingsService test_device_settings_service_; + chromeos::ScopedTestCrosSettings test_cros_settings_; + chromeos::ScopedTestUserManager test_user_manager_; +#endif + + TestStorageMonitor monitor_; + scoped_ptr<TestingProfile> profile_; + scoped_ptr<MediaGalleriesPreferences> gallery_prefs_; + + base::WeakPtrFactory<MediaGalleriesScanResultDialogControllerTest> + weak_factory_; + + DISALLOW_COPY_AND_ASSIGN(MediaGalleriesScanResultDialogControllerTest); +}; + +TEST_F(MediaGalleriesScanResultDialogControllerTest, EmptyDialog) { + StartDialog(); + EXPECT_TRUE(controller()); + EXPECT_TRUE(dialog()); + EXPECT_EQ(0U, controller()->GetGalleryList().size()); + + controller()->DialogFinished(true); + EXPECT_FALSE(controller()); + EXPECT_FALSE(dialog()); + EXPECT_EQ(0, dialog_update_count_at_destruction()); +} + +TEST_F(MediaGalleriesScanResultDialogControllerTest, AddScanResults) { + // Start with two scan results. + MediaGalleryPrefId scan1 = + gallery_prefs()->AddGalleryByPath(MakePath("scan1"), + MediaGalleryPrefInfo::kScanResult); + MediaGalleryPrefId scan2 = + gallery_prefs()->AddGalleryByPath(MakePath("scan2"), + MediaGalleryPrefInfo::kScanResult); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + + // Show the dialog, but cancel it. + StartDialog(); + EXPECT_EQ(2U, controller()->GetGalleryList().size()); + controller()->DialogFinished(false); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + + // Show the dialog, unselect both and accept it. + StartDialog(); + EXPECT_EQ(2U, controller()->GetGalleryList().size()); + controller()->DidToggleGalleryId(scan1, false); + controller()->DidToggleGalleryId(scan2, false); + controller()->DialogFinished(true); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + + // Show the dialog, leave one selected and accept it. + StartDialog(); + EXPECT_EQ(2U, controller()->GetGalleryList().size()); + controller()->DidToggleGalleryId(scan1, false); + controller()->DialogFinished(true); + MediaGalleryPrefIdSet permitted = + gallery_prefs()->GalleriesForExtension(*extension()); + ASSERT_EQ(1U, permitted.size()); + EXPECT_EQ(scan2, *permitted.begin()); + + // Show the dialog, toggle the remaining entry twice and then accept it. + StartDialog(); + EXPECT_EQ(1U, controller()->GetGalleryList().size()); + controller()->DidToggleGalleryId(scan1, false); + controller()->DidToggleGalleryId(scan1, true); + controller()->DialogFinished(true); + EXPECT_EQ(2U, gallery_prefs()->GalleriesForExtension(*extension()).size()); +} + +TEST_F(MediaGalleriesScanResultDialogControllerTest, Blacklisted) { + // Start with two scan results. + MediaGalleryPrefId scan1 = + gallery_prefs()->AddGalleryByPath(MakePath("scan1"), + MediaGalleryPrefInfo::kScanResult); + MediaGalleryPrefId scan2 = + gallery_prefs()->AddGalleryByPath(MakePath("scan2"), + MediaGalleryPrefInfo::kScanResult); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + + // Show the dialog, but cancel it. + StartDialog(); + EXPECT_EQ(2U, controller()->GetGalleryList().size()); + controller()->DialogFinished(false); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + + // Blacklist one and try again. + gallery_prefs()->ForgetGalleryById(scan2); + StartDialog(); + EXPECT_EQ(1U, controller()->GetGalleryList().size()); + controller()->DialogFinished(false); + + // Adding it as a user gallery should change its type. + gallery_prefs()->AddGalleryByPath(MakePath("scan2"), + MediaGalleryPrefInfo::kUserAdded); + StartDialog(); + EXPECT_EQ(1U, controller()->GetGalleryList().size()); + + // Blacklisting the other while the dialog is open should remove it. + gallery_prefs()->ForgetGalleryById(scan1); + EXPECT_EQ(0U, controller()->GetGalleryList().size()); + controller()->DialogFinished(false); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + EXPECT_EQ(1, dialog_update_count_at_destruction()); +} + +TEST_F(MediaGalleriesScanResultDialogControllerTest, PrefUpdates) { + MediaGalleryPrefId selected = + gallery_prefs()->AddGalleryByPath(MakePath("selected"), + MediaGalleryPrefInfo::kScanResult); + MediaGalleryPrefId unselected = + gallery_prefs()->AddGalleryByPath(MakePath("unselected"), + MediaGalleryPrefInfo::kScanResult); + MediaGalleryPrefId selected_add_permission = + gallery_prefs()->AddGalleryByPath(MakePath("selected_add_permission"), + MediaGalleryPrefInfo::kScanResult); + MediaGalleryPrefId unselected_add_permission = + gallery_prefs()->AddGalleryByPath(MakePath("unselected_add_permission"), + MediaGalleryPrefInfo::kScanResult); + MediaGalleryPrefId selected_removed = + gallery_prefs()->AddGalleryByPath(MakePath("selected_removed"), + MediaGalleryPrefInfo::kScanResult); + MediaGalleryPrefId unselected_removed = + gallery_prefs()->AddGalleryByPath(MakePath("unselected_removed"), + MediaGalleryPrefInfo::kScanResult); + MediaGalleryPrefId selected_update = + gallery_prefs()->AddGalleryByPath(MakePath("selected_update"), + MediaGalleryPrefInfo::kScanResult); + MediaGalleryPrefId unselected_update = + gallery_prefs()->AddGalleryByPath(MakePath("unselected_update"), + MediaGalleryPrefInfo::kScanResult); + + gallery_prefs()->AddGalleryByPath(MakePath("user"), + MediaGalleryPrefInfo::kUserAdded); + gallery_prefs()->AddGalleryByPath(MakePath("auto_detected"), + MediaGalleryPrefInfo::kAutoDetected); + MediaGalleryPrefId blacklisted = + gallery_prefs()->AddGalleryByPath(MakePath("blacklisted"), + MediaGalleryPrefInfo::kAutoDetected); + gallery_prefs()->ForgetGalleryById(blacklisted); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + + StartDialog(); + EXPECT_EQ(8U, controller()->GetGalleryList().size()); + controller()->DidToggleGalleryId(unselected, false); + controller()->DidToggleGalleryId(unselected_add_permission, false); + controller()->DidToggleGalleryId(unselected_removed, false); + controller()->DidToggleGalleryId(unselected_update, false); + EXPECT_EQ(0, dialog()->update_count()); + EXPECT_EQ(8U, controller()->GetGalleryList().size()); + + // Add permission. + gallery_prefs()->SetGalleryPermissionForExtension(*extension(), + unselected_add_permission, + true); + EXPECT_EQ(1, dialog()->update_count()); + EXPECT_EQ(7U, controller()->GetGalleryList().size()); + gallery_prefs()->SetGalleryPermissionForExtension(*extension(), + selected_add_permission, + true); + EXPECT_EQ(2, dialog()->update_count()); + EXPECT_EQ(6U, controller()->GetGalleryList().size()); + + // Blacklist scan results. + gallery_prefs()->ForgetGalleryById(unselected_removed); + EXPECT_EQ(3, dialog()->update_count()); + EXPECT_EQ(5U, controller()->GetGalleryList().size()); + gallery_prefs()->ForgetGalleryById(selected_removed); + EXPECT_EQ(4, dialog()->update_count()); + EXPECT_EQ(4U, controller()->GetGalleryList().size()); + + // Update names. + const MediaGalleryPrefInfo& unselected_update_info = + gallery_prefs()->known_galleries().find(unselected_update)->second; + gallery_prefs()->AddGallery( + unselected_update_info.device_id, base::FilePath(), + MediaGalleryPrefInfo::kScanResult, + base::ASCIIToUTF16("Updated & Unselected"), + base::string16(), base::string16(), 0, base::Time(), 0, 0, 0); + EXPECT_EQ(5, dialog()->update_count()); + EXPECT_EQ(4U, controller()->GetGalleryList().size()); + const MediaGalleryPrefInfo& selected_update_info = + gallery_prefs()->known_galleries().find(selected_update)->second; + gallery_prefs()->AddGallery( + selected_update_info.device_id, base::FilePath(), + MediaGalleryPrefInfo::kScanResult, + base::ASCIIToUTF16("Updated & Selected"), + base::string16(), base::string16(), 0, base::Time(), 0, 0, 0); + EXPECT_EQ(6, dialog()->update_count()); + ASSERT_EQ(4U, controller()->GetGalleryList().size()); + + MediaGalleriesScanResultDialogController::OrderedScanResults results = + controller()->GetGalleryList(); + EXPECT_EQ(selected, results[0].pref_info.pref_id); + EXPECT_TRUE(results[0].selected); + EXPECT_EQ(selected_update, results[1].pref_info.pref_id); + EXPECT_TRUE(results[1].selected); + EXPECT_EQ(base::ASCIIToUTF16("Updated & Selected"), + results[1].pref_info.volume_label); + EXPECT_EQ(unselected, results[2].pref_info.pref_id); + EXPECT_FALSE(results[2].selected); + EXPECT_EQ(unselected_update, results[3].pref_info.pref_id); + EXPECT_FALSE(results[3].selected); + EXPECT_EQ(base::ASCIIToUTF16("Updated & Unselected"), + results[3].pref_info.volume_label); + + controller()->DialogFinished(true); + EXPECT_EQ(4U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + StartDialog(); + EXPECT_EQ(2U, controller()->GetGalleryList().size()); + controller()->DialogFinished(false); +} + +TEST_F(MediaGalleriesScanResultDialogControllerTest, ForgetGallery) { + // Start with two scan results. + MediaGalleryPrefId scan1 = + gallery_prefs()->AddGalleryByPath(MakePath("scan1"), + MediaGalleryPrefInfo::kScanResult); + MediaGalleryPrefId scan2 = + gallery_prefs()->AddGalleryByPath(MakePath("scan2"), + MediaGalleryPrefInfo::kScanResult); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + + // Remove one and then cancel. + StartDialog(); + EXPECT_EQ(2U, controller()->GetGalleryList().size()); + controller()->DidForgetGallery(scan1); + controller()->DialogFinished(false); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + + // Remove one and then have it blacklisted from prefs. + StartDialog(); + EXPECT_EQ(2U, controller()->GetGalleryList().size()); + controller()->DidForgetGallery(scan1); + EXPECT_EQ(1, dialog()->update_count()); + controller()->DidToggleGalleryId(scan2, false); // Uncheck the second. + gallery_prefs()->ForgetGalleryById(scan1); + controller()->DialogFinished(true); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + EXPECT_EQ(2, dialog_update_count_at_destruction()); + + // Remove the other. + StartDialog(); + EXPECT_EQ(1U, controller()->GetGalleryList().size()); + controller()->DidForgetGallery(scan2); + controller()->DialogFinished(true); + EXPECT_EQ(0U, gallery_prefs()->GalleriesForExtension(*extension()).size()); + + // Check that nothing shows up. + StartDialog(); + EXPECT_EQ(0U, controller()->GetGalleryList().size()); + controller()->DialogFinished(false); +} + +TEST_F(MediaGalleriesScanResultDialogControllerTest, SortOrder) { + // Intentionally our of order numerically and alphabetically. + MediaGalleryPrefId third = AddScanResult(gallery_prefs(), "third", 2, 2, 2); + MediaGalleryPrefId second = AddScanResult(gallery_prefs(), "second", 9, 0, 0); + MediaGalleryPrefId first = AddScanResult(gallery_prefs(), "first", 8, 2, 3); + MediaGalleryPrefId fifth = AddScanResult(gallery_prefs(), "abb", 3, 0, 0); + MediaGalleryPrefId fourth = AddScanResult(gallery_prefs(), "aaa", 3, 0, 0); + + StartDialog(); + MediaGalleriesScanResultDialogController::OrderedScanResults results = + controller()->GetGalleryList(); + ASSERT_EQ(5U, results.size()); + EXPECT_EQ(first, results[0].pref_info.pref_id); + EXPECT_EQ(second, results[1].pref_info.pref_id); + EXPECT_EQ(third, results[2].pref_info.pref_id); + EXPECT_EQ(fourth, results[3].pref_info.pref_id); + EXPECT_EQ(fifth, results[4].pref_info.pref_id); + controller()->DialogFinished(false); +} diff --git a/chrome/browser/ui/views/extensions/media_galleries_scan_result_dialog_views.cc b/chrome/browser/ui/views/extensions/media_galleries_scan_result_dialog_views.cc new file mode 100644 index 0000000..6ec7293 --- /dev/null +++ b/chrome/browser/ui/views/extensions/media_galleries_scan_result_dialog_views.cc @@ -0,0 +1,307 @@ +// 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/ui/views/extensions/media_galleries_scan_result_dialog_views.h" + +#include "chrome/browser/ui/views/constrained_window_views.h" +#include "components/web_modal/web_contents_modal_dialog_host.h" +#include "components/web_modal/web_contents_modal_dialog_manager.h" +#include "components/web_modal/web_contents_modal_dialog_manager_delegate.h" +#include "content/public/browser/web_contents.h" +#include "grit/generated_resources.h" +#include "grit/locale_settings.h" +#include "grit/ui_resources.h" +#include "third_party/skia/include/core/SkColor.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/base/resource/resource_bundle.h" +#include "ui/views/border.h" +#include "ui/views/controls/button/checkbox.h" +#include "ui/views/controls/button/image_button.h" +#include "ui/views/controls/label.h" +#include "ui/views/controls/scroll_view.h" +#include "ui/views/layout/box_layout.h" +#include "ui/views/layout/grid_layout.h" +#include "ui/views/layout/layout_constants.h" +#include "ui/views/view.h" +#include "ui/views/widget/widget.h" +#include "ui/views/window/dialog_client_view.h" + +typedef MediaGalleriesScanResultDialogController::OrderedScanResults + OrderedScanResults; + +namespace { + +// TODO(vandebo) move this to a common place. +// Equal to the #969696 color used in spec (note WebUI color is #999). +const SkColor kDeemphasizedTextColor = SkColorSetRGB(159, 159, 159); + +const int kScrollAreaHeight = 192; + +// This container has the right Layout() impl to use within a ScrollView. +class ScrollableView : public views::View { + public: + ScrollableView() {} + virtual ~ScrollableView() {} + + virtual void Layout() OVERRIDE; + + private: + DISALLOW_COPY_AND_ASSIGN(ScrollableView); +}; + +void ScrollableView::Layout() { + gfx::Size pref = GetPreferredSize(); + int width = pref.width(); + int height = pref.height(); + if (parent()) { + width = std::max(parent()->width(), width); + height = std::max(parent()->height(), height); + } + SetBounds(x(), y(), width, height); + + views::View::Layout(); +} + +} // namespace + +MediaGalleriesScanResultDialogViews::MediaGalleriesScanResultDialogViews( + MediaGalleriesScanResultDialogController* controller) + : controller_(controller), + window_(NULL), + contents_(new views::View()), + accepted_(false) { + InitChildViews(); + + // Ownership of |contents_| is handed off by this call. |window_| will take + // care of deleting itself after calling DeleteDelegate(). + web_modal::WebContentsModalDialogManager* web_contents_modal_dialog_manager = + web_modal::WebContentsModalDialogManager::FromWebContents( + controller->web_contents()); + DCHECK(web_contents_modal_dialog_manager); + web_modal::WebContentsModalDialogManagerDelegate* modal_delegate = + web_contents_modal_dialog_manager->delegate(); + DCHECK(modal_delegate); + window_ = views::Widget::CreateWindowAsFramelessChild( + this, modal_delegate->GetWebContentsModalDialogHost()->GetHostView()); + web_contents_modal_dialog_manager->ShowDialog(window_->GetNativeView()); +} + +MediaGalleriesScanResultDialogViews::~MediaGalleriesScanResultDialogViews() {} + +void MediaGalleriesScanResultDialogViews::InitChildViews() { + // Outer dialog layout. + contents_->RemoveAllChildViews(true); + int dialog_content_width = views::Widget::GetLocalizedContentsWidth( + IDS_MEDIA_GALLERIES_DIALOG_CONTENT_WIDTH_CHARS); + views::GridLayout* layout = views::GridLayout::CreatePanel(contents_); + contents_->SetLayoutManager(layout); + + int column_set_id = 0; + views::ColumnSet* columns = layout->AddColumnSet(column_set_id); + columns->AddColumn(views::GridLayout::LEADING, + views::GridLayout::LEADING, + 1, + views::GridLayout::FIXED, + dialog_content_width, + 0); + + // Message text. + views::Label* subtext = new views::Label(controller_->GetSubtext()); + subtext->SetMultiLine(true); + subtext->SetHorizontalAlignment(gfx::ALIGN_LEFT); + layout->StartRow(0, column_set_id); + layout->AddView( + subtext, 1, 1, + views::GridLayout::FILL, views::GridLayout::LEADING, + dialog_content_width, subtext->GetHeightForWidth(dialog_content_width)); + layout->AddPaddingRow(0, views::kRelatedControlVerticalSpacing); + + // Scrollable area for checkboxes. + ScrollableView* scroll_container = new ScrollableView(); + scroll_container->SetLayoutManager(new views::BoxLayout( + views::BoxLayout::kVertical, 0, 0, + views::kRelatedControlSmallVerticalSpacing)); + scroll_container->SetBorder(views::Border::CreateEmptyBorder( + views::kRelatedControlVerticalSpacing, + 0, + views::kRelatedControlVerticalSpacing, + 0)); + + // Add attached galleries checkboxes. + gallery_view_map_.clear(); + OrderedScanResults scan_results = controller_->GetGalleryList(); + for (OrderedScanResults::const_iterator it = scan_results.begin(); + it != scan_results.end(); + ++it) { + int spacing = 0; + if (it + 1 == scan_results.end()) + spacing = views::kRelatedControlSmallVerticalSpacing; + AddOrUpdateScanResult(it->pref_info, it->selected, scroll_container, + spacing); + } + + // Add the scrollable area to the outer dialog view. It will squeeze against + // the title/subtitle and buttons to occupy all available space in the dialog. + views::ScrollView* scroll_view = + views::ScrollView::CreateScrollViewWithBorder(); + scroll_view->SetContents(scroll_container); + layout->StartRowWithPadding(1, column_set_id, + 0, views::kRelatedControlVerticalSpacing); + layout->AddView(scroll_view, 1, 1, + views::GridLayout::FILL, views::GridLayout::FILL, + dialog_content_width, kScrollAreaHeight); +} + +void MediaGalleriesScanResultDialogViews::UpdateResults() { + InitChildViews(); + contents_->Layout(); +} + +bool MediaGalleriesScanResultDialogViews::AddOrUpdateScanResult( + const MediaGalleryPrefInfo& gallery, + bool selected, + views::View* container, + int trailing_vertical_space) { + base::string16 label = gallery.GetGalleryDisplayName(); + base::string16 tooltip_text = gallery.GetGalleryTooltip(); + base::string16 details = gallery.GetGalleryAdditionalDetails(); + bool is_attached = gallery.IsGalleryAvailable(); + + GalleryViewMap::iterator it = gallery_view_map_.find(gallery.pref_id); + if (it != gallery_view_map_.end()) { + views::Checkbox* checkbox = it->second.checkbox; + checkbox->SetChecked(selected); + checkbox->SetText(label); + checkbox->SetElideBehavior(views::Label::ELIDE_IN_MIDDLE); + checkbox->SetTooltipText(tooltip_text); + // Replace the details string. + it->second.folder_viewer_button->SetVisible(is_attached); + it->second.secondary_text->SetText(details); + return false; + } + + views::Checkbox* checkbox = new views::Checkbox(label); + checkbox->set_listener(this); + checkbox->SetTooltipText(tooltip_text); + + ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); + views::ImageButton* folder_viewer_button = new views::ImageButton(this); + folder_viewer_button->SetImage(views::ImageButton::STATE_NORMAL, + rb.GetImageSkiaNamed(IDR_OAK)); + folder_viewer_button->SetAccessibleName(l10n_util::GetStringUTF16( + IDS_MEDIA_GALLERIES_SCAN_RESULT_OPEN_FOLDER_VIEW_ACCESSIBILITY_NAME)); + folder_viewer_button->SetFocusable(true); + folder_viewer_button->SetVisible(is_attached); + + views::Label* secondary_text = new views::Label(details); + secondary_text->SetTooltipText(tooltip_text); + secondary_text->SetEnabledColor(kDeemphasizedTextColor); + secondary_text->SetTooltipText(tooltip_text); + secondary_text->SetBorder(views::Border::CreateEmptyBorder( + 0, + views::kRelatedControlSmallHorizontalSpacing, + 0, + views::kRelatedControlSmallHorizontalSpacing)); + + views::View* checkbox_view = new views::View(); + checkbox_view->SetBorder(views::Border::CreateEmptyBorder( + 0, views::kPanelHorizMargin, trailing_vertical_space, 0)); + checkbox_view->SetLayoutManager( + new views::BoxLayout(views::BoxLayout::kHorizontal, 0, 0, 0)); + checkbox_view->AddChildView(checkbox); + checkbox_view->AddChildView(folder_viewer_button); + checkbox_view->AddChildView(secondary_text); + + container->AddChildView(checkbox_view); + + checkbox->SetChecked(selected); + gallery_view_map_[gallery.pref_id].checkbox = checkbox; + gallery_view_map_[gallery.pref_id].folder_viewer_button = + folder_viewer_button; + gallery_view_map_[gallery.pref_id].secondary_text = secondary_text; + + return true; +} + +base::string16 MediaGalleriesScanResultDialogViews::GetWindowTitle() const { + return controller_->GetHeader(); +} + +void MediaGalleriesScanResultDialogViews::DeleteDelegate() { + controller_->DialogFinished(accepted_); +} + +views::Widget* MediaGalleriesScanResultDialogViews::GetWidget() { + return contents_->GetWidget(); +} + +const views::Widget* MediaGalleriesScanResultDialogViews::GetWidget() const { + return contents_->GetWidget(); +} + +views::View* MediaGalleriesScanResultDialogViews::GetContentsView() { + return contents_; +} + +base::string16 MediaGalleriesScanResultDialogViews::GetDialogButtonLabel( + ui::DialogButton button) const { + return l10n_util::GetStringUTF16(button == ui::DIALOG_BUTTON_OK ? + IDS_MEDIA_GALLERIES_DIALOG_CONFIRM : + IDS_MEDIA_GALLERIES_DIALOG_CANCEL); +} + +ui::ModalType MediaGalleriesScanResultDialogViews::GetModalType() const { +#if defined(USE_ASH) + return ui::MODAL_TYPE_CHILD; +#else + return views::WidgetDelegate::GetModalType(); +#endif +} + +bool MediaGalleriesScanResultDialogViews::Cancel() { + return true; +} + +bool MediaGalleriesScanResultDialogViews::Accept() { + accepted_ = true; + + return true; +} + +// TODO(wittman): Remove this override once we move to the new style frame view +// on all dialogs. +views::NonClientFrameView* +MediaGalleriesScanResultDialogViews::CreateNonClientFrameView( + views::Widget* widget) { + return CreateConstrainedStyleNonClientFrameView( + widget, + controller_->web_contents()->GetBrowserContext()); +} + +void MediaGalleriesScanResultDialogViews::ButtonPressed( + views::Button* sender, + const ui::Event& event) { + GetWidget()->client_view()->AsDialogClientView()->UpdateDialogButtons(); + + for (GalleryViewMap::const_iterator it = gallery_view_map_.begin(); + it != gallery_view_map_.end(); ++it) { + if (sender == it->second.checkbox) { + controller_->DidToggleGalleryId(it->first, + it->second.checkbox->checked()); + return; + } + if (sender == it->second.folder_viewer_button) { + controller_->DidClickOpenFolderViewer(it->first); + return; + } + } +} + +// MediaGalleriesScanResultDialogViewsController ------------------------------- + +// static +MediaGalleriesScanResultDialog* MediaGalleriesScanResultDialog::Create( + MediaGalleriesScanResultDialogController* controller) { + return new MediaGalleriesScanResultDialogViews(controller); +} diff --git a/chrome/browser/ui/views/extensions/media_galleries_scan_result_dialog_views.h b/chrome/browser/ui/views/extensions/media_galleries_scan_result_dialog_views.h new file mode 100644 index 0000000..7baf467 --- /dev/null +++ b/chrome/browser/ui/views/extensions/media_galleries_scan_result_dialog_views.h @@ -0,0 +1,88 @@ +// 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. + +#ifndef CHROME_BROWSER_UI_VIEWS_EXTENSIONS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_VIEWS_H_ +#define CHROME_BROWSER_UI_VIEWS_EXTENSIONS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_VIEWS_H_ + +#include <map> + +#include "base/compiler_specific.h" +#include "chrome/browser/media_galleries/media_galleries_scan_result_dialog_controller.h" +#include "ui/views/controls/button/button.h" +#include "ui/views/window/dialog_delegate.h" + +namespace views { +class Checkbox; +class ImageButton; +class Label; +class Widget; +} + +// The media galleries scan result view for Views. It will immediately show +// upon construction. +class MediaGalleriesScanResultDialogViews + : public MediaGalleriesScanResultDialog, + public views::ButtonListener, + public views::DialogDelegate { + public: + explicit MediaGalleriesScanResultDialogViews( + MediaGalleriesScanResultDialogController* controller); + virtual ~MediaGalleriesScanResultDialogViews(); + + // MediaGalleriesScanResultDialog implementation: + virtual void UpdateResults() OVERRIDE; + + // views::DialogDelegate implementation: + virtual base::string16 GetWindowTitle() const OVERRIDE; + virtual void DeleteDelegate() OVERRIDE; + virtual views::Widget* GetWidget() OVERRIDE; + virtual const views::Widget* GetWidget() const OVERRIDE; + virtual views::View* GetContentsView() OVERRIDE; + virtual base::string16 GetDialogButtonLabel( + ui::DialogButton button) const OVERRIDE; + virtual ui::ModalType GetModalType() const OVERRIDE; + virtual bool Cancel() OVERRIDE; + virtual bool Accept() OVERRIDE; + virtual views::NonClientFrameView* CreateNonClientFrameView( + views::Widget* widget) OVERRIDE; + + // views::ButtonListener implementation: + virtual void ButtonPressed(views::Button* sender, + const ui::Event& event) OVERRIDE; + + private: + struct GalleryEntry { + views::Checkbox* checkbox; + views::ImageButton* folder_viewer_button; + views::Label* secondary_text; + }; + typedef std::map<MediaGalleryPrefId, GalleryEntry> GalleryViewMap; + + void InitChildViews(); + + // Adds a checkbox or updates an existing checkbox. Returns true if a new one + // was added. + bool AddOrUpdateScanResult(const MediaGalleryPrefInfo& gallery, + bool selected, + views::View* container, + int trailing_vertical_space); + + MediaGalleriesScanResultDialogController* controller_; + + // The containing window (a weak pointer). + views::Widget* window_; + + // The contents of the dialog. Owned by |window_|'s RootView. + views::View* contents_; + + // A map from media gallery ID to the view elements for each gallery. + GalleryViewMap gallery_view_map_; + + // True if the user has pressed accept. + bool accepted_; + + DISALLOW_COPY_AND_ASSIGN(MediaGalleriesScanResultDialogViews); +}; + +#endif // CHROME_BROWSER_UI_VIEWS_EXTENSIONS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_VIEWS_H_ diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index 0fffe8d..f5ea815 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -1165,6 +1165,8 @@ 'browser/media_galleries/media_galleries_preferences.h', 'browser/media_galleries/media_galleries_preferences_factory.cc', 'browser/media_galleries/media_galleries_preferences_factory.h', + 'browser/media_galleries/media_galleries_scan_result_dialog_controller.cc', + 'browser/media_galleries/media_galleries_scan_result_dialog_controller.h', 'browser/media_galleries/media_scan_manager.cc', 'browser/media_galleries/media_scan_manager.h', 'browser/media_galleries/media_scan_manager_observer.h', diff --git a/chrome/chrome_browser_ui.gypi b/chrome/chrome_browser_ui.gypi index a4dbb98..1d2ab06 100644 --- a/chrome/chrome_browser_ui.gypi +++ b/chrome/chrome_browser_ui.gypi @@ -1835,6 +1835,8 @@ 'browser/ui/views/extensions/extension_view_views.h', 'browser/ui/views/extensions/media_galleries_dialog_views.cc', 'browser/ui/views/extensions/media_galleries_dialog_views.h', + 'browser/ui/views/extensions/media_galleries_scan_result_dialog_views.cc', + 'browser/ui/views/extensions/media_galleries_scan_result_dialog_views.h', 'browser/ui/views/external_protocol_dialog.cc', 'browser/ui/views/external_protocol_dialog.h', 'browser/ui/views/find_bar_host.cc', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index 30b3fe1..0d422bd 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -1032,9 +1032,10 @@ 'browser/media_galleries/media_file_system_registry_unittest.cc', 'browser/media_galleries/media_galleries_dialog_controller_mock.cc', 'browser/media_galleries/media_galleries_dialog_controller_mock.h', - 'browser/media_galleries/media_galleries_permissions_unittest.cc', 'browser/media_galleries/media_galleries_dialog_controller_unittest.cc', + 'browser/media_galleries/media_galleries_permissions_unittest.cc', 'browser/media_galleries/media_galleries_preferences_unittest.cc', + 'browser/media_galleries/media_galleries_scan_result_dialog_controller_unittest.cc', 'browser/media_galleries/win/mtp_device_object_enumerator_unittest.cc', 'browser/metrics/compression_utils_unittest.cc', 'browser/metrics/metrics_log_unittest.cc', diff --git a/chrome/renderer/resources/extensions/media_galleries_custom_bindings.js b/chrome/renderer/resources/extensions/media_galleries_custom_bindings.js index 68a17b0..2d9116d 100644 --- a/chrome/renderer/resources/extensions/media_galleries_custom_bindings.js +++ b/chrome/renderer/resources/extensions/media_galleries_custom_bindings.js @@ -31,8 +31,9 @@ function createFileSystemObjectsAndUpdateMetadata(response) { binding.registerCustomHook(function(bindingsAPI, extensionId) { var apiFunctions = bindingsAPI.apiFunctions; - // getMediaFileSystems uses a custom callback so that it can instantiate and - // return an array of file system objects. + // getMediaFileSystems, addUserSelectedFolder, and addScanResults use a + // custom callback so that they can instantiate and return an array of file + // system objects. apiFunctions.setCustomCallback('getMediaFileSystems', function(name, request, response) { var result = createFileSystemObjectsAndUpdateMetadata(response); @@ -41,8 +42,14 @@ binding.registerCustomHook(function(bindingsAPI, extensionId) { request.callback = null; }); - // addUserSelectedFolder uses a custom callback so that it can instantiate - // and return an array of file system objects. + apiFunctions.setCustomCallback('addScanResults', + function(name, request, response) { + var result = createFileSystemObjectsAndUpdateMetadata(response); + if (request.callback) + request.callback(result); + request.callback = null; + }); + apiFunctions.setCustomCallback('addUserSelectedFolder', function(name, request, response) { var fileSystems = []; |