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/chromeos/drive/directory_loader.h"
6
7 #include "base/callback.h"
8 #include "base/callback_helpers.h"
9 #include "base/metrics/histogram.h"
10 #include "base/strings/string_number_conversions.h"
11 #include "base/time/time.h"
12 #include "chrome/browser/chromeos/drive/change_list_loader.h"
13 #include "chrome/browser/chromeos/drive/change_list_loader_observer.h"
14 #include "chrome/browser/chromeos/drive/change_list_processor.h"
15 #include "chrome/browser/chromeos/drive/file_system_util.h"
16 #include "chrome/browser/chromeos/drive/job_scheduler.h"
17 #include "chrome/browser/chromeos/drive/resource_metadata.h"
18 #include "chrome/browser/drive/event_logger.h"
19 #include "content/public/browser/browser_thread.h"
20 #include "google_apis/drive/drive_api_parser.h"
21 #include "url/gurl.h"
22
23 using content::BrowserThread;
24
25 namespace drive {
26 namespace internal {
27
28 namespace {
29
30 // Minimum changestamp gap required to start loading directory.
31 const int kMinimumChangestampGap = 50;
32
CheckLocalState(ResourceMetadata * resource_metadata,const google_apis::AboutResource & about_resource,const std::string & local_id,ResourceEntry * entry,int64 * local_changestamp)33 FileError CheckLocalState(ResourceMetadata* resource_metadata,
34 const google_apis::AboutResource& about_resource,
35 const std::string& local_id,
36 ResourceEntry* entry,
37 int64* local_changestamp) {
38 // Fill My Drive resource ID.
39 ResourceEntry mydrive;
40 FileError error = resource_metadata->GetResourceEntryByPath(
41 util::GetDriveMyDriveRootPath(), &mydrive);
42 if (error != FILE_ERROR_OK)
43 return error;
44
45 if (mydrive.resource_id().empty()) {
46 mydrive.set_resource_id(about_resource.root_folder_id());
47 error = resource_metadata->RefreshEntry(mydrive);
48 if (error != FILE_ERROR_OK)
49 return error;
50 }
51
52 // Get entry.
53 error = resource_metadata->GetResourceEntryById(local_id, entry);
54 if (error != FILE_ERROR_OK)
55 return error;
56
57 // Get the local changestamp.
58 return resource_metadata->GetLargestChangestamp(local_changestamp);
59 }
60
UpdateChangestamp(ResourceMetadata * resource_metadata,const DirectoryFetchInfo & directory_fetch_info,base::FilePath * directory_path)61 FileError UpdateChangestamp(ResourceMetadata* resource_metadata,
62 const DirectoryFetchInfo& directory_fetch_info,
63 base::FilePath* directory_path) {
64 // Update the directory changestamp.
65 ResourceEntry directory;
66 FileError error = resource_metadata->GetResourceEntryById(
67 directory_fetch_info.local_id(), &directory);
68 if (error != FILE_ERROR_OK)
69 return error;
70
71 if (!directory.file_info().is_directory())
72 return FILE_ERROR_NOT_A_DIRECTORY;
73
74 directory.mutable_directory_specific_info()->set_changestamp(
75 directory_fetch_info.changestamp());
76 error = resource_metadata->RefreshEntry(directory);
77 if (error != FILE_ERROR_OK)
78 return error;
79
80 // Get the directory path.
81 return resource_metadata->GetFilePath(directory_fetch_info.local_id(),
82 directory_path);
83 }
84
85 } // namespace
86
87 struct DirectoryLoader::ReadDirectoryCallbackState {
88 ReadDirectoryEntriesCallback entries_callback;
89 FileOperationCallback completion_callback;
90 std::set<std::string> sent_entry_names;
91 };
92
93 // Fetches the resource entries in the directory with |directory_resource_id|.
94 class DirectoryLoader::FeedFetcher {
95 public:
FeedFetcher(DirectoryLoader * loader,const DirectoryFetchInfo & directory_fetch_info,const std::string & root_folder_id)96 FeedFetcher(DirectoryLoader* loader,
97 const DirectoryFetchInfo& directory_fetch_info,
98 const std::string& root_folder_id)
99 : loader_(loader),
100 directory_fetch_info_(directory_fetch_info),
101 root_folder_id_(root_folder_id),
102 weak_ptr_factory_(this) {
103 }
104
~FeedFetcher()105 ~FeedFetcher() {
106 }
107
Run(const FileOperationCallback & callback)108 void Run(const FileOperationCallback& callback) {
109 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
110 DCHECK(!callback.is_null());
111 DCHECK(!directory_fetch_info_.resource_id().empty());
112
113 // Remember the time stamp for usage stats.
114 start_time_ = base::TimeTicks::Now();
115
116 loader_->scheduler_->GetFileListInDirectory(
117 directory_fetch_info_.resource_id(),
118 base::Bind(&FeedFetcher::OnFileListFetched,
119 weak_ptr_factory_.GetWeakPtr(), callback));
120 }
121
122 private:
OnFileListFetched(const FileOperationCallback & callback,google_apis::GDataErrorCode status,scoped_ptr<google_apis::FileList> file_list)123 void OnFileListFetched(const FileOperationCallback& callback,
124 google_apis::GDataErrorCode status,
125 scoped_ptr<google_apis::FileList> file_list) {
126 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
127 DCHECK(!callback.is_null());
128
129 FileError error = GDataToFileError(status);
130 if (error != FILE_ERROR_OK) {
131 callback.Run(error);
132 return;
133 }
134
135 DCHECK(file_list);
136 scoped_ptr<ChangeList> change_list(new ChangeList(*file_list));
137 GURL next_url = file_list->next_link();
138
139 ResourceEntryVector* entries = new ResourceEntryVector;
140 loader_->loader_controller_->ScheduleRun(base::Bind(
141 base::IgnoreResult(
142 &base::PostTaskAndReplyWithResult<FileError, FileError>),
143 loader_->blocking_task_runner_,
144 FROM_HERE,
145 base::Bind(&ChangeListProcessor::RefreshDirectory,
146 loader_->resource_metadata_,
147 directory_fetch_info_,
148 base::Passed(&change_list),
149 entries),
150 base::Bind(&FeedFetcher::OnDirectoryRefreshed,
151 weak_ptr_factory_.GetWeakPtr(),
152 callback,
153 next_url,
154 base::Owned(entries))));
155 }
156
OnDirectoryRefreshed(const FileOperationCallback & callback,const GURL & next_url,const std::vector<ResourceEntry> * refreshed_entries,FileError error)157 void OnDirectoryRefreshed(
158 const FileOperationCallback& callback,
159 const GURL& next_url,
160 const std::vector<ResourceEntry>* refreshed_entries,
161 FileError error) {
162 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
163 DCHECK(!callback.is_null());
164
165 if (error != FILE_ERROR_OK) {
166 callback.Run(error);
167 return;
168 }
169
170 loader_->SendEntries(directory_fetch_info_.local_id(), *refreshed_entries);
171
172 if (!next_url.is_empty()) {
173 // There is the remaining result so fetch it.
174 loader_->scheduler_->GetRemainingFileList(
175 next_url,
176 base::Bind(&FeedFetcher::OnFileListFetched,
177 weak_ptr_factory_.GetWeakPtr(), callback));
178 return;
179 }
180
181 UMA_HISTOGRAM_TIMES("Drive.DirectoryFeedLoadTime",
182 base::TimeTicks::Now() - start_time_);
183
184 // Note: The fetcher is managed by DirectoryLoader, and the instance
185 // will be deleted in the callback. Do not touch the fields after this
186 // invocation.
187 callback.Run(FILE_ERROR_OK);
188 }
189
190 DirectoryLoader* loader_;
191 DirectoryFetchInfo directory_fetch_info_;
192 std::string root_folder_id_;
193 base::TimeTicks start_time_;
194 base::WeakPtrFactory<FeedFetcher> weak_ptr_factory_;
195 DISALLOW_COPY_AND_ASSIGN(FeedFetcher);
196 };
197
DirectoryLoader(EventLogger * logger,base::SequencedTaskRunner * blocking_task_runner,ResourceMetadata * resource_metadata,JobScheduler * scheduler,AboutResourceLoader * about_resource_loader,LoaderController * loader_controller)198 DirectoryLoader::DirectoryLoader(
199 EventLogger* logger,
200 base::SequencedTaskRunner* blocking_task_runner,
201 ResourceMetadata* resource_metadata,
202 JobScheduler* scheduler,
203 AboutResourceLoader* about_resource_loader,
204 LoaderController* loader_controller)
205 : logger_(logger),
206 blocking_task_runner_(blocking_task_runner),
207 resource_metadata_(resource_metadata),
208 scheduler_(scheduler),
209 about_resource_loader_(about_resource_loader),
210 loader_controller_(loader_controller),
211 weak_ptr_factory_(this) {
212 }
213
~DirectoryLoader()214 DirectoryLoader::~DirectoryLoader() {
215 STLDeleteElements(&fast_fetch_feed_fetcher_set_);
216 }
217
AddObserver(ChangeListLoaderObserver * observer)218 void DirectoryLoader::AddObserver(ChangeListLoaderObserver* observer) {
219 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
220 observers_.AddObserver(observer);
221 }
222
RemoveObserver(ChangeListLoaderObserver * observer)223 void DirectoryLoader::RemoveObserver(ChangeListLoaderObserver* observer) {
224 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
225 observers_.RemoveObserver(observer);
226 }
227
ReadDirectory(const base::FilePath & directory_path,const ReadDirectoryEntriesCallback & entries_callback,const FileOperationCallback & completion_callback)228 void DirectoryLoader::ReadDirectory(
229 const base::FilePath& directory_path,
230 const ReadDirectoryEntriesCallback& entries_callback,
231 const FileOperationCallback& completion_callback) {
232 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
233 DCHECK(!completion_callback.is_null());
234
235 ResourceEntry* entry = new ResourceEntry;
236 base::PostTaskAndReplyWithResult(
237 blocking_task_runner_.get(),
238 FROM_HERE,
239 base::Bind(&ResourceMetadata::GetResourceEntryByPath,
240 base::Unretained(resource_metadata_),
241 directory_path,
242 entry),
243 base::Bind(&DirectoryLoader::ReadDirectoryAfterGetEntry,
244 weak_ptr_factory_.GetWeakPtr(),
245 directory_path,
246 entries_callback,
247 completion_callback,
248 true, // should_try_loading_parent
249 base::Owned(entry)));
250 }
251
ReadDirectoryAfterGetEntry(const base::FilePath & directory_path,const ReadDirectoryEntriesCallback & entries_callback,const FileOperationCallback & completion_callback,bool should_try_loading_parent,const ResourceEntry * entry,FileError error)252 void DirectoryLoader::ReadDirectoryAfterGetEntry(
253 const base::FilePath& directory_path,
254 const ReadDirectoryEntriesCallback& entries_callback,
255 const FileOperationCallback& completion_callback,
256 bool should_try_loading_parent,
257 const ResourceEntry* entry,
258 FileError error) {
259 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
260 DCHECK(!completion_callback.is_null());
261
262 if (error == FILE_ERROR_NOT_FOUND &&
263 should_try_loading_parent &&
264 util::GetDriveGrandRootPath().IsParent(directory_path)) {
265 // This entry may be found after loading the parent.
266 ReadDirectory(directory_path.DirName(),
267 ReadDirectoryEntriesCallback(),
268 base::Bind(&DirectoryLoader::ReadDirectoryAfterLoadParent,
269 weak_ptr_factory_.GetWeakPtr(),
270 directory_path,
271 entries_callback,
272 completion_callback));
273 return;
274 }
275 if (error != FILE_ERROR_OK) {
276 completion_callback.Run(error);
277 return;
278 }
279
280 if (!entry->file_info().is_directory()) {
281 completion_callback.Run(FILE_ERROR_NOT_A_DIRECTORY);
282 return;
283 }
284
285 DirectoryFetchInfo directory_fetch_info(
286 entry->local_id(),
287 entry->resource_id(),
288 entry->directory_specific_info().changestamp());
289
290 // Register the callback function to be called when it is loaded.
291 const std::string& local_id = directory_fetch_info.local_id();
292 ReadDirectoryCallbackState callback_state;
293 callback_state.entries_callback = entries_callback;
294 callback_state.completion_callback = completion_callback;
295 pending_load_callback_[local_id].push_back(callback_state);
296
297 // If loading task for |local_id| is already running, do nothing.
298 if (pending_load_callback_[local_id].size() > 1)
299 return;
300
301 // Note: To be precise, we need to call UpdateAboutResource() here. However,
302 // - It is costly to do GetAboutResource HTTP request every time.
303 // - The chance using an old value is small; it only happens when
304 // ReadDirectory is called during one GetAboutResource roundtrip time
305 // of a change list fetching.
306 // - Even if the value is old, it just marks the directory as older. It may
307 // trigger one future unnecessary re-fetch, but it'll never lose data.
308 about_resource_loader_->GetAboutResource(
309 base::Bind(&DirectoryLoader::ReadDirectoryAfterGetAboutResource,
310 weak_ptr_factory_.GetWeakPtr(), local_id));
311 }
312
ReadDirectoryAfterLoadParent(const base::FilePath & directory_path,const ReadDirectoryEntriesCallback & entries_callback,const FileOperationCallback & completion_callback,FileError error)313 void DirectoryLoader::ReadDirectoryAfterLoadParent(
314 const base::FilePath& directory_path,
315 const ReadDirectoryEntriesCallback& entries_callback,
316 const FileOperationCallback& completion_callback,
317 FileError error) {
318 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
319 DCHECK(!completion_callback.is_null());
320
321 if (error != FILE_ERROR_OK) {
322 completion_callback.Run(error);
323 return;
324 }
325
326 ResourceEntry* entry = new ResourceEntry;
327 base::PostTaskAndReplyWithResult(
328 blocking_task_runner_.get(),
329 FROM_HERE,
330 base::Bind(&ResourceMetadata::GetResourceEntryByPath,
331 base::Unretained(resource_metadata_),
332 directory_path,
333 entry),
334 base::Bind(&DirectoryLoader::ReadDirectoryAfterGetEntry,
335 weak_ptr_factory_.GetWeakPtr(),
336 directory_path,
337 entries_callback,
338 completion_callback,
339 false, // should_try_loading_parent
340 base::Owned(entry)));
341 }
342
ReadDirectoryAfterGetAboutResource(const std::string & local_id,google_apis::GDataErrorCode status,scoped_ptr<google_apis::AboutResource> about_resource)343 void DirectoryLoader::ReadDirectoryAfterGetAboutResource(
344 const std::string& local_id,
345 google_apis::GDataErrorCode status,
346 scoped_ptr<google_apis::AboutResource> about_resource) {
347 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
348
349 FileError error = GDataToFileError(status);
350 if (error != FILE_ERROR_OK) {
351 OnDirectoryLoadComplete(local_id, error);
352 return;
353 }
354
355 DCHECK(about_resource);
356
357 // Check the current status of local metadata, and start loading if needed.
358 google_apis::AboutResource* about_resource_ptr = about_resource.get();
359 ResourceEntry* entry = new ResourceEntry;
360 int64* local_changestamp = new int64;
361 base::PostTaskAndReplyWithResult(
362 blocking_task_runner_,
363 FROM_HERE,
364 base::Bind(&CheckLocalState,
365 resource_metadata_,
366 *about_resource_ptr,
367 local_id,
368 entry,
369 local_changestamp),
370 base::Bind(&DirectoryLoader::ReadDirectoryAfterCheckLocalState,
371 weak_ptr_factory_.GetWeakPtr(),
372 base::Passed(&about_resource),
373 local_id,
374 base::Owned(entry),
375 base::Owned(local_changestamp)));
376 }
377
ReadDirectoryAfterCheckLocalState(scoped_ptr<google_apis::AboutResource> about_resource,const std::string & local_id,const ResourceEntry * entry,const int64 * local_changestamp,FileError error)378 void DirectoryLoader::ReadDirectoryAfterCheckLocalState(
379 scoped_ptr<google_apis::AboutResource> about_resource,
380 const std::string& local_id,
381 const ResourceEntry* entry,
382 const int64* local_changestamp,
383 FileError error) {
384 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
385 DCHECK(about_resource);
386
387 if (error != FILE_ERROR_OK) {
388 OnDirectoryLoadComplete(local_id, error);
389 return;
390 }
391 // This entry does not exist on the server.
392 if (entry->resource_id().empty()) {
393 OnDirectoryLoadComplete(local_id, FILE_ERROR_OK);
394 return;
395 }
396
397 int64 remote_changestamp = about_resource->largest_change_id();
398
399 // Start loading the directory.
400 int64 directory_changestamp = std::max(
401 entry->directory_specific_info().changestamp(), *local_changestamp);
402
403 DirectoryFetchInfo directory_fetch_info(
404 local_id, entry->resource_id(), remote_changestamp);
405
406 // If the directory's changestamp is new enough, just schedule to run the
407 // callback, as there is no need to fetch the directory.
408 if (directory_changestamp + kMinimumChangestampGap > remote_changestamp) {
409 OnDirectoryLoadComplete(local_id, FILE_ERROR_OK);
410 } else {
411 // Start fetching the directory content, and mark it with the changestamp
412 // |remote_changestamp|.
413 LoadDirectoryFromServer(directory_fetch_info);
414 }
415 }
416
OnDirectoryLoadComplete(const std::string & local_id,FileError error)417 void DirectoryLoader::OnDirectoryLoadComplete(const std::string& local_id,
418 FileError error) {
419 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
420
421 LoadCallbackMap::iterator it = pending_load_callback_.find(local_id);
422 if (it == pending_load_callback_.end())
423 return;
424
425 // No need to read metadata when no one needs entries.
426 bool needs_to_send_entries = false;
427 for (size_t i = 0; i < it->second.size(); ++i) {
428 const ReadDirectoryCallbackState& callback_state = it->second[i];
429 if (!callback_state.entries_callback.is_null())
430 needs_to_send_entries = true;
431 }
432
433 if (!needs_to_send_entries) {
434 OnDirectoryLoadCompleteAfterRead(local_id, NULL, FILE_ERROR_OK);
435 return;
436 }
437
438 ResourceEntryVector* entries = new ResourceEntryVector;
439 base::PostTaskAndReplyWithResult(
440 blocking_task_runner_.get(),
441 FROM_HERE,
442 base::Bind(&ResourceMetadata::ReadDirectoryById,
443 base::Unretained(resource_metadata_), local_id, entries),
444 base::Bind(&DirectoryLoader::OnDirectoryLoadCompleteAfterRead,
445 weak_ptr_factory_.GetWeakPtr(),
446 local_id,
447 base::Owned(entries)));
448 }
449
OnDirectoryLoadCompleteAfterRead(const std::string & local_id,const ResourceEntryVector * entries,FileError error)450 void DirectoryLoader::OnDirectoryLoadCompleteAfterRead(
451 const std::string& local_id,
452 const ResourceEntryVector* entries,
453 FileError error) {
454 LoadCallbackMap::iterator it = pending_load_callback_.find(local_id);
455 if (it != pending_load_callback_.end()) {
456 DVLOG(1) << "Running callback for " << local_id;
457
458 if (error == FILE_ERROR_OK && entries)
459 SendEntries(local_id, *entries);
460
461 for (size_t i = 0; i < it->second.size(); ++i) {
462 const ReadDirectoryCallbackState& callback_state = it->second[i];
463 callback_state.completion_callback.Run(error);
464 }
465 pending_load_callback_.erase(it);
466 }
467 }
468
SendEntries(const std::string & local_id,const ResourceEntryVector & entries)469 void DirectoryLoader::SendEntries(const std::string& local_id,
470 const ResourceEntryVector& entries) {
471 LoadCallbackMap::iterator it = pending_load_callback_.find(local_id);
472 DCHECK(it != pending_load_callback_.end());
473
474 for (size_t i = 0; i < it->second.size(); ++i) {
475 ReadDirectoryCallbackState* callback_state = &it->second[i];
476 if (callback_state->entries_callback.is_null())
477 continue;
478
479 // Filter out entries which were already sent.
480 scoped_ptr<ResourceEntryVector> entries_to_send(new ResourceEntryVector);
481 for (size_t i = 0; i < entries.size(); ++i) {
482 const ResourceEntry& entry = entries[i];
483 if (!callback_state->sent_entry_names.count(entry.base_name())) {
484 callback_state->sent_entry_names.insert(entry.base_name());
485 entries_to_send->push_back(entry);
486 }
487 }
488 callback_state->entries_callback.Run(entries_to_send.Pass());
489 }
490 }
491
LoadDirectoryFromServer(const DirectoryFetchInfo & directory_fetch_info)492 void DirectoryLoader::LoadDirectoryFromServer(
493 const DirectoryFetchInfo& directory_fetch_info) {
494 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
495 DCHECK(!directory_fetch_info.empty());
496 DVLOG(1) << "Start loading directory: " << directory_fetch_info.ToString();
497
498 const google_apis::AboutResource* about_resource =
499 about_resource_loader_->cached_about_resource();
500 DCHECK(about_resource);
501
502 logger_->Log(logging::LOG_INFO,
503 "Fast-fetch start: %s; Server changestamp: %s",
504 directory_fetch_info.ToString().c_str(),
505 base::Int64ToString(
506 about_resource->largest_change_id()).c_str());
507
508 FeedFetcher* fetcher = new FeedFetcher(this,
509 directory_fetch_info,
510 about_resource->root_folder_id());
511 fast_fetch_feed_fetcher_set_.insert(fetcher);
512 fetcher->Run(
513 base::Bind(&DirectoryLoader::LoadDirectoryFromServerAfterLoad,
514 weak_ptr_factory_.GetWeakPtr(),
515 directory_fetch_info,
516 fetcher));
517 }
518
LoadDirectoryFromServerAfterLoad(const DirectoryFetchInfo & directory_fetch_info,FeedFetcher * fetcher,FileError error)519 void DirectoryLoader::LoadDirectoryFromServerAfterLoad(
520 const DirectoryFetchInfo& directory_fetch_info,
521 FeedFetcher* fetcher,
522 FileError error) {
523 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
524 DCHECK(!directory_fetch_info.empty());
525
526 // Delete the fetcher.
527 fast_fetch_feed_fetcher_set_.erase(fetcher);
528 delete fetcher;
529
530 logger_->Log(logging::LOG_INFO,
531 "Fast-fetch complete: %s => %s",
532 directory_fetch_info.ToString().c_str(),
533 FileErrorToString(error).c_str());
534
535 if (error != FILE_ERROR_OK) {
536 LOG(ERROR) << "Failed to load directory: "
537 << directory_fetch_info.local_id()
538 << ": " << FileErrorToString(error);
539 OnDirectoryLoadComplete(directory_fetch_info.local_id(), error);
540 return;
541 }
542
543 // Update changestamp and get the directory path.
544 base::FilePath* directory_path = new base::FilePath;
545 base::PostTaskAndReplyWithResult(
546 blocking_task_runner_.get(),
547 FROM_HERE,
548 base::Bind(&UpdateChangestamp,
549 resource_metadata_,
550 directory_fetch_info,
551 directory_path),
552 base::Bind(
553 &DirectoryLoader::LoadDirectoryFromServerAfterUpdateChangestamp,
554 weak_ptr_factory_.GetWeakPtr(),
555 directory_fetch_info,
556 base::Owned(directory_path)));
557 }
558
LoadDirectoryFromServerAfterUpdateChangestamp(const DirectoryFetchInfo & directory_fetch_info,const base::FilePath * directory_path,FileError error)559 void DirectoryLoader::LoadDirectoryFromServerAfterUpdateChangestamp(
560 const DirectoryFetchInfo& directory_fetch_info,
561 const base::FilePath* directory_path,
562 FileError error) {
563 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
564
565 DVLOG(1) << "Directory loaded: " << directory_fetch_info.ToString();
566 OnDirectoryLoadComplete(directory_fetch_info.local_id(), error);
567
568 // Also notify the observers.
569 if (error == FILE_ERROR_OK && !directory_path->empty()) {
570 FOR_EACH_OBSERVER(ChangeListLoaderObserver, observers_,
571 OnDirectoryChanged(*directory_path));
572 }
573 }
574
575 } // namespace internal
576 } // namespace drive
577