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/gallery_watch_manager.h"
6
7#include "base/bind.h"
8#include "base/stl_util.h"
9#include "base/time/time.h"
10#include "chrome/browser/browser_process.h"
11#include "chrome/browser/media_galleries/gallery_watch_manager_observer.h"
12#include "chrome/browser/media_galleries/media_file_system_registry.h"
13#include "chrome/browser/profiles/profile.h"
14#include "components/storage_monitor/storage_monitor.h"
15#include "content/public/browser/browser_context.h"
16#include "content/public/browser/browser_thread.h"
17#include "extensions/common/extension.h"
18
19using content::BrowserContext;
20using content::BrowserThread;
21
22namespace {
23
24// Don't send a notification more than once per 3 seconds (chosen arbitrarily).
25const int kMinNotificiationDelayInSeconds = 3;
26
27}  // namespace.
28
29const char GalleryWatchManager::kInvalidGalleryIDError[] = "Invalid gallery ID";
30const char GalleryWatchManager::kNoPermissionError[] =
31    "No permission for gallery ID.";
32const char GalleryWatchManager::kCouldNotWatchGalleryError[] =
33    "Could not watch gallery path.";
34
35// Manages a collection of file path watchers on the FILE thread and relays
36// the change events to |callback| on the UI thread. This file is constructed
37// on the UI thread, but operates and is destroyed on the FILE thread.
38// If |callback| is called with an error, all watches on that path have been
39// dropped.
40class GalleryWatchManager::FileWatchManager {
41 public:
42  explicit FileWatchManager(const base::FilePathWatcher::Callback& callback);
43  ~FileWatchManager();
44
45  // Posts success or failure via |callback| to the UI thread.
46  void AddFileWatch(const base::FilePath& path,
47                    const base::Callback<void(bool)>& callback);
48
49  void RemoveFileWatch(const base::FilePath& path);
50
51  base::WeakPtr<FileWatchManager> GetWeakPtr();
52
53 private:
54  typedef std::map<base::FilePath, linked_ptr<base::FilePathWatcher> >
55      WatcherMap;
56
57  void OnFilePathChanged(const base::FilePath& path, bool error);
58
59  WatcherMap watchers_;
60
61  base::FilePathWatcher::Callback callback_;
62
63  base::WeakPtrFactory<FileWatchManager> weak_factory_;
64
65  DISALLOW_COPY_AND_ASSIGN(FileWatchManager);
66};
67
68GalleryWatchManager::FileWatchManager::FileWatchManager(
69    const base::FilePathWatcher::Callback& callback)
70    : callback_(callback), weak_factory_(this) {
71  DCHECK_CURRENTLY_ON(BrowserThread::UI);
72}
73
74GalleryWatchManager::FileWatchManager::~FileWatchManager() {
75  DCHECK_CURRENTLY_ON(BrowserThread::FILE);
76}
77
78void GalleryWatchManager::FileWatchManager::AddFileWatch(
79    const base::FilePath& path,
80    const base::Callback<void(bool)>& callback) {
81  DCHECK_CURRENTLY_ON(BrowserThread::FILE);
82
83  // This can occur if the GalleryWatchManager attempts to watch the same path
84  // again before recieving the callback. It's benign.
85  if (ContainsKey(watchers_, path)) {
86    BrowserThread::PostTask(
87        BrowserThread::UI, FROM_HERE, base::Bind(callback, false));
88    return;
89  }
90
91  linked_ptr<base::FilePathWatcher> watcher(new base::FilePathWatcher);
92  bool success = watcher->Watch(path,
93                                true /*recursive*/,
94                                base::Bind(&FileWatchManager::OnFilePathChanged,
95                                           weak_factory_.GetWeakPtr()));
96
97  if (success)
98    watchers_[path] = watcher;
99
100  BrowserThread::PostTask(
101      BrowserThread::UI, FROM_HERE, base::Bind(callback, success));
102}
103
104void GalleryWatchManager::FileWatchManager::RemoveFileWatch(
105    const base::FilePath& path) {
106  DCHECK_CURRENTLY_ON(BrowserThread::FILE);
107  size_t erased = watchers_.erase(path);
108  DCHECK_EQ(erased, 1u);
109}
110
111base::WeakPtr<GalleryWatchManager::FileWatchManager>
112GalleryWatchManager::FileWatchManager::GetWeakPtr() {
113  return weak_factory_.GetWeakPtr();
114}
115
116void GalleryWatchManager::FileWatchManager::OnFilePathChanged(
117    const base::FilePath& path,
118    bool error) {
119  DCHECK_CURRENTLY_ON(BrowserThread::FILE);
120  if (error)
121    RemoveFileWatch(path);
122  BrowserThread::PostTask(
123      BrowserThread::UI, FROM_HERE, base::Bind(callback_, path, error));
124}
125
126GalleryWatchManager::WatchOwner::WatchOwner(BrowserContext* browser_context,
127                                            const std::string& extension_id,
128                                            MediaGalleryPrefId gallery_id)
129    : browser_context(browser_context),
130      extension_id(extension_id),
131      gallery_id(gallery_id) {
132}
133
134bool GalleryWatchManager::WatchOwner::operator<(const WatchOwner& other) const {
135  return browser_context < other.browser_context ||
136         extension_id < other.extension_id || gallery_id < other.gallery_id;
137}
138
139GalleryWatchManager::NotificationInfo::NotificationInfo()
140    : delayed_notification_pending(false) {
141}
142
143GalleryWatchManager::NotificationInfo::~NotificationInfo() {
144}
145
146GalleryWatchManager::GalleryWatchManager()
147    : storage_monitor_observed_(false), weak_factory_(this) {
148  DCHECK_CURRENTLY_ON(BrowserThread::UI);
149  watch_manager_.reset(new FileWatchManager(base::Bind(
150      &GalleryWatchManager::OnFilePathChanged, weak_factory_.GetWeakPtr())));
151}
152
153GalleryWatchManager::~GalleryWatchManager() {
154  weak_factory_.InvalidateWeakPtrs();
155
156  if (storage_monitor_observed_ &&
157      storage_monitor::StorageMonitor::GetInstance()) {
158    storage_monitor::StorageMonitor::GetInstance()->RemoveObserver(this);
159  }
160
161  BrowserThread::DeleteSoon(
162      BrowserThread::FILE, FROM_HERE, watch_manager_.release());
163}
164
165void GalleryWatchManager::AddObserver(BrowserContext* browser_context,
166                                      GalleryWatchManagerObserver* observer) {
167  DCHECK(browser_context);
168  DCHECK(observer);
169  DCHECK(!ContainsKey(observers_, browser_context));
170  observers_[browser_context] = observer;
171}
172
173void GalleryWatchManager::RemoveObserver(BrowserContext* browser_context) {
174  DCHECK(browser_context);
175  size_t erased = observers_.erase(browser_context);
176  DCHECK_EQ(erased, 1u);
177}
178
179void GalleryWatchManager::ShutdownBrowserContext(
180    BrowserContext* browser_context) {
181  DCHECK_CURRENTLY_ON(BrowserThread::UI);
182  DCHECK(browser_context);
183
184  MediaGalleriesPreferences* preferences =
185      g_browser_process->media_file_system_registry()->GetPreferences(
186          Profile::FromBrowserContext(browser_context));
187  size_t observed = observed_preferences_.erase(preferences);
188  if (observed > 0)
189    preferences->RemoveGalleryChangeObserver(this);
190}
191
192void GalleryWatchManager::AddWatch(BrowserContext* browser_context,
193                                   const extensions::Extension* extension,
194                                   MediaGalleryPrefId gallery_id,
195                                   const ResultCallback& callback) {
196  DCHECK_CURRENTLY_ON(BrowserThread::UI);
197  DCHECK(browser_context);
198  DCHECK(extension);
199
200  WatchOwner owner(browser_context, extension->id(), gallery_id);
201  if (ContainsKey(watches_, owner)) {
202    callback.Run(std::string());
203    return;
204  }
205
206  MediaGalleriesPreferences* preferences =
207      g_browser_process->media_file_system_registry()->GetPreferences(
208          Profile::FromBrowserContext(browser_context));
209
210  if (!ContainsKey(preferences->known_galleries(), gallery_id)) {
211    callback.Run(kInvalidGalleryIDError);
212    return;
213  }
214
215  MediaGalleryPrefIdSet permitted =
216      preferences->GalleriesForExtension(*extension);
217  if (!ContainsKey(permitted, gallery_id)) {
218    callback.Run(kNoPermissionError);
219    return;
220  }
221
222  base::FilePath path =
223      preferences->known_galleries().find(gallery_id)->second.AbsolutePath();
224
225  if (!storage_monitor_observed_) {
226    storage_monitor_observed_ = true;
227    storage_monitor::StorageMonitor::GetInstance()->AddObserver(this);
228  }
229
230  // Observe the preferences if we haven't already.
231  if (!ContainsKey(observed_preferences_, preferences)) {
232    observed_preferences_.insert(preferences);
233    preferences->AddGalleryChangeObserver(this);
234  }
235
236  watches_[owner] = path;
237
238  // Start the FilePathWatcher on |gallery_path| if necessary.
239  if (ContainsKey(watched_paths_, path)) {
240    OnFileWatchActivated(owner, path, callback, true);
241  } else {
242    base::Callback<void(bool)> on_watch_added =
243        base::Bind(&GalleryWatchManager::OnFileWatchActivated,
244                   weak_factory_.GetWeakPtr(),
245                   owner,
246                   path,
247                   callback);
248    BrowserThread::PostTask(BrowserThread::FILE,
249                            FROM_HERE,
250                            base::Bind(&FileWatchManager::AddFileWatch,
251                                       watch_manager_->GetWeakPtr(),
252                                       path,
253                                       on_watch_added));
254  }
255}
256
257void GalleryWatchManager::RemoveWatch(BrowserContext* browser_context,
258                                      const std::string& extension_id,
259                                      MediaGalleryPrefId gallery_id) {
260  DCHECK_CURRENTLY_ON(BrowserThread::UI);
261  DCHECK(browser_context);
262
263  WatchOwner owner(browser_context, extension_id, gallery_id);
264  WatchesMap::iterator it = watches_.find(owner);
265  if (it != watches_.end()) {
266    DeactivateFileWatch(owner, it->second);
267    watches_.erase(it);
268  }
269}
270
271void GalleryWatchManager::RemoveAllWatches(BrowserContext* browser_context,
272                                           const std::string& extension_id) {
273  DCHECK_CURRENTLY_ON(BrowserThread::UI);
274  DCHECK(browser_context);
275
276  WatchesMap::iterator it = watches_.begin();
277  while (it != watches_.end()) {
278    if (it->first.extension_id == extension_id) {
279      DeactivateFileWatch(it->first, it->second);
280      // Post increment moves iterator to next element while deleting current.
281      watches_.erase(it++);
282    } else {
283      ++it;
284    }
285  }
286}
287
288MediaGalleryPrefIdSet GalleryWatchManager::GetWatchSet(
289    BrowserContext* browser_context,
290    const std::string& extension_id) {
291  DCHECK_CURRENTLY_ON(BrowserThread::UI);
292  DCHECK(browser_context);
293
294  MediaGalleryPrefIdSet result;
295  for (WatchesMap::const_iterator it = watches_.begin(); it != watches_.end();
296       ++it) {
297    if (it->first.browser_context == browser_context &&
298        it->first.extension_id == extension_id) {
299      result.insert(it->first.gallery_id);
300    }
301  }
302  return result;
303}
304
305void GalleryWatchManager::DeactivateFileWatch(const WatchOwner& owner,
306                                              const base::FilePath& path) {
307  DCHECK_CURRENTLY_ON(BrowserThread::UI);
308  WatchedPaths::iterator it = watched_paths_.find(path);
309  if (it == watched_paths_.end())
310    return;
311
312  it->second.owners.erase(owner);
313  if (it->second.owners.empty()) {
314    watched_paths_.erase(it);
315    BrowserThread::PostTask(BrowserThread::FILE,
316                            FROM_HERE,
317                            base::Bind(&FileWatchManager::RemoveFileWatch,
318                                       watch_manager_->GetWeakPtr(),
319                                       path));
320  }
321}
322
323void GalleryWatchManager::OnFileWatchActivated(const WatchOwner& owner,
324                                               const base::FilePath& path,
325                                               const ResultCallback& callback,
326                                               bool success) {
327  if (success) {
328    // |watched_paths_| doesn't necessarily to contain |path| yet.
329    // In that case, it calls the default constructor for NotificationInfo.
330    watched_paths_[path].owners.insert(owner);
331    callback.Run(std::string());
332  } else {
333    callback.Run(kCouldNotWatchGalleryError);
334  }
335}
336
337void GalleryWatchManager::OnFilePathChanged(const base::FilePath& path,
338                                            bool error) {
339  WatchedPaths::iterator notification_info = watched_paths_.find(path);
340  if (notification_info == watched_paths_.end())
341    return;
342
343  // On error, all watches on that path are dropped, so update records and
344  // notify observers.
345  if (error) {
346    // Make a copy, as |watched_paths_| is modified as we erase watches.
347    std::set<WatchOwner> owners = notification_info->second.owners;
348    for (std::set<WatchOwner>::iterator it = owners.begin(); it != owners.end();
349         ++it) {
350      Profile* profile = Profile::FromBrowserContext(it->browser_context);
351      RemoveWatch(it->browser_context, it->extension_id, it->gallery_id);
352      if (ContainsKey(observers_, profile))
353        observers_[profile]->OnGalleryWatchDropped(it->extension_id,
354                                                   it->gallery_id);
355    }
356
357    return;
358  }
359
360  base::TimeDelta time_since_last_notify =
361      base::Time::Now() - notification_info->second.last_notify_time;
362  if (time_since_last_notify <
363      base::TimeDelta::FromSeconds(kMinNotificiationDelayInSeconds)) {
364    if (!notification_info->second.delayed_notification_pending) {
365      notification_info->second.delayed_notification_pending = true;
366      base::TimeDelta delay_to_next_valid_time =
367          notification_info->second.last_notify_time +
368          base::TimeDelta::FromSeconds(kMinNotificiationDelayInSeconds) -
369          base::Time::Now();
370      BrowserThread::PostDelayedTask(
371          BrowserThread::UI,
372          FROM_HERE,
373          base::Bind(&GalleryWatchManager::OnFilePathChanged,
374                     weak_factory_.GetWeakPtr(),
375                     path,
376                     error),
377          delay_to_next_valid_time);
378    }
379    return;
380  }
381  notification_info->second.delayed_notification_pending = false;
382  notification_info->second.last_notify_time = base::Time::Now();
383
384  std::set<WatchOwner>::const_iterator it;
385  for (it = notification_info->second.owners.begin();
386       it != notification_info->second.owners.end();
387       ++it) {
388    DCHECK(ContainsKey(watches_, *it));
389    if (ContainsKey(observers_, it->browser_context)) {
390      observers_[it->browser_context]->OnGalleryChanged(it->extension_id,
391                                                        it->gallery_id);
392    }
393  }
394}
395
396void GalleryWatchManager::OnPermissionRemoved(MediaGalleriesPreferences* pref,
397                                              const std::string& extension_id,
398                                              MediaGalleryPrefId pref_id) {
399  RemoveWatch(pref->profile(), extension_id, pref_id);
400  if (ContainsKey(observers_, pref->profile()))
401    observers_[pref->profile()]->OnGalleryWatchDropped(extension_id, pref_id);
402}
403
404void GalleryWatchManager::OnGalleryRemoved(MediaGalleriesPreferences* pref,
405                                           MediaGalleryPrefId pref_id) {
406  // Removing a watch may update |watches_|, so extract the extension ids first.
407  std::set<std::string> extension_ids;
408  for (WatchesMap::const_iterator it = watches_.begin(); it != watches_.end();
409       ++it) {
410    if (it->first.browser_context == pref->profile() &&
411        it->first.gallery_id == pref_id) {
412      extension_ids.insert(it->first.extension_id);
413    }
414  }
415
416  for (std::set<std::string>::const_iterator it = extension_ids.begin();
417       it != extension_ids.end();
418       ++it) {
419    RemoveWatch(pref->profile(), *it, pref_id);
420    if (ContainsKey(observers_, pref->profile()))
421      observers_[pref->profile()]->OnGalleryWatchDropped(*it, pref_id);
422  }
423}
424
425void GalleryWatchManager::OnRemovableStorageDetached(
426    const storage_monitor::StorageInfo& info) {
427  WatchesMap::iterator it = watches_.begin();
428  while (it != watches_.end()) {
429    MediaGalleriesPreferences* preferences =
430        g_browser_process->media_file_system_registry()->GetPreferences(
431            Profile::FromBrowserContext(it->first.browser_context));
432    MediaGalleryPrefIdSet detached_ids =
433        preferences->LookUpGalleriesByDeviceId(info.device_id());
434
435    if (ContainsKey(detached_ids, it->first.gallery_id)) {
436      DeactivateFileWatch(it->first, it->second);
437      // Post increment moves iterator to next element while deleting current.
438      watches_.erase(it++);
439      observers_[preferences->profile()]->OnGalleryWatchDropped(
440          it->first.extension_id, it->first.gallery_id);
441    } else {
442      ++it;
443    }
444  }
445}
446