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 "chrome/grit/generated_resources.h"
19#include "components/storage_monitor/storage_info.h"
20#include "components/storage_monitor/storage_monitor.h"
21#include "content/public/browser/web_contents.h"
22#include "extensions/browser/extension_prefs.h"
23#include "extensions/common/extension.h"
24#include "extensions/common/permissions/media_galleries_permission.h"
25#include "extensions/common/permissions/permissions_data.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, &copy_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