Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(234)

Side by Side Diff: chrome/browser/download/download_history.cc

Issue 10915180: Make DownloadHistory observe manager, items (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: . Created 8 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 // DownloadHistory manages persisting DownloadItems to the history service by
6 // observing a single DownloadManager and all its DownloadItems.
Randy Smith (Not in Mondays) 2012/09/24 18:03:25 Suggestion: Modify now that it's not doing so dire
benjhayden 2012/11/02 17:21:37 Done.
7 // DownloadHistory decides whether and when to add items to, remove items from,
8 // and update items in the database. DownloadHistory uses DownloadHistoryData to
9 // store per-DownloadItem data such as its db_handle, whether the item is being
10 // added and waiting for its db_handle, and the last DownloadPersistentStoreInfo
11 // that was passed to the database. When the DownloadManager and its delegate
12 // (ChromeDownloadManagerDelegate) are initialized, DownloadHistory is created
13 // and queries the HistoryService. When the HistoryService calls back from
14 // QueryDownloads() to QueryCallback(), DownloadHistory uses
15 // DownloadManager::CreateDownloadItem() to inform DownloadManager of these
16 // persisted DownloadItems. CreateDownloadItem() internally calls
17 // OnDownloadCreated(), which normally adds items to the database, so
18 // QueryCallback() uses |loading_db_handle_| to disable adding these items to
19 // the database as it matches them up with their db_handles. If a download is
20 // removed via OnDownloadRemoved() while the item is still being added to the
21 // database, DownloadHistory uses |removed_while_adding_| to remember to remove
22 // the item when its ItemAdded() callback is called. All callbacks are bound
23 // with a weak pointer to DownloadHistory to prevent use-after-free bugs.
Randy Smith (Not in Mondays) 2012/09/24 18:03:25 Worth at least a note that this is redundant; I'm
benjhayden 2012/11/02 17:21:37 Done.
24 // ChromeDownloadManagerDelegate owns DownloadHistory, and deletes it in
25 // Shutdown(), which is called by DownloadManagerImpl::Shutdown() after all
26 // DownloadItems are destroyed.
27
5 #include "chrome/browser/download/download_history.h" 28 #include "chrome/browser/download/download_history.h"
6 29
7 #include "base/logging.h" 30 #include "base/metrics/histogram.h"
8 #include "chrome/browser/download/download_crx_util.h" 31 #include "chrome/browser/download/download_crx_util.h"
9 #include "chrome/browser/history/history_marshaling.h" 32 #include "chrome/browser/history/download_database.h"
10 #include "chrome/browser/history/history_service_factory.h" 33 #include "chrome/browser/history/download_persistent_store_info.h"
11 #include "chrome/browser/profiles/profile.h" 34 #include "chrome/browser/history/history.h"
35 #include "content/public/browser/browser_thread.h"
12 #include "content/public/browser/download_item.h" 36 #include "content/public/browser/download_item.h"
13 #include "content/public/browser/download_persistent_store_info.h" 37 #include "content/public/browser/download_manager.h"
14 38
15 using content::DownloadItem; 39 namespace {
16 using content::DownloadPersistentStoreInfo; 40
17 41 // Per-DownloadItem data. This information does not belong inside DownloadItem,
18 DownloadHistory::DownloadHistory(Profile* profile) 42 // and keeping maps in DownloadHistory from DownloadItem to this information is
19 : profile_(profile), 43 // error-prone and complicated. Unfortunately, DownloadHistory::removing_ and
20 next_fake_db_handle_(DownloadItem::kUninitializedHandle - 1) { 44 // removed_while_adding_ cannot be moved into this class partly because
21 DCHECK(profile); 45 // DownloadHistoryData is destroyed when DownloadItems are destroyed, and we
22 } 46 // have no control over when DownloadItems are destroyed.
23 47 class DownloadHistoryData : public base::SupportsUserData::Data {
24 DownloadHistory::~DownloadHistory() {} 48 public:
25 49 static DownloadHistoryData* Get(content::DownloadItem* item) {
26 void DownloadHistory::GetNextId( 50 base::SupportsUserData::Data* data = item->GetUserData(kKey);
27 const HistoryService::DownloadNextIdCallback& callback) { 51 return (data == NULL) ? NULL :
28 HistoryService* hs = HistoryServiceFactory::GetForProfile( 52 static_cast<DownloadHistoryData*>(data);
29 profile_, Profile::EXPLICIT_ACCESS); 53 }
30 if (!hs) 54
31 return; 55 DownloadHistoryData(content::DownloadItem* item, int64 handle)
32 56 : is_adding_(false),
33 hs->GetNextDownloadId(&history_consumer_, callback); 57 db_handle_(handle),
34 } 58 info_(NULL) {
35 59 item->SetUserData(kKey, this);
36 void DownloadHistory::Load( 60 }
37 const HistoryService::DownloadQueryCallback& callback) { 61
38 HistoryService* hs = HistoryServiceFactory::GetForProfile( 62 virtual ~DownloadHistoryData() {
39 profile_, Profile::EXPLICIT_ACCESS); 63 }
40 if (!hs) 64
41 return; 65 // Whether this item is currently being added to the database.
42 66 bool is_adding() const { return is_adding_; }
43 hs->QueryDownloads(&history_consumer_, callback); 67 void set_is_adding(bool a) { is_adding_ = a; }
44 68
45 // This is the initial load, so do a cleanup of corrupt in-progress entries. 69 // Whether this item is already persisted in the database.
46 hs->CleanUpInProgressEntries(); 70 bool is_persisted() const {
71 return db_handle_ != history::DownloadDatabase::kUninitializedHandle;
72 }
73
74 int64 db_handle() const { return db_handle_; }
75 void set_db_handle(int64 h) { db_handle_ = h; }
76
77 // This allows DownloadHistory::OnDownloadUpdated() to see what changed in a
78 // DownloadItem if anything, in order to prevent writing to the database
79 // unnecessarily. It is nullified when the item is no longer in progress in
80 // order to save memory.
81 DownloadPersistentStoreInfo* info() { return info_.get(); }
82 void set_info(const DownloadPersistentStoreInfo& i) {
83 info_.reset(new DownloadPersistentStoreInfo(i));
84 }
85 void clear_info() {
86 info_.reset();
87 }
88
89 private:
90 static const char kKey[];
91
92 bool is_adding_;
93 int64 db_handle_;
94 scoped_ptr<DownloadPersistentStoreInfo> info_;
95
96 DISALLOW_COPY_AND_ASSIGN(DownloadHistoryData);
97 };
98
99 const char DownloadHistoryData::kKey[] =
100 "DownloadItem DownloadHistoryData";
101
102 DownloadPersistentStoreInfo GetPersistentStoreInfo(
103 content::DownloadItem* item) {
104 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
105 return DownloadPersistentStoreInfo(
106 item->GetFullPath(),
107 item->GetURL(),
108 item->GetReferrerUrl(),
109 item->GetStartTime(),
110 item->GetEndTime(),
111 item->GetReceivedBytes(),
112 item->GetTotalBytes(),
113 item->GetState(),
114 ((dhd != NULL) ? dhd->db_handle()
115 : history::DownloadDatabase::kUninitializedHandle),
116 item->GetOpened());
117 }
118
119 typedef std::vector<DownloadPersistentStoreInfo> InfoVector;
120
121 } // anonymous namespace
122
123 DownloadHistory::Observer::Observer() {}
124 DownloadHistory::Observer::~Observer() {}
125
126 DownloadHistory::DownloadHistory(
127 content::DownloadManager* manager,
128 HistoryService* history)
129 : ALLOW_THIS_IN_INITIALIZER_LIST(notifier_(manager, this)),
130 history_(history),
131 loading_db_handle_(history::DownloadDatabase::kUninitializedHandle),
132 history_size_(0),
133 ALLOW_THIS_IN_INITIALIZER_LIST(weak_ptr_factory_(this)) {
134 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
135 content::DownloadManager::DownloadVector items;
136 notifier_.GetManager()->GetAllDownloads(&items);
137 for (content::DownloadManager::DownloadVector::const_iterator
138 it = items.begin(); it != items.end(); ++it) {
139 OnDownloadCreated(notifier_.GetManager(), *it);
140 }
141 history_->QueryDownloads(&history_consumer_, base::Bind(
142 &DownloadHistory::QueryCallback, weak_ptr_factory_.GetWeakPtr()));
143 }
144
145 DownloadHistory::~DownloadHistory() {
146 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
147 }
148
149 void DownloadHistory::QueryCallback(InfoVector* infos) {
150 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
151 // ManagerGoingDown() may have happened before the history loaded.
152 if (!notifier_.GetManager())
153 return;
154 for (InfoVector::const_iterator it = infos->begin();
155 it != infos->end(); ++it) {
156 // OnDownloadCreated() is called inside DM::CreateDownloadItem(), so set
157 // loading_db_handle_ to match up the created item with its db_handle. All
158 // methods run on the UI thread and CreateDownloadItem() is synchronous.
159 loading_db_handle_ = it->db_handle;
160 content::DownloadItem* download_item =
161 notifier_.GetManager()->CreateDownloadItem(
162 it->path,
163 it->url,
164 it->referrer_url,
165 it->start_time,
166 it->end_time,
167 it->received_bytes,
168 it->total_bytes,
169 it->state,
170 it->opened);
171 DownloadHistoryData* dhd = DownloadHistoryData::Get(download_item);
172
173 // If this DCHECK fails, then you probably added an Observer that
174 // synchronously creates a DownloadItem in response to
175 // DownloadManager::OnDownloadCreated(), and your observer runs before
176 // DownloadHistory, and DownloadManager creates items synchronously. Just
177 // bounce your DownloadItem creation off the message loop to flush
178 // DownloadHistory::OnDownloadCreated.
179 DCHECK_EQ(it->db_handle, dhd->db_handle());
180 ++history_size_;
181 }
182 notifier_.GetManager()->CheckForHistoryFilesRemoval();
183 }
184
185 void DownloadHistory::OnDownloadCreated(
186 content::DownloadManager* manager, content::DownloadItem* item) {
187 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
188
189 // All downloads should pass through OnDownloadCreated exactly once.
190 CHECK(!DownloadHistoryData::Get(item));
191 DownloadHistoryData* dhd = new DownloadHistoryData(item, loading_db_handle_);
192 loading_db_handle_ = history::DownloadDatabase::kUninitializedHandle;
193 if (item->GetState() == content::DownloadItem::IN_PROGRESS) {
194 dhd->set_info(GetPersistentStoreInfo(item));
195 }
196 MaybeAddToHistory(item);
197 }
198
199 void DownloadHistory::MaybeAddToHistory(content::DownloadItem* item) {
Randy Smith (Not in Mondays) 2012/09/24 18:03:25 Maybe add some blank lines in this function to gro
benjhayden 2012/11/02 17:21:37 Done.
200 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
201 int32 download_id = item->GetId();
202 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
203 bool removing = (removing_.find(dhd->db_handle()) != removing_.end());
204 // TODO(benjhayden): Remove IsTemporary().
205 if (download_crx_util::IsExtensionDownload(*item) ||
206 dhd->is_persisted() ||
207 item->IsTemporary() ||
208 removing ||
209 dhd->is_adding())
210 return;
211 dhd->set_is_adding(true);
212 if (dhd->info() == NULL) {
213 // Keep the info here regardless of whether the item is in progress so
214 // that, when ItemAdded() calls OnDownloadUpdated(), it can choose more
215 // intelligently whether to Update the db and/or discard the info.
216 dhd->set_info(GetPersistentStoreInfo(item));
217 }
218 history_->CreateDownload(*dhd->info(), &history_consumer_, base::Bind(
219 &DownloadHistory::ItemAdded, weak_ptr_factory_.GetWeakPtr(),
220 download_id));
221 FOR_EACH_OBSERVER(Observer, observers_, OnDownloadStored(*dhd->info()));
222 }
223
224 void DownloadHistory::ItemAdded(int32 download_id, int64 db_handle) {
225 if (removed_while_adding_.find(download_id) !=
226 removed_while_adding_.end()) {
227 removed_while_adding_.erase(download_id);
228 if (removing_.empty()) {
229 content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE,
230 base::Bind(&DownloadHistory::RemoveDownloadsBatch,
231 weak_ptr_factory_.GetWeakPtr()));
232 }
233 removing_.insert(db_handle);
234 return;
235 }
236
237 if (!notifier_.GetManager())
238 return;
239
240 content::DownloadItem* item = notifier_.GetManager()->GetDownload(
241 download_id);
242 if (!item) {
243 // This item will have called OnDownloadDestroyed(). If the item should
244 // have been removed from history, then it would have also called
245 // OnDownloadRemoved(), which would have put |download_id| in
246 // removed_while_adding_, handled above.
247 return;
248 }
249
250 UMA_HISTOGRAM_CUSTOM_COUNTS("Download.HistorySize2",
251 history_size_,
252 0/*min*/,
253 (1 << 23)/*max*/,
254 (1 << 7)/*num_buckets*/);
255 ++history_size_;
256
257 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
258 dhd->set_is_adding(false);
259 DCHECK_NE(db_handle, history::DownloadDatabase::kUninitializedHandle);
260 dhd->set_db_handle(db_handle);
261
262 // In case the item changed or became temporary while it was being added.
263 // Don't just update all of the item's observers because we're the only
264 // observer that can also see db_handle, which is the only thing that
265 // ItemAdded changed.
266 OnDownloadUpdated(notifier_.GetManager(), item);
267 }
268
269 void DownloadHistory::OnDownloadUpdated(
270 content::DownloadManager* manager, content::DownloadItem* item) {
271 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
272
273 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
274 if (!dhd->is_persisted()) {
275 MaybeAddToHistory(item);
276 return;
277 }
278 if (item->IsTemporary()) {
279 OnDownloadRemoved(notifier_.GetManager(), item);
280 return;
281 }
282
283 // TODO(asanka): Persist GetTargetFilePath() as well.
284 DownloadPersistentStoreInfo current_info(GetPersistentStoreInfo(item));
285 DownloadPersistentStoreInfo* previous_info = dhd->info();
286 bool do_update = (
287 (previous_info == NULL) ||
Randy Smith (Not in Mondays) 2012/09/24 18:03:25 Suggestion: Given that you've abstracted out GetPe
benjhayden 2012/11/02 17:21:37 Done.
288 (previous_info->path != current_info.path) ||
289 (previous_info->end_time != current_info.end_time) ||
290 (previous_info->received_bytes != current_info.received_bytes) ||
291 (previous_info->total_bytes != current_info.total_bytes) ||
292 (previous_info->state != current_info.state) ||
293 (previous_info->opened != current_info.opened));
294 UMA_HISTOGRAM_ENUMERATION("Download.HistoryPropagatedUpdate", do_update, 2);
295 if (do_update) {
296 history_->UpdateDownload(current_info);
297 FOR_EACH_OBSERVER(Observer, observers_, OnDownloadStored(current_info));
298 }
299 if (item->GetState() == content::DownloadItem::IN_PROGRESS) {
300 dhd->set_info(current_info);
301 } else {
302 dhd->clear_info();
303 }
304 }
305
306 // Downloads may be opened after they are completed.
307 void DownloadHistory::OnDownloadOpened(
308 content::DownloadManager* manager, content::DownloadItem* item) {
309 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
310 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
311 if (!dhd->is_persisted()) {
312 MaybeAddToHistory(item);
313 return;
314 }
315 if (item->IsTemporary()) {
316 OnDownloadRemoved(manager, item);
317 return;
318 }
319
320 DownloadPersistentStoreInfo current_info(GetPersistentStoreInfo(item));
321 history_->UpdateDownload(current_info);
322 FOR_EACH_OBSERVER(Observer, observers_, OnDownloadStored(current_info));
323 if (item->GetState() == content::DownloadItem::IN_PROGRESS) {
324 dhd->set_info(current_info);
325 }
326 }
327
328 void DownloadHistory::OnDownloadRemoved(
329 content::DownloadManager* manager, content::DownloadItem* item) {
330 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
331
332 DownloadHistoryData* dhd = DownloadHistoryData::Get(item);
333 if (!dhd->is_persisted()) {
334 if (dhd->is_adding()) {
335 removed_while_adding_.insert(item->GetId());
336 }
337 return;
338 }
339
340 // For database efficiency, batch removals together if they happen all at
341 // once.
342 if (removing_.empty()) {
343 content::BrowserThread::PostTask(content::BrowserThread::UI, FROM_HERE,
344 base::Bind(&DownloadHistory::RemoveDownloadsBatch,
345 weak_ptr_factory_.GetWeakPtr()));
346 }
347 removing_.insert(dhd->db_handle());
348 dhd->set_db_handle(history::DownloadDatabase::kUninitializedHandle);
349 --history_size_;
350 }
351
352 void DownloadHistory::RemoveDownloadsBatch() {
353 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
354 HandleSet remove_handles;
355 removing_.swap(remove_handles);
356 history_->RemoveDownloads(remove_handles);
357 FOR_EACH_OBSERVER(Observer, observers_, OnDownloadsRemoved(remove_handles));
47 } 358 }
48 359
49 void DownloadHistory::CheckVisitedReferrerBefore( 360 void DownloadHistory::CheckVisitedReferrerBefore(
50 int32 download_id,
51 const GURL& referrer_url, 361 const GURL& referrer_url,
52 const VisitedBeforeDoneCallback& callback) { 362 const VisitedBeforeDoneCallback& callback) {
53 if (referrer_url.is_valid()) { 363 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
54 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists( 364 if (!referrer_url.is_valid()) {
55 profile_, Profile::EXPLICIT_ACCESS); 365 callback.Run(false);
56 if (hs) { 366 return;
57 HistoryService::Handle handle = 367 }
58 hs->GetVisibleVisitCountToHost(referrer_url, &history_consumer_, 368 history_->GetVisibleVisitCountToHost(
59 base::Bind(&DownloadHistory::OnGotVisitCountToHost, 369 referrer_url, &history_consumer_, base::Bind(
60 base::Unretained(this))); 370 &DownloadHistory::OnGotVisitCountToHost,
61 visited_before_requests_[handle] = callback; 371 weak_ptr_factory_.GetWeakPtr(), callback));
62 return; 372 }
63 } 373
64 } 374 void DownloadHistory::OnGotVisitCountToHost(
65 callback.Run(false); 375 const VisitedBeforeDoneCallback& callback,
66 } 376 HistoryService::Handle unused_handle,
67 377 bool found_visits,
68 void DownloadHistory::AddEntry( 378 int count,
69 DownloadItem* download_item, 379 base::Time first_visit) {
70 const HistoryService::DownloadCreateCallback& callback) { 380 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
71 DCHECK(download_item);
72 // Do not store the download in the history database for a few special cases:
73 // - incognito mode (that is the point of this mode)
74 // - extensions (users don't think of extension installation as 'downloading')
75 // - temporary download, like in drag-and-drop
76 // - history service is not available (e.g. in tests)
77 // We have to make sure that these handles don't collide with normal db
78 // handles, so we use a negative value. Eventually, they could overlap, but
79 // you'd have to do enough downloading that your ISP would likely stab you in
80 // the neck first. YMMV.
81 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists(
82 profile_, Profile::EXPLICIT_ACCESS);
83 if (download_crx_util::IsExtensionDownload(*download_item) ||
84 download_item->IsTemporary() || !hs) {
85 callback.Run(download_item->GetId(), GetNextFakeDbHandle());
86 return;
87 }
88
89 int32 id = download_item->GetId();
90 DownloadPersistentStoreInfo history_info =
91 download_item->GetPersistentStoreInfo();
92 hs->CreateDownload(id, history_info, &history_consumer_, callback);
93 }
94
95 void DownloadHistory::UpdateEntry(DownloadItem* download_item) {
96 // Don't store info in the database if the download was initiated while in
97 // incognito mode or if it hasn't been initialized in our database table.
98 if (download_item->GetDbHandle() <= DownloadItem::kUninitializedHandle)
99 return;
100
101 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists(
102 profile_, Profile::EXPLICIT_ACCESS);
103 if (!hs)
104 return;
105 hs->UpdateDownload(download_item->GetPersistentStoreInfo());
106 }
107
108 void DownloadHistory::UpdateDownloadPath(DownloadItem* download_item,
109 const FilePath& new_path) {
110 // No update necessary if the download was initiated while in incognito mode.
111 if (download_item->GetDbHandle() <= DownloadItem::kUninitializedHandle)
112 return;
113
114 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists(
115 profile_, Profile::EXPLICIT_ACCESS);
116 if (hs)
117 hs->UpdateDownloadPath(new_path, download_item->GetDbHandle());
118 }
119
120 void DownloadHistory::RemoveEntry(DownloadItem* download_item) {
121 // No update necessary if the download was initiated while in incognito mode.
122 if (download_item->GetDbHandle() <= DownloadItem::kUninitializedHandle)
123 return;
124
125 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists(
126 profile_, Profile::EXPLICIT_ACCESS);
127 if (hs)
128 hs->RemoveDownload(download_item->GetDbHandle());
129 }
130
131 void DownloadHistory::RemoveEntriesBetween(const base::Time remove_begin,
132 const base::Time remove_end) {
133 HistoryService* hs = HistoryServiceFactory::GetForProfileIfExists(
134 profile_, Profile::EXPLICIT_ACCESS);
135 if (hs)
136 hs->RemoveDownloadsBetween(remove_begin, remove_end);
137 }
138
139 int64 DownloadHistory::GetNextFakeDbHandle() {
140 return next_fake_db_handle_--;
141 }
142
143 void DownloadHistory::OnGotVisitCountToHost(HistoryService::Handle handle,
144 bool found_visits,
145 int count,
146 base::Time first_visit) {
147 VisitedBeforeRequestsMap::iterator request =
148 visited_before_requests_.find(handle);
149 DCHECK(request != visited_before_requests_.end());
150 VisitedBeforeDoneCallback callback = request->second;
151 visited_before_requests_.erase(request);
152 callback.Run(found_visits && count && 381 callback.Run(found_visits && count &&
153 (first_visit.LocalMidnight() < base::Time::Now().LocalMidnight())); 382 (first_visit.LocalMidnight() < base::Time::Now().LocalMidnight()));
154 } 383 }
384
385 void DownloadHistory::AddObserver(DownloadHistory::Observer* observer) {
386 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
387 observers_.AddObserver(observer);
388 }
389
390 void DownloadHistory::RemoveObserver(DownloadHistory::Observer* observer) {
391 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
392 observers_.RemoveObserver(observer);
393 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698