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_scan_result_controller.h"
6
7#include <algorithm>
8#include <list>
9
10#include "base/bind.h"
11#include "base/logging.h"
12#include "base/metrics/histogram.h"
13#include "base/stl_util.h"
14#include "base/strings/utf_string_conversions.h"
15#include "chrome/browser/browser_process.h"
16#include "chrome/browser/media_galleries/media_file_system_registry.h"
17#include "chrome/browser/media_galleries/media_galleries_histograms.h"
18#include "chrome/browser/media_galleries/media_gallery_context_menu.h"
19#include "chrome/browser/platform_util.h"
20#include "chrome/browser/profiles/profile.h"
21#include "chrome/grit/generated_resources.h"
22#include "components/storage_monitor/storage_info.h"
23#include "components/storage_monitor/storage_monitor.h"
24#include "content/public/browser/web_contents.h"
25#include "extensions/common/extension.h"
26#include "extensions/common/permissions/media_galleries_permission.h"
27#include "extensions/common/permissions/permissions_data.h"
28#include "ui/base/l10n/l10n_util.h"
29
30using storage_monitor::StorageInfo;
31using storage_monitor::StorageMonitor;
32
33namespace {
34
35// Comparator for sorting Entries -- more files first and then sorts by
36// absolute path.
37bool ScanResultsComparator(
38    const MediaGalleriesDialogController::Entry& a,
39    const MediaGalleriesDialogController::Entry& b) {
40  int a_media_count = a.pref_info.audio_count + a.pref_info.image_count +
41                      a.pref_info.video_count;
42  int b_media_count = b.pref_info.audio_count + b.pref_info.image_count +
43                      b.pref_info.video_count;
44  if (a_media_count == b_media_count)
45    return a.pref_info.AbsolutePath() < b.pref_info.AbsolutePath();
46  return a_media_count > b_media_count;
47}
48
49}  // namespace
50
51// static
52size_t MediaGalleriesScanResultController::ScanResultCountForExtension(
53    MediaGalleriesPreferences* preferences,
54    const extensions::Extension* extension) {
55  ScanResults scan_results;
56  UpdateScanResultsFromPreferences(preferences, extension,
57                                   MediaGalleryPrefIdSet(), &scan_results);
58  return scan_results.size();
59}
60
61MediaGalleriesScanResultController::MediaGalleriesScanResultController(
62    content::WebContents* web_contents,
63    const extensions::Extension& extension,
64    const base::Closure& on_finish)
65      : web_contents_(web_contents),
66        extension_(&extension),
67        on_finish_(on_finish),
68        create_dialog_callback_(base::Bind(&MediaGalleriesDialog::Create)) {
69  preferences_ =
70      g_browser_process->media_file_system_registry()->GetPreferences(
71          GetProfile());
72  // Passing unretained pointer is safe, since the dialog controller
73  // is self-deleting, and so won't be deleted until it can be shown
74  // and then closed.
75  preferences_->EnsureInitialized(base::Bind(
76        &MediaGalleriesScanResultController::OnPreferencesInitialized,
77        base::Unretained(this)));
78
79  // Unretained is safe because |this| owns |context_menu_|.
80  context_menu_.reset(new MediaGalleryContextMenu(base::Bind(
81          &MediaGalleriesScanResultController::DidForgetEntry,
82          base::Unretained(this))));
83}
84
85MediaGalleriesScanResultController::MediaGalleriesScanResultController(
86    const extensions::Extension& extension,
87    MediaGalleriesPreferences* preferences,
88    const CreateDialogCallback& create_dialog_callback,
89    const base::Closure& on_finish)
90    : web_contents_(NULL),
91      extension_(&extension),
92      on_finish_(on_finish),
93      preferences_(preferences),
94      create_dialog_callback_(create_dialog_callback) {
95  OnPreferencesInitialized();
96}
97
98MediaGalleriesScanResultController::~MediaGalleriesScanResultController() {
99  // |preferences_| may be NULL in tests.
100  if (preferences_)
101    preferences_->RemoveGalleryChangeObserver(this);
102  if (StorageMonitor::GetInstance())
103    StorageMonitor::GetInstance()->RemoveObserver(this);
104}
105
106base::string16 MediaGalleriesScanResultController::GetHeader() const {
107  return l10n_util::GetStringFUTF16(
108      IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_HEADER,
109      base::UTF8ToUTF16(extension_->name()));
110}
111
112base::string16 MediaGalleriesScanResultController::GetSubtext() const {
113  extensions::MediaGalleriesPermission::CheckParam copy_to_param(
114      extensions::MediaGalleriesPermission::kCopyToPermission);
115  extensions::MediaGalleriesPermission::CheckParam delete_param(
116      extensions::MediaGalleriesPermission::kDeletePermission);
117  const extensions::PermissionsData* permissions_data =
118      extension_->permissions_data();
119  bool has_copy_to_permission = permissions_data->CheckAPIPermissionWithParam(
120      extensions::APIPermission::kMediaGalleries, &copy_to_param);
121  bool has_delete_permission = permissions_data->CheckAPIPermissionWithParam(
122      extensions::APIPermission::kMediaGalleries, &delete_param);
123
124  int id;
125  if (has_copy_to_permission)
126    id = IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_SUBTEXT_READ_WRITE;
127  else if (has_delete_permission)
128    id = IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_SUBTEXT_READ_DELETE;
129  else
130    id = IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_SUBTEXT_READ_ONLY;
131
132  return l10n_util::GetStringFUTF16(id, base::UTF8ToUTF16(extension_->name()));
133}
134
135bool MediaGalleriesScanResultController::IsAcceptAllowed() const {
136  return true;
137}
138
139bool MediaGalleriesScanResultController::ShouldShowFolderViewer(
140    const Entry& entry) const {
141  return entry.pref_info.IsGalleryAvailable();
142}
143
144std::vector<base::string16>
145MediaGalleriesScanResultController::GetSectionHeaders() const {
146  std::vector<base::string16> result;
147  result.push_back(base::string16());
148  return result;
149}
150
151MediaGalleriesDialogController::Entries
152MediaGalleriesScanResultController::GetSectionEntries(
153    size_t index) const {
154  DCHECK_EQ(0U, index);
155  Entries result;
156  result.reserve(scan_results_.size());
157  for (ScanResults::const_iterator it = scan_results_.begin();
158       it != scan_results_.end();
159       ++it) {
160    result.push_back(it->second);
161  }
162  std::sort(result.begin(), result.end(), ScanResultsComparator);
163  return result;
164}
165
166base::string16
167MediaGalleriesScanResultController::GetAuxiliaryButtonText() const {
168  return base::string16();
169}
170
171void MediaGalleriesScanResultController::DidClickAuxiliaryButton() {
172  NOTREACHED();
173}
174
175void MediaGalleriesScanResultController::DidToggleEntry(
176    MediaGalleryPrefId pref_id, bool selected) {
177  DCHECK(ContainsKey(scan_results_, pref_id));
178  ScanResults::iterator entry = scan_results_.find(pref_id);
179  entry->second.selected = selected;
180}
181
182void MediaGalleriesScanResultController::DidClickOpenFolderViewer(
183    MediaGalleryPrefId pref_id) {
184  ScanResults::const_iterator entry = scan_results_.find(pref_id);
185  if (entry == scan_results_.end()) {
186    NOTREACHED();
187    return;
188  }
189  platform_util::OpenItem(GetProfile(), entry->second.pref_info.AbsolutePath());
190}
191
192void MediaGalleriesScanResultController::DidForgetEntry(
193    MediaGalleryPrefId pref_id) {
194  media_galleries::UsageCount(media_galleries::ADD_SCAN_RESULTS_FORGET_GALLERY);
195  results_to_remove_.insert(pref_id);
196  scan_results_.erase(pref_id);
197  dialog_->UpdateGalleries();
198}
199
200base::string16 MediaGalleriesScanResultController::GetAcceptButtonText() const {
201  return l10n_util::GetStringUTF16(
202      IDS_MEDIA_GALLERIES_SCAN_RESULT_DIALOG_CONFIRM);
203}
204
205void MediaGalleriesScanResultController::DialogFinished(bool accepted) {
206  // No longer interested in preference updates (and the below code generates
207  // some).
208  // |preferences_| may be NULL in tests.
209  if (preferences_)
210    preferences_->RemoveGalleryChangeObserver(this);
211
212  if (accepted) {
213    DCHECK(preferences_);
214    media_galleries::UsageCount(media_galleries::ADD_SCAN_RESULTS_ACCEPTED);
215    int granted = 0;
216    int total = 0;
217    for (ScanResults::const_iterator it = scan_results_.begin();
218         it != scan_results_.end();
219         ++it) {
220      if (it->second.selected) {
221        bool changed = preferences_->SetGalleryPermissionForExtension(
222              *extension_, it->first, true);
223        DCHECK(changed);
224        granted++;
225      }
226      total++;
227    }
228    if (total > 0) {
229      UMA_HISTOGRAM_PERCENTAGE("MediaGalleries.ScanGalleriesGranted",
230                               (granted * 100 / total));
231    }
232    for (MediaGalleryPrefIdSet::const_iterator it = results_to_remove_.begin();
233        it != results_to_remove_.end();
234        ++it) {
235      preferences_->ForgetGalleryById(*it);
236    }
237  } else {
238    media_galleries::UsageCount(media_galleries::ADD_SCAN_RESULTS_CANCELLED);
239  }
240
241  on_finish_.Run();
242  delete this;
243}
244
245ui::MenuModel* MediaGalleriesScanResultController::GetContextMenu(
246    MediaGalleryPrefId id) {
247  context_menu_->set_pref_id(id);
248  return context_menu_.get();
249}
250
251content::WebContents* MediaGalleriesScanResultController::WebContents() {
252  return web_contents_;
253}
254
255// static
256void MediaGalleriesScanResultController::UpdateScanResultsFromPreferences(
257    MediaGalleriesPreferences* preferences,
258    const extensions::Extension* extension,
259    MediaGalleryPrefIdSet ignore_list,
260    ScanResults* scan_results) {
261  DCHECK(preferences->IsInitialized());
262  const MediaGalleriesPrefInfoMap& galleries = preferences->known_galleries();
263  MediaGalleryPrefIdSet permitted =
264      preferences->GalleriesForExtension(*extension);
265
266  // Add or update any scan results that the extension doesn't already have
267  // access to or isn't in |ignore_list|.
268  for (MediaGalleriesPrefInfoMap::const_iterator it = galleries.begin();
269       it != galleries.end();
270       ++it) {
271    const MediaGalleryPrefInfo& gallery = it->second;
272    if ((gallery.audio_count || gallery.image_count || gallery.video_count) &&
273        !gallery.IsBlackListedType() &&
274        !ContainsKey(permitted, gallery.pref_id) &&
275        !ContainsKey(ignore_list, gallery.pref_id)) {
276      ScanResults::iterator existing = scan_results->find(gallery.pref_id);
277      if (existing == scan_results->end()) {
278        // Default to selected.
279        (*scan_results)[gallery.pref_id] = Entry(gallery, true);
280      } else {
281        // Update pref_info, in case anything has been updated.
282        existing->second.pref_info = gallery;
283      }
284    }
285  }
286
287  // Remove anything from |scan_results| that's no longer valid or the user
288  // already has access to.
289  std::list<ScanResults::iterator> to_remove;
290  for (ScanResults::iterator it = scan_results->begin();
291       it != scan_results->end();
292       ++it) {
293    MediaGalleriesPrefInfoMap::const_iterator pref_gallery =
294        galleries.find(it->first);
295    if (pref_gallery == galleries.end() ||
296        pref_gallery->second.IsBlackListedType() ||
297        ContainsKey(permitted, it->first)) {
298      to_remove.push_back(it);
299    }
300  }
301  while (!to_remove.empty()) {
302    scan_results->erase(to_remove.front());
303    to_remove.pop_front();
304  }
305}
306
307void MediaGalleriesScanResultController::OnPreferencesInitialized() {
308  // These may be NULL in tests.
309  if (StorageMonitor::GetInstance())
310    StorageMonitor::GetInstance()->AddObserver(this);
311  if (preferences_) {
312    preferences_->AddGalleryChangeObserver(this);
313    UpdateScanResultsFromPreferences(preferences_, extension_,
314                                     results_to_remove_, &scan_results_);
315  }
316
317  dialog_.reset(create_dialog_callback_.Run(this));
318}
319
320void MediaGalleriesScanResultController::OnPreferenceUpdate(
321    const std::string& extension_id) {
322  if (extension_id == extension_->id()) {
323    UpdateScanResultsFromPreferences(preferences_, extension_,
324                                     results_to_remove_, &scan_results_);
325    dialog_->UpdateGalleries();
326  }
327}
328
329void MediaGalleriesScanResultController::OnRemovableDeviceUpdate(
330    const std::string device_id) {
331  for (ScanResults::const_iterator it = scan_results_.begin();
332       it != scan_results_.end();
333       ++it) {
334    if (it->second.pref_info.device_id == device_id) {
335      dialog_->UpdateGalleries();
336      return;
337    }
338  }
339}
340
341Profile* MediaGalleriesScanResultController::GetProfile() const {
342  return Profile::FromBrowserContext(web_contents_->GetBrowserContext());
343}
344
345void MediaGalleriesScanResultController::OnRemovableStorageAttached(
346    const StorageInfo& info) {
347  OnRemovableDeviceUpdate(info.device_id());
348}
349
350void MediaGalleriesScanResultController::OnRemovableStorageDetached(
351    const StorageInfo& info) {
352  OnRemovableDeviceUpdate(info.device_id());
353}
354
355void MediaGalleriesScanResultController::OnPermissionAdded(
356    MediaGalleriesPreferences* /*pref*/,
357    const std::string& extension_id,
358    MediaGalleryPrefId /*pref_id*/) {
359  OnPreferenceUpdate(extension_id);
360}
361
362void MediaGalleriesScanResultController::OnPermissionRemoved(
363    MediaGalleriesPreferences* /*pref*/,
364    const std::string& extension_id,
365    MediaGalleryPrefId /*pref_id*/) {
366  OnPreferenceUpdate(extension_id);
367}
368
369void MediaGalleriesScanResultController::OnGalleryAdded(
370    MediaGalleriesPreferences* /*prefs*/,
371    MediaGalleryPrefId /*pref_id*/) {
372  OnPreferenceUpdate(extension_->id());
373}
374
375void MediaGalleriesScanResultController::OnGalleryRemoved(
376    MediaGalleriesPreferences* /*prefs*/,
377    MediaGalleryPrefId /*pref_id*/) {
378  OnPreferenceUpdate(extension_->id());
379}
380
381void MediaGalleriesScanResultController::OnGalleryInfoUpdated(
382    MediaGalleriesPreferences* /*prefs*/,
383    MediaGalleryPrefId /*pref_id*/) {
384  OnPreferenceUpdate(extension_->id());
385}
386