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_galleries_permission_controller.h" 6 7#include "base/base_paths.h" 8#include "base/path_service.h" 9#include "base/stl_util.h" 10#include "base/strings/utf_string_conversions.h" 11#include "chrome/browser/browser_process.h" 12#include "chrome/browser/extensions/api/file_system/file_system_api.h" 13#include "chrome/browser/media_galleries/media_file_system_registry.h" 14#include "chrome/browser/media_galleries/media_galleries_histograms.h" 15#include "chrome/browser/media_galleries/media_gallery_context_menu.h" 16#include "chrome/browser/profiles/profile.h" 17#include "chrome/browser/ui/chrome_select_file_policy.h" 18#include "components/storage_monitor/storage_info.h" 19#include "components/storage_monitor/storage_monitor.h" 20#include "content/public/browser/web_contents.h" 21#include "extensions/browser/extension_prefs.h" 22#include "extensions/common/extension.h" 23#include "extensions/common/permissions/media_galleries_permission.h" 24#include "extensions/common/permissions/permissions_data.h" 25#include "grit/generated_resources.h" 26#include "ui/base/l10n/l10n_util.h" 27#include "ui/base/models/simple_menu_model.h" 28#include "ui/base/text/bytes_formatting.h" 29 30using extensions::APIPermission; 31using extensions::Extension; 32using storage_monitor::StorageInfo; 33using storage_monitor::StorageMonitor; 34 35namespace { 36 37// Comparator for sorting gallery entries. Sort Removable entries above 38// non-removable ones. Within those two groups, sort on media counts 39// if populated, otherwise on paths. 40bool GalleriesVectorComparator( 41 const MediaGalleriesDialogController::Entry& a, 42 const MediaGalleriesDialogController::Entry& b) { 43 if (StorageInfo::IsRemovableDevice(a.pref_info.device_id) != 44 StorageInfo::IsRemovableDevice(b.pref_info.device_id)) { 45 return StorageInfo::IsRemovableDevice(a.pref_info.device_id); 46 } 47 int a_media_count = a.pref_info.audio_count + a.pref_info.image_count + 48 a.pref_info.video_count; 49 int b_media_count = b.pref_info.audio_count + b.pref_info.image_count + 50 b.pref_info.video_count; 51 if (a_media_count != b_media_count) 52 return a_media_count > b_media_count; 53 return a.pref_info.AbsolutePath() < b.pref_info.AbsolutePath(); 54} 55 56} // namespace 57 58MediaGalleriesPermissionController::MediaGalleriesPermissionController( 59 content::WebContents* web_contents, 60 const Extension& extension, 61 const base::Closure& on_finish) 62 : web_contents_(web_contents), 63 extension_(&extension), 64 on_finish_(on_finish), 65 preferences_( 66 g_browser_process->media_file_system_registry()->GetPreferences( 67 GetProfile())), 68 create_dialog_callback_(base::Bind(&MediaGalleriesDialog::Create)) { 69 // Passing unretained pointer is safe, since the dialog controller 70 // is self-deleting, and so won't be deleted until it can be shown 71 // and then closed. 72 preferences_->EnsureInitialized( 73 base::Bind(&MediaGalleriesPermissionController::OnPreferencesInitialized, 74 base::Unretained(this))); 75 76 // Unretained is safe because |this| owns |context_menu_|. 77 context_menu_.reset( 78 new MediaGalleryContextMenu( 79 base::Bind(&MediaGalleriesPermissionController::DidForgetEntry, 80 base::Unretained(this)))); 81} 82 83void MediaGalleriesPermissionController::OnPreferencesInitialized() { 84 if (StorageMonitor::GetInstance()) 85 StorageMonitor::GetInstance()->AddObserver(this); 86 87 // |preferences_| may be NULL in tests. 88 if (preferences_) { 89 preferences_->AddGalleryChangeObserver(this); 90 InitializePermissions(); 91 } 92 93 dialog_.reset(create_dialog_callback_.Run(this)); 94} 95 96MediaGalleriesPermissionController::MediaGalleriesPermissionController( 97 const extensions::Extension& extension, 98 MediaGalleriesPreferences* preferences, 99 const CreateDialogCallback& create_dialog_callback, 100 const base::Closure& on_finish) 101 : web_contents_(NULL), 102 extension_(&extension), 103 on_finish_(on_finish), 104 preferences_(preferences), 105 create_dialog_callback_(create_dialog_callback) { 106 OnPreferencesInitialized(); 107} 108 109MediaGalleriesPermissionController::~MediaGalleriesPermissionController() { 110 if (StorageMonitor::GetInstance()) 111 StorageMonitor::GetInstance()->RemoveObserver(this); 112 113 // |preferences_| may be NULL in tests. 114 if (preferences_) 115 preferences_->RemoveGalleryChangeObserver(this); 116 117 if (select_folder_dialog_.get()) 118 select_folder_dialog_->ListenerDestroyed(); 119} 120 121base::string16 MediaGalleriesPermissionController::GetHeader() const { 122 return l10n_util::GetStringFUTF16(IDS_MEDIA_GALLERIES_DIALOG_HEADER, 123 base::UTF8ToUTF16(extension_->name())); 124} 125 126base::string16 MediaGalleriesPermissionController::GetSubtext() const { 127 extensions::MediaGalleriesPermission::CheckParam copy_to_param( 128 extensions::MediaGalleriesPermission::kCopyToPermission); 129 extensions::MediaGalleriesPermission::CheckParam delete_param( 130 extensions::MediaGalleriesPermission::kDeletePermission); 131 const extensions::PermissionsData* permission_data = 132 extension_->permissions_data(); 133 bool has_copy_to_permission = permission_data->CheckAPIPermissionWithParam( 134 APIPermission::kMediaGalleries, ©_to_param); 135 bool has_delete_permission = permission_data->CheckAPIPermissionWithParam( 136 APIPermission::kMediaGalleries, &delete_param); 137 138 int id; 139 if (has_copy_to_permission) 140 id = IDS_MEDIA_GALLERIES_DIALOG_SUBTEXT_READ_WRITE; 141 else if (has_delete_permission) 142 id = IDS_MEDIA_GALLERIES_DIALOG_SUBTEXT_READ_DELETE; 143 else 144 id = IDS_MEDIA_GALLERIES_DIALOG_SUBTEXT_READ_ONLY; 145 146 return l10n_util::GetStringFUTF16(id, base::UTF8ToUTF16(extension_->name())); 147} 148 149bool MediaGalleriesPermissionController::IsAcceptAllowed() const { 150 if (!toggled_galleries_.empty() || !forgotten_galleries_.empty()) 151 return true; 152 153 for (GalleryPermissionsMap::const_iterator iter = new_galleries_.begin(); 154 iter != new_galleries_.end(); 155 ++iter) { 156 if (iter->second.selected) 157 return true; 158 } 159 160 return false; 161} 162 163bool MediaGalleriesPermissionController::ShouldShowFolderViewer( 164 const Entry& entry) const { 165 return false; 166} 167 168std::vector<base::string16> 169MediaGalleriesPermissionController::GetSectionHeaders() const { 170 std::vector<base::string16> result; 171 result.push_back(base::string16()); // First section has no header. 172 result.push_back( 173 l10n_util::GetStringUTF16(IDS_MEDIA_GALLERIES_PERMISSION_SUGGESTIONS)); 174 return result; 175} 176 177// Note: sorts by display criterion: GalleriesVectorComparator. 178MediaGalleriesDialogController::Entries 179MediaGalleriesPermissionController::GetSectionEntries(size_t index) const { 180 DCHECK_GT(2U, index); // This dialog only has two sections. 181 182 bool existing = !index; 183 MediaGalleriesDialogController::Entries result; 184 for (GalleryPermissionsMap::const_iterator iter = known_galleries_.begin(); 185 iter != known_galleries_.end(); ++iter) { 186 MediaGalleryPrefId pref_id = GetPrefId(iter->first); 187 if (!ContainsKey(forgotten_galleries_, iter->first) && 188 existing == ContainsKey(pref_permitted_galleries_, pref_id)) { 189 result.push_back(iter->second); 190 } 191 } 192 if (existing) { 193 for (GalleryPermissionsMap::const_iterator iter = new_galleries_.begin(); 194 iter != new_galleries_.end(); ++iter) { 195 result.push_back(iter->second); 196 } 197 } 198 199 std::sort(result.begin(), result.end(), GalleriesVectorComparator); 200 return result; 201} 202 203base::string16 204MediaGalleriesPermissionController::GetAuxiliaryButtonText() const { 205 return l10n_util::GetStringUTF16(IDS_MEDIA_GALLERIES_DIALOG_ADD_GALLERY); 206} 207 208// This is the 'Add Folder' button. 209void MediaGalleriesPermissionController::DidClickAuxiliaryButton() { 210 base::FilePath default_path = 211 extensions::file_system_api::GetLastChooseEntryDirectory( 212 extensions::ExtensionPrefs::Get(GetProfile()), extension_->id()); 213 if (default_path.empty()) 214 PathService::Get(base::DIR_USER_DESKTOP, &default_path); 215 select_folder_dialog_ = 216 ui::SelectFileDialog::Create(this, new ChromeSelectFilePolicy(NULL)); 217 select_folder_dialog_->SelectFile( 218 ui::SelectFileDialog::SELECT_FOLDER, 219 l10n_util::GetStringUTF16(IDS_MEDIA_GALLERIES_DIALOG_ADD_GALLERY_TITLE), 220 default_path, 221 NULL, 222 0, 223 base::FilePath::StringType(), 224 web_contents_->GetTopLevelNativeWindow(), 225 NULL); 226} 227 228void MediaGalleriesPermissionController::DidToggleEntry( 229 GalleryDialogId gallery_id, bool selected) { 230 // Check known galleries. 231 GalleryPermissionsMap::iterator iter = known_galleries_.find(gallery_id); 232 if (iter != known_galleries_.end()) { 233 if (iter->second.selected == selected) 234 return; 235 236 iter->second.selected = selected; 237 toggled_galleries_[gallery_id] = selected; 238 return; 239 } 240 241 iter = new_galleries_.find(gallery_id); 242 if (iter != new_galleries_.end()) 243 iter->second.selected = selected; 244 245 // Don't sort -- the dialog is open, and we don't want to adjust any 246 // positions for future updates to the dialog contents until they are 247 // redrawn. 248} 249 250void MediaGalleriesPermissionController::DidClickOpenFolderViewer( 251 GalleryDialogId gallery_id) { 252 NOTREACHED(); 253} 254 255void MediaGalleriesPermissionController::DidForgetEntry( 256 GalleryDialogId gallery_id) { 257 media_galleries::UsageCount(media_galleries::DIALOG_FORGET_GALLERY); 258 if (!new_galleries_.erase(gallery_id)) { 259 DCHECK(ContainsKey(known_galleries_, gallery_id)); 260 forgotten_galleries_.insert(gallery_id); 261 } 262 dialog_->UpdateGalleries(); 263} 264 265base::string16 MediaGalleriesPermissionController::GetAcceptButtonText() const { 266 return l10n_util::GetStringUTF16(IDS_MEDIA_GALLERIES_DIALOG_CONFIRM); 267} 268 269void MediaGalleriesPermissionController::DialogFinished(bool accepted) { 270 // The dialog has finished, so there is no need to watch for more updates 271 // from |preferences_|. 272 // |preferences_| may be NULL in tests. 273 if (preferences_) 274 preferences_->RemoveGalleryChangeObserver(this); 275 276 if (accepted) 277 SavePermissions(); 278 279 on_finish_.Run(); 280 281 delete this; 282} 283 284content::WebContents* MediaGalleriesPermissionController::WebContents() { 285 return web_contents_; 286} 287 288void MediaGalleriesPermissionController::FileSelected( 289 const base::FilePath& path, 290 int /*index*/, 291 void* /*params*/) { 292 // |web_contents_| is NULL in tests. 293 if (web_contents_) { 294 extensions::file_system_api::SetLastChooseEntryDirectory( 295 extensions::ExtensionPrefs::Get(GetProfile()), 296 extension_->id(), 297 path); 298 } 299 300 // Try to find it in the prefs. 301 MediaGalleryPrefInfo gallery; 302 DCHECK(preferences_); 303 bool gallery_exists = preferences_->LookUpGalleryByPath(path, &gallery); 304 if (gallery_exists && !gallery.IsBlackListedType()) { 305 // The prefs are in sync with |known_galleries_|, so it should exist in 306 // |known_galleries_| as well. User selecting a known gallery effectively 307 // just sets the gallery to permitted. 308 GalleryDialogId gallery_id = GetDialogId(gallery.pref_id); 309 GalleryPermissionsMap::iterator iter = known_galleries_.find(gallery_id); 310 DCHECK(iter != known_galleries_.end()); 311 iter->second.selected = true; 312 forgotten_galleries_.erase(gallery_id); 313 dialog_->UpdateGalleries(); 314 return; 315 } 316 317 // Try to find it in |new_galleries_| (user added same folder twice). 318 for (GalleryPermissionsMap::iterator iter = new_galleries_.begin(); 319 iter != new_galleries_.end(); ++iter) { 320 if (iter->second.pref_info.path == gallery.path && 321 iter->second.pref_info.device_id == gallery.device_id) { 322 iter->second.selected = true; 323 dialog_->UpdateGalleries(); 324 return; 325 } 326 } 327 328 // Lastly, if not found, add a new gallery to |new_galleries_|. 329 // prefId == kInvalidMediaGalleryPrefId for completely new galleries. 330 // The old prefId is retained for blacklisted galleries. 331 gallery.pref_id = GetDialogId(gallery.pref_id); 332 new_galleries_[gallery.pref_id] = Entry(gallery, true); 333 dialog_->UpdateGalleries(); 334} 335 336void MediaGalleriesPermissionController::OnRemovableStorageAttached( 337 const StorageInfo& info) { 338 UpdateGalleriesOnDeviceEvent(info.device_id()); 339} 340 341void MediaGalleriesPermissionController::OnRemovableStorageDetached( 342 const StorageInfo& info) { 343 UpdateGalleriesOnDeviceEvent(info.device_id()); 344} 345 346void MediaGalleriesPermissionController::OnPermissionAdded( 347 MediaGalleriesPreferences* /* prefs */, 348 const std::string& extension_id, 349 MediaGalleryPrefId /* pref_id */) { 350 if (extension_id != extension_->id()) 351 return; 352 UpdateGalleriesOnPreferencesEvent(); 353} 354 355void MediaGalleriesPermissionController::OnPermissionRemoved( 356 MediaGalleriesPreferences* /* prefs */, 357 const std::string& extension_id, 358 MediaGalleryPrefId /* pref_id */) { 359 if (extension_id != extension_->id()) 360 return; 361 UpdateGalleriesOnPreferencesEvent(); 362} 363 364void MediaGalleriesPermissionController::OnGalleryAdded( 365 MediaGalleriesPreferences* /* prefs */, 366 MediaGalleryPrefId /* pref_id */) { 367 UpdateGalleriesOnPreferencesEvent(); 368} 369 370void MediaGalleriesPermissionController::OnGalleryRemoved( 371 MediaGalleriesPreferences* /* prefs */, 372 MediaGalleryPrefId /* pref_id */) { 373 UpdateGalleriesOnPreferencesEvent(); 374} 375 376void MediaGalleriesPermissionController::OnGalleryInfoUpdated( 377 MediaGalleriesPreferences* prefs, 378 MediaGalleryPrefId pref_id) { 379 DCHECK(preferences_); 380 const MediaGalleriesPrefInfoMap& pref_galleries = 381 preferences_->known_galleries(); 382 MediaGalleriesPrefInfoMap::const_iterator pref_it = 383 pref_galleries.find(pref_id); 384 if (pref_it == pref_galleries.end()) 385 return; 386 const MediaGalleryPrefInfo& gallery_info = pref_it->second; 387 UpdateGalleriesOnDeviceEvent(gallery_info.device_id); 388} 389 390void MediaGalleriesPermissionController::InitializePermissions() { 391 known_galleries_.clear(); 392 DCHECK(preferences_); 393 const MediaGalleriesPrefInfoMap& galleries = preferences_->known_galleries(); 394 for (MediaGalleriesPrefInfoMap::const_iterator iter = galleries.begin(); 395 iter != galleries.end(); 396 ++iter) { 397 const MediaGalleryPrefInfo& gallery = iter->second; 398 if (gallery.IsBlackListedType()) 399 continue; 400 401 GalleryDialogId gallery_id = GetDialogId(gallery.pref_id); 402 known_galleries_[gallery_id] = Entry(gallery, false); 403 known_galleries_[gallery_id].pref_info.pref_id = gallery_id; 404 } 405 406 pref_permitted_galleries_ = preferences_->GalleriesForExtension(*extension_); 407 408 for (MediaGalleryPrefIdSet::iterator iter = pref_permitted_galleries_.begin(); 409 iter != pref_permitted_galleries_.end(); 410 ++iter) { 411 GalleryDialogId gallery_id = GetDialogId(*iter); 412 DCHECK(ContainsKey(known_galleries_, gallery_id)); 413 known_galleries_[gallery_id].selected = true; 414 } 415 416 // Preserve state of toggled galleries. 417 for (ToggledGalleryMap::const_iterator iter = toggled_galleries_.begin(); 418 iter != toggled_galleries_.end(); 419 ++iter) { 420 known_galleries_[iter->first].selected = iter->second; 421 } 422} 423 424void MediaGalleriesPermissionController::SavePermissions() { 425 DCHECK(preferences_); 426 media_galleries::UsageCount(media_galleries::SAVE_DIALOG); 427 for (GalleryPermissionsMap::const_iterator iter = known_galleries_.begin(); 428 iter != known_galleries_.end(); ++iter) { 429 MediaGalleryPrefId pref_id = GetPrefId(iter->first); 430 if (ContainsKey(forgotten_galleries_, iter->first)) { 431 preferences_->ForgetGalleryById(pref_id); 432 } else { 433 bool changed = preferences_->SetGalleryPermissionForExtension( 434 *extension_, pref_id, iter->second.selected); 435 if (changed) { 436 if (iter->second.selected) { 437 media_galleries::UsageCount( 438 media_galleries::DIALOG_PERMISSION_ADDED); 439 } else { 440 media_galleries::UsageCount( 441 media_galleries::DIALOG_PERMISSION_REMOVED); 442 } 443 } 444 } 445 } 446 447 for (GalleryPermissionsMap::const_iterator iter = new_galleries_.begin(); 448 iter != new_galleries_.end(); ++iter) { 449 media_galleries::UsageCount(media_galleries::DIALOG_GALLERY_ADDED); 450 // If the user added a gallery then unchecked it, forget about it. 451 if (!iter->second.selected) 452 continue; 453 454 const MediaGalleryPrefInfo& gallery = iter->second.pref_info; 455 MediaGalleryPrefId id = preferences_->AddGallery( 456 gallery.device_id, gallery.path, MediaGalleryPrefInfo::kUserAdded, 457 gallery.volume_label, gallery.vendor_name, gallery.model_name, 458 gallery.total_size_in_bytes, gallery.last_attach_time, 0, 0, 0); 459 preferences_->SetGalleryPermissionForExtension(*extension_, id, true); 460 } 461} 462 463void MediaGalleriesPermissionController::UpdateGalleriesOnPreferencesEvent() { 464 // Merge in the permissions from |preferences_|. Afterwards, 465 // |known_galleries_| may contain galleries that no longer belong there, 466 // but the code below will put |known_galleries_| back in a consistent state. 467 InitializePermissions(); 468 469 std::set<GalleryDialogId> new_galleries_to_remove; 470 // Look for duplicate entries in |new_galleries_| in case one was added 471 // in another dialog. 472 for (GalleryPermissionsMap::iterator it = known_galleries_.begin(); 473 it != known_galleries_.end(); 474 ++it) { 475 Entry& gallery = it->second; 476 for (GalleryPermissionsMap::iterator new_it = new_galleries_.begin(); 477 new_it != new_galleries_.end(); 478 ++new_it) { 479 if (new_it->second.pref_info.path == gallery.pref_info.path && 480 new_it->second.pref_info.device_id == gallery.pref_info.device_id) { 481 // Found duplicate entry. Get the existing permission from it and then 482 // remove it. 483 gallery.selected = new_it->second.selected; 484 new_galleries_to_remove.insert(new_it->first); 485 break; 486 } 487 } 488 } 489 for (std::set<GalleryDialogId>::const_iterator it = 490 new_galleries_to_remove.begin(); 491 it != new_galleries_to_remove.end(); 492 ++it) { 493 new_galleries_.erase(*it); 494 } 495 496 dialog_->UpdateGalleries(); 497} 498 499void MediaGalleriesPermissionController::UpdateGalleriesOnDeviceEvent( 500 const std::string& device_id) { 501 dialog_->UpdateGalleries(); 502} 503 504ui::MenuModel* MediaGalleriesPermissionController::GetContextMenu( 505 GalleryDialogId gallery_id) { 506 context_menu_->set_pref_id(gallery_id); 507 return context_menu_.get(); 508} 509 510GalleryDialogId MediaGalleriesPermissionController::GetDialogId( 511 MediaGalleryPrefId pref_id) { 512 return id_map_.GetDialogId(pref_id); 513} 514 515MediaGalleryPrefId MediaGalleriesPermissionController::GetPrefId( 516 GalleryDialogId id) const { 517 return id_map_.GetPrefId(id); 518} 519 520Profile* MediaGalleriesPermissionController::GetProfile() { 521 return Profile::FromBrowserContext(web_contents_->GetBrowserContext()); 522} 523 524MediaGalleriesPermissionController::DialogIdMap::DialogIdMap() 525 : next_dialog_id_(1) { 526 // Dialog id of 0 is invalid, so fill the slot. 527 forward_mapping_.push_back(kInvalidMediaGalleryPrefId); 528} 529 530MediaGalleriesPermissionController::DialogIdMap::~DialogIdMap() { 531} 532 533GalleryDialogId 534MediaGalleriesPermissionController::DialogIdMap::GetDialogId( 535 MediaGalleryPrefId pref_id) { 536 std::map<GalleryDialogId, MediaGalleryPrefId>::const_iterator it = 537 back_map_.find(pref_id); 538 if (it != back_map_.end()) 539 return it->second; 540 541 GalleryDialogId result = next_dialog_id_++; 542 DCHECK_EQ(result, forward_mapping_.size()); 543 forward_mapping_.push_back(pref_id); 544 if (pref_id != kInvalidMediaGalleryPrefId) 545 back_map_[pref_id] = result; 546 return result; 547} 548 549MediaGalleryPrefId 550MediaGalleriesPermissionController::DialogIdMap::GetPrefId( 551 GalleryDialogId id) const { 552 DCHECK_LT(id, next_dialog_id_); 553 return forward_mapping_[id]; 554} 555 556// MediaGalleries dialog ------------------------------------------------------- 557 558MediaGalleriesDialog::~MediaGalleriesDialog() {} 559