Home | History | Annotate | Download | only in drive
      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
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/drive/fake_drive_service.h"
      6 
      7 #include <string>
      8 
      9 #include "base/file_util.h"
     10 #include "base/logging.h"
     11 #include "base/md5.h"
     12 #include "base/message_loop/message_loop.h"
     13 #include "base/strings/string_number_conversions.h"
     14 #include "base/strings/string_split.h"
     15 #include "base/strings/string_tokenizer.h"
     16 #include "base/strings/string_util.h"
     17 #include "base/strings/stringprintf.h"
     18 #include "base/strings/utf_string_conversions.h"
     19 #include "chrome/browser/google_apis/drive_api_parser.h"
     20 #include "chrome/browser/google_apis/gdata_wapi_parser.h"
     21 #include "chrome/browser/google_apis/test_util.h"
     22 #include "chrome/browser/google_apis/time_util.h"
     23 #include "content/public/browser/browser_thread.h"
     24 #include "net/base/escape.h"
     25 #include "net/base/url_util.h"
     26 
     27 using content::BrowserThread;
     28 using google_apis::AboutResource;
     29 using google_apis::AccountMetadata;
     30 using google_apis::AppList;
     31 using google_apis::AuthStatusCallback;
     32 using google_apis::AuthorizeAppCallback;
     33 using google_apis::CancelCallback;
     34 using google_apis::DownloadActionCallback;
     35 using google_apis::EntryActionCallback;
     36 using google_apis::GDataErrorCode;
     37 using google_apis::GDATA_FILE_ERROR;
     38 using google_apis::GDATA_NO_CONNECTION;
     39 using google_apis::GDATA_OTHER_ERROR;
     40 using google_apis::GetAboutResourceCallback;
     41 using google_apis::GetAppListCallback;
     42 using google_apis::GetContentCallback;
     43 using google_apis::GetResourceEntryCallback;
     44 using google_apis::GetResourceListCallback;
     45 using google_apis::GetShareUrlCallback;
     46 using google_apis::HTTP_BAD_REQUEST;
     47 using google_apis::HTTP_CREATED;
     48 using google_apis::HTTP_NOT_FOUND;
     49 using google_apis::HTTP_PRECONDITION;
     50 using google_apis::HTTP_RESUME_INCOMPLETE;
     51 using google_apis::HTTP_SUCCESS;
     52 using google_apis::InitiateUploadCallback;
     53 using google_apis::Link;
     54 using google_apis::ProgressCallback;
     55 using google_apis::ResourceEntry;
     56 using google_apis::ResourceList;
     57 using google_apis::UploadRangeCallback;
     58 using google_apis::UploadRangeResponse;
     59 namespace test_util = google_apis::test_util;
     60 namespace util = google_apis::util;
     61 
     62 namespace drive {
     63 namespace {
     64 
     65 // Rel property of an upload link in the entries dictionary value.
     66 const char kUploadUrlRel[] =
     67     "http://schemas.google.com/g/2005#resumable-create-media";
     68 
     69 // Rel property of a share link in the entries dictionary value.
     70 const char kShareUrlRel[] =
     71     "http://schemas.google.com/docs/2007#share";
     72 
     73 // Returns true if a resource entry matches with the search query.
     74 // Supports queries consist of following format.
     75 // - Phrases quoted by double/single quotes
     76 // - AND search for multiple words/phrases segmented by space
     77 // - Limited attribute search.  Only "title:" is supported.
     78 bool EntryMatchWithQuery(const ResourceEntry& entry,
     79                          const std::string& query) {
     80   base::StringTokenizer tokenizer(query, " ");
     81   tokenizer.set_quote_chars("\"'");
     82   while (tokenizer.GetNext()) {
     83     std::string key, value;
     84     const std::string& token = tokenizer.token();
     85     if (token.find(':') == std::string::npos) {
     86       TrimString(token, "\"'", &value);
     87     } else {
     88       base::StringTokenizer key_value(token, ":");
     89       key_value.set_quote_chars("\"'");
     90       if (!key_value.GetNext())
     91         return false;
     92       key = key_value.token();
     93       if (!key_value.GetNext())
     94         return false;
     95       TrimString(key_value.token(), "\"'", &value);
     96     }
     97 
     98     // TODO(peria): Deal with other attributes than title.
     99     if (!key.empty() && key != "title")
    100       return false;
    101     // Search query in the title.
    102     if (entry.title().find(value) == std::string::npos)
    103       return false;
    104   }
    105   return true;
    106 }
    107 
    108 // Returns |url| without query parameter.
    109 GURL RemoveQueryParameter(const GURL& url) {
    110   GURL::Replacements replacements;
    111   replacements.ClearQuery();
    112   return url.ReplaceComponents(replacements);
    113 }
    114 
    115 void ScheduleUploadRangeCallback(const UploadRangeCallback& callback,
    116                                  int64 start_position,
    117                                  int64 end_position,
    118                                  GDataErrorCode error,
    119                                  scoped_ptr<ResourceEntry> entry) {
    120   base::MessageLoop::current()->PostTask(
    121       FROM_HERE,
    122       base::Bind(callback,
    123                  UploadRangeResponse(error,
    124                                      start_position,
    125                                      end_position),
    126                  base::Passed(&entry)));
    127 }
    128 
    129 }  // namespace
    130 
    131 struct FakeDriveService::UploadSession {
    132   std::string content_type;
    133   int64 content_length;
    134   std::string parent_resource_id;
    135   std::string resource_id;
    136   std::string etag;
    137   std::string title;
    138 
    139   int64 uploaded_size;
    140 
    141   UploadSession()
    142       : content_length(0),
    143         uploaded_size(0) {}
    144 
    145   UploadSession(
    146       std::string content_type,
    147       int64 content_length,
    148       std::string parent_resource_id,
    149       std::string resource_id,
    150       std::string etag,
    151       std::string title)
    152     : content_type(content_type),
    153       content_length(content_length),
    154       parent_resource_id(parent_resource_id),
    155       resource_id(resource_id),
    156       etag(etag),
    157       title(title),
    158       uploaded_size(0) {
    159   }
    160 };
    161 
    162 FakeDriveService::FakeDriveService()
    163     : largest_changestamp_(0),
    164       published_date_seq_(0),
    165       next_upload_sequence_number_(0),
    166       default_max_results_(0),
    167       resource_id_count_(0),
    168       resource_list_load_count_(0),
    169       change_list_load_count_(0),
    170       directory_load_count_(0),
    171       about_resource_load_count_(0),
    172       app_list_load_count_(0),
    173       offline_(false) {
    174   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    175 }
    176 
    177 FakeDriveService::~FakeDriveService() {
    178   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    179 }
    180 
    181 bool FakeDriveService::LoadResourceListForWapi(
    182     const std::string& relative_path) {
    183   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    184   scoped_ptr<Value> raw_value = test_util::LoadJSONFile(relative_path);
    185   base::DictionaryValue* as_dict = NULL;
    186   scoped_ptr<base::Value> feed;
    187   base::DictionaryValue* feed_as_dict = NULL;
    188 
    189   // Extract the "feed" from the raw value and take the ownership.
    190   // Note that Remove() transfers the ownership to |feed|.
    191   if (raw_value->GetAsDictionary(&as_dict) &&
    192       as_dict->Remove("feed", &feed) &&
    193       feed->GetAsDictionary(&feed_as_dict)) {
    194     ignore_result(feed.release());
    195     resource_list_value_.reset(feed_as_dict);
    196 
    197     // Go through entries and convert test$data from a string to a binary blob.
    198     base::ListValue* entries = NULL;
    199     if (feed_as_dict->GetList("entry", &entries)) {
    200       for (size_t i = 0; i < entries->GetSize(); ++i) {
    201         base::DictionaryValue* entry = NULL;
    202         std::string content_data;
    203         if (entries->GetDictionary(i, &entry) &&
    204             entry->GetString("test$data", &content_data)) {
    205           entry->Set("test$data",
    206                      base::BinaryValue::CreateWithCopiedBuffer(
    207                          content_data.c_str(), content_data.size()));
    208         }
    209       }
    210     }
    211   }
    212 
    213   return resource_list_value_;
    214 }
    215 
    216 bool FakeDriveService::LoadAccountMetadataForWapi(
    217     const std::string& relative_path) {
    218   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    219 
    220   // Load JSON data, which must be a dictionary.
    221   scoped_ptr<base::Value> value = test_util::LoadJSONFile(relative_path);
    222   CHECK_EQ(base::Value::TYPE_DICTIONARY, value->GetType());
    223   account_metadata_value_.reset(
    224       static_cast<base::DictionaryValue*>(value.release()));
    225 
    226   // Update the largest_changestamp_.
    227   scoped_ptr<AccountMetadata> account_metadata =
    228       AccountMetadata::CreateFrom(*account_metadata_value_);
    229   largest_changestamp_ = account_metadata->largest_changestamp();
    230 
    231   // Add the largest changestamp to the existing entries.
    232   // This will be used to generate change lists in GetResourceList().
    233   if (resource_list_value_) {
    234     base::ListValue* entries = NULL;
    235     if (resource_list_value_->GetList("entry", &entries)) {
    236       for (size_t i = 0; i < entries->GetSize(); ++i) {
    237         base::DictionaryValue* entry = NULL;
    238         if (entries->GetDictionary(i, &entry)) {
    239           entry->SetString("docs$changestamp.value",
    240                            base::Int64ToString(largest_changestamp_));
    241         }
    242       }
    243     }
    244   }
    245 
    246   return account_metadata_value_;
    247 }
    248 
    249 bool FakeDriveService::LoadAppListForDriveApi(
    250     const std::string& relative_path) {
    251   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    252   app_info_value_ = test_util::LoadJSONFile(relative_path);
    253   return app_info_value_;
    254 }
    255 
    256 void FakeDriveService::SetQuotaValue(int64 used, int64 total) {
    257   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    258   DCHECK(account_metadata_value_);
    259 
    260   account_metadata_value_->SetString("entry.gd$quotaBytesUsed.$t",
    261                                      base::Int64ToString16(used));
    262   account_metadata_value_->SetString("entry.gd$quotaBytesTotal.$t",
    263                                      base::Int64ToString16(total));
    264 }
    265 
    266 GURL FakeDriveService::GetFakeLinkUrl(const std::string& resource_id) {
    267   return GURL("https://fake_server/" + net::EscapePath(resource_id));
    268 }
    269 
    270 void FakeDriveService::Initialize() {
    271   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    272 }
    273 
    274 void FakeDriveService::AddObserver(DriveServiceObserver* observer) {
    275   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    276 }
    277 
    278 void FakeDriveService::RemoveObserver(DriveServiceObserver* observer) {
    279   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    280 }
    281 
    282 bool FakeDriveService::CanSendRequest() const {
    283   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    284   return true;
    285 }
    286 
    287 std::string FakeDriveService::CanonicalizeResourceId(
    288     const std::string& resource_id) const {
    289   return resource_id;
    290 }
    291 
    292 bool FakeDriveService::HasAccessToken() const {
    293   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    294   return true;
    295 }
    296 
    297 void FakeDriveService::RequestAccessToken(const AuthStatusCallback& callback) {
    298   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    299   DCHECK(!callback.is_null());
    300   callback.Run(google_apis::HTTP_NOT_MODIFIED, "fake_access_token");
    301 }
    302 
    303 bool FakeDriveService::HasRefreshToken() const {
    304   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    305   return true;
    306 }
    307 
    308 void FakeDriveService::ClearAccessToken() {
    309   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    310 }
    311 
    312 void FakeDriveService::ClearRefreshToken() {
    313   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    314 }
    315 
    316 std::string FakeDriveService::GetRootResourceId() const {
    317   return "fake_root";
    318 }
    319 
    320 CancelCallback FakeDriveService::GetAllResourceList(
    321     const GetResourceListCallback& callback) {
    322   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    323   DCHECK(!callback.is_null());
    324 
    325   GetResourceListInternal(0,  // start changestamp
    326                           std::string(),  // empty search query
    327                           std::string(),  // no directory resource id,
    328                           0,  // start offset
    329                           default_max_results_,
    330                           &resource_list_load_count_,
    331                           callback);
    332   return CancelCallback();
    333 }
    334 
    335 CancelCallback FakeDriveService::GetResourceListInDirectory(
    336     const std::string& directory_resource_id,
    337     const GetResourceListCallback& callback) {
    338   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    339   DCHECK(!directory_resource_id.empty());
    340   DCHECK(!callback.is_null());
    341 
    342   GetResourceListInternal(0,  // start changestamp
    343                           std::string(),  // empty search query
    344                           directory_resource_id,
    345                           0,  // start offset
    346                           default_max_results_,
    347                           &directory_load_count_,
    348                           callback);
    349   return CancelCallback();
    350 }
    351 
    352 CancelCallback FakeDriveService::Search(
    353     const std::string& search_query,
    354     const GetResourceListCallback& callback) {
    355   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    356   DCHECK(!search_query.empty());
    357   DCHECK(!callback.is_null());
    358 
    359   GetResourceListInternal(0,  // start changestamp
    360                           search_query,
    361                           std::string(),  // no directory resource id,
    362                           0,  // start offset
    363                           default_max_results_,
    364                           NULL,
    365                           callback);
    366   return CancelCallback();
    367 }
    368 
    369 CancelCallback FakeDriveService::SearchByTitle(
    370     const std::string& title,
    371     const std::string& directory_resource_id,
    372     const GetResourceListCallback& callback) {
    373   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    374   DCHECK(!title.empty());
    375   DCHECK(!callback.is_null());
    376 
    377   // Note: the search implementation here doesn't support quotation unescape,
    378   // so don't escape here.
    379   GetResourceListInternal(0,  // start changestamp
    380                           base::StringPrintf("title:'%s'", title.c_str()),
    381                           directory_resource_id,
    382                           0,  // start offset
    383                           default_max_results_,
    384                           NULL,
    385                           callback);
    386   return CancelCallback();
    387 }
    388 
    389 CancelCallback FakeDriveService::GetChangeList(
    390     int64 start_changestamp,
    391     const GetResourceListCallback& callback) {
    392   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    393   DCHECK(!callback.is_null());
    394 
    395   GetResourceListInternal(start_changestamp,
    396                           std::string(),  // empty search query
    397                           std::string(),  // no directory resource id,
    398                           0,  // start offset
    399                           default_max_results_,
    400                           &change_list_load_count_,
    401                           callback);
    402   return CancelCallback();
    403 }
    404 
    405 CancelCallback FakeDriveService::ContinueGetResourceList(
    406     const GURL& override_url,
    407     const GetResourceListCallback& callback) {
    408   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    409   DCHECK(!override_url.is_empty());
    410   DCHECK(!callback.is_null());
    411 
    412   // "changestamp", "q", "parent" and "start-offset" are parameters to
    413   // implement "paging" of the result on FakeDriveService.
    414   // The URL should be the one filled in GetResourceListInternal of the
    415   // previous method invocation, so it should start with "http://localhost/?".
    416   // See also GetResourceListInternal.
    417   DCHECK_EQ(override_url.host(), "localhost");
    418   DCHECK_EQ(override_url.path(), "/");
    419 
    420   int64 start_changestamp = 0;
    421   std::string search_query;
    422   std::string directory_resource_id;
    423   int start_offset = 0;
    424   int max_results = default_max_results_;
    425   std::vector<std::pair<std::string, std::string> > parameters;
    426   if (base::SplitStringIntoKeyValuePairs(
    427           override_url.query(), '=', '&', &parameters)) {
    428     for (size_t i = 0; i < parameters.size(); ++i) {
    429       if (parameters[i].first == "changestamp") {
    430         base::StringToInt64(parameters[i].second, &start_changestamp);
    431       } else if (parameters[i].first == "q") {
    432         search_query =
    433             net::UnescapeURLComponent(parameters[i].second,
    434                                       net::UnescapeRule::URL_SPECIAL_CHARS);
    435       } else if (parameters[i].first == "parent") {
    436         directory_resource_id =
    437             net::UnescapeURLComponent(parameters[i].second,
    438                                       net::UnescapeRule::URL_SPECIAL_CHARS);
    439       } else if (parameters[i].first == "start-offset") {
    440         base::StringToInt(parameters[i].second, &start_offset);
    441       } else if (parameters[i].first == "max-results") {
    442         base::StringToInt(parameters[i].second, &max_results);
    443       }
    444     }
    445   }
    446 
    447   GetResourceListInternal(
    448       start_changestamp, search_query, directory_resource_id,
    449       start_offset, max_results, NULL, callback);
    450   return CancelCallback();
    451 }
    452 
    453 CancelCallback FakeDriveService::GetResourceEntry(
    454     const std::string& resource_id,
    455     const GetResourceEntryCallback& callback) {
    456   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    457   DCHECK(!callback.is_null());
    458 
    459   if (offline_) {
    460     scoped_ptr<ResourceEntry> null;
    461     base::MessageLoop::current()->PostTask(
    462         FROM_HERE,
    463         base::Bind(callback,
    464                    GDATA_NO_CONNECTION,
    465                    base::Passed(&null)));
    466     return CancelCallback();
    467   }
    468 
    469   base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
    470   if (entry) {
    471     scoped_ptr<ResourceEntry> resource_entry =
    472         ResourceEntry::CreateFrom(*entry);
    473     base::MessageLoop::current()->PostTask(
    474         FROM_HERE,
    475         base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_entry)));
    476     return CancelCallback();
    477   }
    478 
    479   scoped_ptr<ResourceEntry> null;
    480   base::MessageLoop::current()->PostTask(
    481       FROM_HERE,
    482       base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
    483   return CancelCallback();
    484 }
    485 
    486 CancelCallback FakeDriveService::GetShareUrl(
    487     const std::string& resource_id,
    488     const GURL& /* embed_origin */,
    489     const GetShareUrlCallback& callback) {
    490   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    491   DCHECK(!callback.is_null());
    492 
    493   if (offline_) {
    494     scoped_ptr<ResourceEntry> null;
    495     base::MessageLoop::current()->PostTask(
    496         FROM_HERE,
    497         base::Bind(callback,
    498                    GDATA_NO_CONNECTION,
    499                    GURL()));
    500     return CancelCallback();
    501   }
    502 
    503   base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
    504   if (entry) {
    505     // Share urls are stored in the resource entry, and they do not rely on the
    506     // embedding origin.
    507     scoped_ptr<ResourceEntry> resource_entry =
    508         ResourceEntry::CreateFrom(*entry);
    509     const Link* share_url = resource_entry->GetLinkByType(Link::LINK_SHARE);
    510     if (share_url) {
    511       base::MessageLoop::current()->PostTask(
    512           FROM_HERE,
    513           base::Bind(callback, HTTP_SUCCESS, share_url->href()));
    514     } else {
    515       base::MessageLoop::current()->PostTask(
    516           FROM_HERE,
    517           base::Bind(callback, HTTP_SUCCESS, GURL()));
    518     }
    519     return CancelCallback();
    520   }
    521 
    522   scoped_ptr<ResourceEntry> null;
    523   base::MessageLoop::current()->PostTask(
    524       FROM_HERE,
    525       base::Bind(callback, HTTP_NOT_FOUND, GURL()));
    526   return CancelCallback();
    527 }
    528 
    529 CancelCallback FakeDriveService::GetAboutResource(
    530     const GetAboutResourceCallback& callback) {
    531   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    532   DCHECK(!callback.is_null());
    533 
    534   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    535   DCHECK(!callback.is_null());
    536 
    537   if (offline_) {
    538     scoped_ptr<AboutResource> null;
    539     base::MessageLoop::current()->PostTask(
    540         FROM_HERE,
    541         base::Bind(callback,
    542                    GDATA_NO_CONNECTION, base::Passed(&null)));
    543     return CancelCallback();
    544   }
    545 
    546   ++about_resource_load_count_;
    547   scoped_ptr<AboutResource> about_resource(
    548       AboutResource::CreateFromAccountMetadata(
    549           *AccountMetadata::CreateFrom(*account_metadata_value_),
    550           GetRootResourceId()));
    551   // Overwrite the change id.
    552   about_resource->set_largest_change_id(largest_changestamp_);
    553   base::MessageLoop::current()->PostTask(
    554       FROM_HERE,
    555       base::Bind(callback,
    556                  HTTP_SUCCESS, base::Passed(&about_resource)));
    557   return CancelCallback();
    558 }
    559 
    560 CancelCallback FakeDriveService::GetAppList(
    561     const GetAppListCallback& callback) {
    562   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    563   DCHECK(!callback.is_null());
    564   DCHECK(app_info_value_);
    565 
    566   if (offline_) {
    567     scoped_ptr<AppList> null;
    568     base::MessageLoop::current()->PostTask(
    569         FROM_HERE,
    570         base::Bind(callback,
    571                    GDATA_NO_CONNECTION,
    572                    base::Passed(&null)));
    573     return CancelCallback();
    574   }
    575 
    576   ++app_list_load_count_;
    577   scoped_ptr<AppList> app_list(AppList::CreateFrom(*app_info_value_));
    578   base::MessageLoop::current()->PostTask(
    579       FROM_HERE,
    580       base::Bind(callback, HTTP_SUCCESS, base::Passed(&app_list)));
    581   return CancelCallback();
    582 }
    583 
    584 CancelCallback FakeDriveService::DeleteResource(
    585     const std::string& resource_id,
    586     const std::string& etag,
    587     const EntryActionCallback& callback) {
    588   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    589   DCHECK(!callback.is_null());
    590 
    591   if (offline_) {
    592     base::MessageLoop::current()->PostTask(
    593         FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
    594     return CancelCallback();
    595   }
    596 
    597   base::ListValue* entries = NULL;
    598   // Go through entries and remove the one that matches |resource_id|.
    599   if (resource_list_value_->GetList("entry", &entries)) {
    600     for (size_t i = 0; i < entries->GetSize(); ++i) {
    601       base::DictionaryValue* entry = NULL;
    602       std::string current_resource_id;
    603       if (entries->GetDictionary(i, &entry) &&
    604           entry->GetString("gd$resourceId.$t", &current_resource_id) &&
    605           resource_id == current_resource_id) {
    606         GDataErrorCode error = google_apis::GDATA_OTHER_ERROR;
    607         if (entry->HasKey("gd$deleted")) {
    608           error = HTTP_NOT_FOUND;
    609         } else {
    610           entry->Set("gd$deleted", new DictionaryValue);
    611           AddNewChangestampAndETag(entry);
    612           error = HTTP_SUCCESS;
    613         }
    614         base::MessageLoop::current()->PostTask(
    615             FROM_HERE, base::Bind(callback, error));
    616         return CancelCallback();
    617       }
    618     }
    619   }
    620 
    621   base::MessageLoop::current()->PostTask(
    622       FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
    623   return CancelCallback();
    624 }
    625 
    626 CancelCallback FakeDriveService::DownloadFile(
    627     const base::FilePath& local_cache_path,
    628     const std::string& resource_id,
    629     const DownloadActionCallback& download_action_callback,
    630     const GetContentCallback& get_content_callback,
    631     const ProgressCallback& progress_callback) {
    632   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    633   DCHECK(!download_action_callback.is_null());
    634 
    635   if (offline_) {
    636     base::MessageLoop::current()->PostTask(
    637         FROM_HERE,
    638         base::Bind(download_action_callback,
    639                    GDATA_NO_CONNECTION,
    640                    base::FilePath()));
    641     return CancelCallback();
    642   }
    643 
    644   // The field content.src is the URL to download the file.
    645   base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
    646   if (!entry) {
    647     base::MessageLoopProxy::current()->PostTask(
    648         FROM_HERE,
    649         base::Bind(download_action_callback, HTTP_NOT_FOUND, base::FilePath()));
    650     return CancelCallback();
    651   }
    652 
    653   // Write "x"s of the file size specified in the entry.
    654   std::string file_size_string;
    655   entry->GetString("docs$size.$t", &file_size_string);
    656   int64 file_size = 0;
    657   if (base::StringToInt64(file_size_string, &file_size)) {
    658     base::BinaryValue* content_binary_data;
    659     std::string content_data;
    660     if (entry->GetBinary("test$data", &content_binary_data)) {
    661       content_data = std::string(content_binary_data->GetBuffer(),
    662           content_binary_data->GetSize());
    663     }
    664     DCHECK_EQ(static_cast<size_t>(file_size), content_data.size());
    665 
    666     if (!get_content_callback.is_null()) {
    667       const int64 kBlockSize = 5;
    668       for (int64 i = 0; i < file_size; i += kBlockSize) {
    669         const int64 size = std::min(kBlockSize, file_size - i);
    670         scoped_ptr<std::string> content_for_callback(
    671             new std::string(content_data.substr(i, size)));
    672         base::MessageLoopProxy::current()->PostTask(
    673             FROM_HERE,
    674             base::Bind(get_content_callback, HTTP_SUCCESS,
    675                        base::Passed(&content_for_callback)));
    676       }
    677     }
    678 
    679     if (test_util::WriteStringToFile(local_cache_path, content_data)) {
    680       if (!progress_callback.is_null()) {
    681         // See also the comment in ResumeUpload(). For testing that clients
    682         // can handle the case progress_callback is called multiple times,
    683         // here we invoke the callback twice.
    684         base::MessageLoopProxy::current()->PostTask(
    685             FROM_HERE,
    686             base::Bind(progress_callback, file_size / 2, file_size));
    687         base::MessageLoopProxy::current()->PostTask(
    688             FROM_HERE,
    689             base::Bind(progress_callback, file_size, file_size));
    690       }
    691       base::MessageLoopProxy::current()->PostTask(
    692           FROM_HERE,
    693           base::Bind(download_action_callback,
    694                      HTTP_SUCCESS,
    695                      local_cache_path));
    696       return CancelCallback();
    697     }
    698   }
    699 
    700   // Failed to write the content.
    701   base::MessageLoopProxy::current()->PostTask(
    702       FROM_HERE,
    703       base::Bind(download_action_callback, GDATA_FILE_ERROR, base::FilePath()));
    704   return CancelCallback();
    705 }
    706 
    707 CancelCallback FakeDriveService::CopyResource(
    708     const std::string& resource_id,
    709     const std::string& in_parent_resource_id,
    710     const std::string& new_title,
    711     const GetResourceEntryCallback& callback) {
    712   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    713   DCHECK(!callback.is_null());
    714 
    715   if (offline_) {
    716     scoped_ptr<ResourceEntry> null;
    717     base::MessageLoop::current()->PostTask(
    718         FROM_HERE,
    719         base::Bind(callback,
    720                    GDATA_NO_CONNECTION,
    721                    base::Passed(&null)));
    722     return CancelCallback();
    723   }
    724 
    725   const std::string& parent_resource_id = in_parent_resource_id.empty() ?
    726       GetRootResourceId() : in_parent_resource_id;
    727 
    728   base::ListValue* entries = NULL;
    729   // Go through entries and copy the one that matches |resource_id|.
    730   if (resource_list_value_->GetList("entry", &entries)) {
    731     for (size_t i = 0; i < entries->GetSize(); ++i) {
    732       base::DictionaryValue* entry = NULL;
    733       std::string current_resource_id;
    734       if (entries->GetDictionary(i, &entry) &&
    735           entry->GetString("gd$resourceId.$t", &current_resource_id) &&
    736           resource_id == current_resource_id) {
    737         // Make a copy and set the new resource ID and the new title.
    738         scoped_ptr<DictionaryValue> copied_entry(entry->DeepCopy());
    739         copied_entry->SetString("gd$resourceId.$t",
    740                                 resource_id + "_copied");
    741         copied_entry->SetString("title.$t", new_title);
    742 
    743         // Reset parent directory.
    744         base::ListValue* links = NULL;
    745         if (!entry->GetList("link", &links)) {
    746           links = new base::ListValue;
    747           entry->Set("link", links);
    748         }
    749         links->Clear();
    750 
    751         base::DictionaryValue* link = new base::DictionaryValue;
    752         link->SetString(
    753             "rel", "http://schemas.google.com/docs/2007#parent");
    754         link->SetString("href", GetFakeLinkUrl(parent_resource_id).spec());
    755         links->Append(link);
    756 
    757         AddNewChangestampAndETag(copied_entry.get());
    758 
    759         // Parse the new entry.
    760         scoped_ptr<ResourceEntry> resource_entry =
    761             ResourceEntry::CreateFrom(*copied_entry);
    762         // Add it to the resource list.
    763         entries->Append(copied_entry.release());
    764 
    765         base::MessageLoop::current()->PostTask(
    766             FROM_HERE,
    767             base::Bind(callback,
    768                        HTTP_SUCCESS,
    769                        base::Passed(&resource_entry)));
    770         return CancelCallback();
    771       }
    772     }
    773   }
    774 
    775   scoped_ptr<ResourceEntry> null;
    776   base::MessageLoop::current()->PostTask(
    777       FROM_HERE,
    778       base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
    779   return CancelCallback();
    780 }
    781 
    782 CancelCallback FakeDriveService::CopyHostedDocument(
    783     const std::string& resource_id,
    784     const std::string& new_title,
    785     const GetResourceEntryCallback& callback) {
    786   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    787   DCHECK(!callback.is_null());
    788 
    789   return CopyResource(resource_id, std::string(), new_title, callback);
    790 }
    791 
    792 CancelCallback FakeDriveService::RenameResource(
    793     const std::string& resource_id,
    794     const std::string& new_title,
    795     const EntryActionCallback& callback) {
    796   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    797   DCHECK(!callback.is_null());
    798 
    799   if (offline_) {
    800     base::MessageLoop::current()->PostTask(
    801         FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
    802     return CancelCallback();
    803   }
    804 
    805   base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
    806   if (entry) {
    807     entry->SetString("title.$t", new_title);
    808     AddNewChangestampAndETag(entry);
    809     base::MessageLoop::current()->PostTask(
    810         FROM_HERE, base::Bind(callback, HTTP_SUCCESS));
    811     return CancelCallback();
    812   }
    813 
    814   base::MessageLoop::current()->PostTask(
    815       FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
    816   return CancelCallback();
    817 }
    818 
    819 CancelCallback FakeDriveService::TouchResource(
    820     const std::string& resource_id,
    821     const base::Time& modified_date,
    822     const base::Time& last_viewed_by_me_date,
    823     const GetResourceEntryCallback& callback) {
    824   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    825   DCHECK(!modified_date.is_null());
    826   DCHECK(!last_viewed_by_me_date.is_null());
    827   DCHECK(!callback.is_null());
    828 
    829   if (offline_) {
    830     base::MessageLoop::current()->PostTask(
    831         FROM_HERE,
    832         base::Bind(callback, GDATA_NO_CONNECTION,
    833                    base::Passed(scoped_ptr<ResourceEntry>())));
    834     return CancelCallback();
    835   }
    836 
    837   base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
    838   if (!entry) {
    839     base::MessageLoop::current()->PostTask(
    840         FROM_HERE,
    841         base::Bind(callback, HTTP_NOT_FOUND,
    842                    base::Passed(scoped_ptr<ResourceEntry>())));
    843     return CancelCallback();
    844   }
    845 
    846   entry->SetString("updated.$t",
    847                    util::FormatTimeAsString(modified_date));
    848   entry->SetString("gd$lastViewed.$t",
    849                    util::FormatTimeAsString(last_viewed_by_me_date));
    850   AddNewChangestampAndETag(entry);
    851 
    852   scoped_ptr<ResourceEntry> parsed_entry(ResourceEntry::CreateFrom(*entry));
    853   base::MessageLoop::current()->PostTask(
    854       FROM_HERE,
    855       base::Bind(callback, HTTP_SUCCESS, base::Passed(&parsed_entry)));
    856   return CancelCallback();
    857 }
    858 
    859 CancelCallback FakeDriveService::AddResourceToDirectory(
    860     const std::string& parent_resource_id,
    861     const std::string& resource_id,
    862     const EntryActionCallback& callback) {
    863   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    864   DCHECK(!callback.is_null());
    865 
    866   if (offline_) {
    867     base::MessageLoop::current()->PostTask(
    868         FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
    869     return CancelCallback();
    870   }
    871 
    872   base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
    873   if (entry) {
    874     base::ListValue* links = NULL;
    875     if (!entry->GetList("link", &links)) {
    876       links = new base::ListValue;
    877       entry->Set("link", links);
    878     }
    879 
    880     // On the real Drive server, resources do not necessary shape a tree
    881     // structure. That is, each resource can have multiple parent.
    882     // We mimic the behavior here; AddResourceToDirectoy just adds
    883     // one more parent link, not overwriting old links.
    884     base::DictionaryValue* link = new base::DictionaryValue;
    885     link->SetString("rel", "http://schemas.google.com/docs/2007#parent");
    886     link->SetString(
    887         "href", GetFakeLinkUrl(parent_resource_id).spec());
    888     links->Append(link);
    889 
    890     AddNewChangestampAndETag(entry);
    891     base::MessageLoop::current()->PostTask(
    892         FROM_HERE, base::Bind(callback, HTTP_SUCCESS));
    893     return CancelCallback();
    894   }
    895 
    896   base::MessageLoop::current()->PostTask(
    897       FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
    898   return CancelCallback();
    899 }
    900 
    901 CancelCallback FakeDriveService::RemoveResourceFromDirectory(
    902     const std::string& parent_resource_id,
    903     const std::string& resource_id,
    904     const EntryActionCallback& callback) {
    905   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    906   DCHECK(!callback.is_null());
    907 
    908   if (offline_) {
    909     base::MessageLoop::current()->PostTask(
    910         FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
    911     return CancelCallback();
    912   }
    913 
    914   base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
    915   if (entry) {
    916     base::ListValue* links = NULL;
    917     if (entry->GetList("link", &links)) {
    918       GURL parent_content_url = GetFakeLinkUrl(parent_resource_id);
    919       for (size_t i = 0; i < links->GetSize(); ++i) {
    920         base::DictionaryValue* link = NULL;
    921         std::string rel;
    922         std::string href;
    923         if (links->GetDictionary(i, &link) &&
    924             link->GetString("rel", &rel) &&
    925             link->GetString("href", &href) &&
    926             rel == "http://schemas.google.com/docs/2007#parent" &&
    927             GURL(href) == parent_content_url) {
    928           links->Remove(i, NULL);
    929           AddNewChangestampAndETag(entry);
    930           base::MessageLoop::current()->PostTask(
    931               FROM_HERE, base::Bind(callback, HTTP_SUCCESS));
    932           return CancelCallback();
    933         }
    934       }
    935     }
    936   }
    937 
    938   base::MessageLoop::current()->PostTask(
    939       FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
    940   return CancelCallback();
    941 }
    942 
    943 CancelCallback FakeDriveService::AddNewDirectory(
    944     const std::string& parent_resource_id,
    945     const std::string& directory_title,
    946     const GetResourceEntryCallback& callback) {
    947   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    948   DCHECK(!callback.is_null());
    949 
    950   if (offline_) {
    951     scoped_ptr<ResourceEntry> null;
    952     base::MessageLoop::current()->PostTask(
    953         FROM_HERE,
    954         base::Bind(callback,
    955                    GDATA_NO_CONNECTION,
    956                    base::Passed(&null)));
    957     return CancelCallback();
    958   }
    959 
    960   const char kContentType[] = "application/atom+xml;type=feed";
    961   const base::DictionaryValue* new_entry = AddNewEntry(kContentType,
    962                                                        "",  // content_data
    963                                                        parent_resource_id,
    964                                                        directory_title,
    965                                                        false,  // shared_with_me
    966                                                        "folder");
    967   if (!new_entry) {
    968     scoped_ptr<ResourceEntry> null;
    969     base::MessageLoop::current()->PostTask(
    970         FROM_HERE,
    971         base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
    972     return CancelCallback();
    973   }
    974 
    975   scoped_ptr<ResourceEntry> parsed_entry(ResourceEntry::CreateFrom(*new_entry));
    976   base::MessageLoop::current()->PostTask(
    977       FROM_HERE,
    978       base::Bind(callback, HTTP_CREATED, base::Passed(&parsed_entry)));
    979   return CancelCallback();
    980 }
    981 
    982 CancelCallback FakeDriveService::InitiateUploadNewFile(
    983     const std::string& content_type,
    984     int64 content_length,
    985     const std::string& parent_resource_id,
    986     const std::string& title,
    987     const InitiateUploadCallback& callback) {
    988   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    989   DCHECK(!callback.is_null());
    990 
    991   if (offline_) {
    992     base::MessageLoop::current()->PostTask(
    993         FROM_HERE,
    994         base::Bind(callback, GDATA_NO_CONNECTION, GURL()));
    995     return CancelCallback();
    996   }
    997 
    998   if (parent_resource_id != GetRootResourceId() &&
    999       !FindEntryByResourceId(parent_resource_id)) {
   1000     base::MessageLoop::current()->PostTask(
   1001         FROM_HERE,
   1002         base::Bind(callback, HTTP_NOT_FOUND, GURL()));
   1003     return CancelCallback();
   1004   }
   1005 
   1006   GURL session_url = GetNewUploadSessionUrl();
   1007   upload_sessions_[session_url] =
   1008       UploadSession(content_type, content_length,
   1009                     parent_resource_id,
   1010                     "",  // resource_id
   1011                     "",  // etag
   1012                     title);
   1013 
   1014   base::MessageLoop::current()->PostTask(
   1015       FROM_HERE,
   1016       base::Bind(callback, HTTP_SUCCESS, session_url));
   1017   return CancelCallback();
   1018 }
   1019 
   1020 CancelCallback FakeDriveService::InitiateUploadExistingFile(
   1021     const std::string& content_type,
   1022     int64 content_length,
   1023     const std::string& resource_id,
   1024     const std::string& etag,
   1025     const InitiateUploadCallback& callback) {
   1026   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
   1027   DCHECK(!callback.is_null());
   1028 
   1029   if (offline_) {
   1030     base::MessageLoop::current()->PostTask(
   1031         FROM_HERE,
   1032         base::Bind(callback, GDATA_NO_CONNECTION, GURL()));
   1033     return CancelCallback();
   1034   }
   1035 
   1036   DictionaryValue* entry = FindEntryByResourceId(resource_id);
   1037   if (!entry) {
   1038     base::MessageLoop::current()->PostTask(
   1039         FROM_HERE,
   1040         base::Bind(callback, HTTP_NOT_FOUND, GURL()));
   1041     return CancelCallback();
   1042   }
   1043 
   1044   std::string entry_etag;
   1045   entry->GetString("gd$etag", &entry_etag);
   1046   if (!etag.empty() && etag != entry_etag) {
   1047     base::MessageLoop::current()->PostTask(
   1048         FROM_HERE,
   1049         base::Bind(callback, HTTP_PRECONDITION, GURL()));
   1050     return CancelCallback();
   1051   }
   1052 
   1053   GURL session_url = GetNewUploadSessionUrl();
   1054   upload_sessions_[session_url] =
   1055       UploadSession(content_type, content_length,
   1056                     "",  // parent_resource_id
   1057                     resource_id,
   1058                     entry_etag,
   1059                     "" /* title */);
   1060 
   1061   base::MessageLoop::current()->PostTask(
   1062       FROM_HERE,
   1063       base::Bind(callback, HTTP_SUCCESS, session_url));
   1064   return CancelCallback();
   1065 }
   1066 
   1067 CancelCallback FakeDriveService::GetUploadStatus(
   1068     const GURL& upload_url,
   1069     int64 content_length,
   1070     const UploadRangeCallback& callback) {
   1071   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
   1072   DCHECK(!callback.is_null());
   1073   return CancelCallback();
   1074 }
   1075 
   1076 CancelCallback FakeDriveService::ResumeUpload(
   1077       const GURL& upload_url,
   1078       int64 start_position,
   1079       int64 end_position,
   1080       int64 content_length,
   1081       const std::string& content_type,
   1082       const base::FilePath& local_file_path,
   1083       const UploadRangeCallback& callback,
   1084       const ProgressCallback& progress_callback) {
   1085   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
   1086   DCHECK(!callback.is_null());
   1087 
   1088   GetResourceEntryCallback completion_callback
   1089       = base::Bind(&ScheduleUploadRangeCallback,
   1090                    callback, start_position, end_position);
   1091 
   1092   if (offline_) {
   1093     completion_callback.Run(GDATA_NO_CONNECTION, scoped_ptr<ResourceEntry>());
   1094     return CancelCallback();
   1095   }
   1096 
   1097   if (!upload_sessions_.count(upload_url)) {
   1098     completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>());
   1099     return CancelCallback();
   1100   }
   1101 
   1102   UploadSession* session = &upload_sessions_[upload_url];
   1103 
   1104   // Chunks are required to be sent in such a ways that they fill from the start
   1105   // of the not-yet-uploaded part with no gaps nor overlaps.
   1106   if (session->uploaded_size != start_position) {
   1107     completion_callback.Run(HTTP_BAD_REQUEST, scoped_ptr<ResourceEntry>());
   1108     return CancelCallback();
   1109   }
   1110 
   1111   if (!progress_callback.is_null()) {
   1112     // In the real GDataWapi/Drive DriveService, progress is reported in
   1113     // nondeterministic timing. In this fake implementation, we choose to call
   1114     // it twice per one ResumeUpload. This is for making sure that client code
   1115     // works fine even if the callback is invoked more than once; it is the
   1116     // crucial difference of the progress callback from others.
   1117     // Note that progress is notified in the relative offset in each chunk.
   1118     const int64 chunk_size = end_position - start_position;
   1119     base::MessageLoop::current()->PostTask(
   1120         FROM_HERE, base::Bind(progress_callback, chunk_size / 2, chunk_size));
   1121     base::MessageLoop::current()->PostTask(
   1122         FROM_HERE, base::Bind(progress_callback, chunk_size, chunk_size));
   1123   }
   1124 
   1125   if (content_length != end_position) {
   1126     session->uploaded_size = end_position;
   1127     completion_callback.Run(HTTP_RESUME_INCOMPLETE,
   1128                             scoped_ptr<ResourceEntry>());
   1129     return CancelCallback();
   1130   }
   1131 
   1132   std::string content_data;
   1133   if (!file_util::ReadFileToString(local_file_path, &content_data)) {
   1134     session->uploaded_size = end_position;
   1135     completion_callback.Run(GDATA_FILE_ERROR, scoped_ptr<ResourceEntry>());
   1136     return CancelCallback();
   1137   }
   1138   session->uploaded_size = end_position;
   1139 
   1140   // |resource_id| is empty if the upload is for new file.
   1141   if (session->resource_id.empty()) {
   1142     DCHECK(!session->parent_resource_id.empty());
   1143     DCHECK(!session->title.empty());
   1144     const DictionaryValue* new_entry = AddNewEntry(
   1145         session->content_type,
   1146         content_data,
   1147         session->parent_resource_id,
   1148         session->title,
   1149         false,  // shared_with_me
   1150         "file");
   1151     if (!new_entry) {
   1152       completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>());
   1153       return CancelCallback();
   1154     }
   1155 
   1156     completion_callback.Run(HTTP_CREATED,
   1157                             ResourceEntry::CreateFrom(*new_entry));
   1158     return CancelCallback();
   1159   }
   1160 
   1161   DictionaryValue* entry = FindEntryByResourceId(session->resource_id);
   1162   if (!entry) {
   1163     completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>());
   1164     return CancelCallback();
   1165   }
   1166 
   1167   std::string entry_etag;
   1168   entry->GetString("gd$etag", &entry_etag);
   1169   if (entry_etag.empty() || session->etag != entry_etag) {
   1170     completion_callback.Run(HTTP_PRECONDITION, scoped_ptr<ResourceEntry>());
   1171     return CancelCallback();
   1172   }
   1173 
   1174   entry->SetString("docs$md5Checksum.$t", base::MD5String(content_data));
   1175   entry->Set("test$data",
   1176              base::BinaryValue::CreateWithCopiedBuffer(
   1177                  content_data.data(), content_data.size()));
   1178   entry->SetString("docs$size.$t", base::Int64ToString(end_position));
   1179   AddNewChangestampAndETag(entry);
   1180 
   1181   completion_callback.Run(HTTP_SUCCESS, ResourceEntry::CreateFrom(*entry));
   1182   return CancelCallback();
   1183 }
   1184 
   1185 CancelCallback FakeDriveService::AuthorizeApp(
   1186     const std::string& resource_id,
   1187     const std::string& app_id,
   1188     const AuthorizeAppCallback& callback) {
   1189   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
   1190   DCHECK(!callback.is_null());
   1191   return CancelCallback();
   1192 }
   1193 
   1194 void FakeDriveService::AddNewFile(const std::string& content_type,
   1195                                   const std::string& content_data,
   1196                                   const std::string& parent_resource_id,
   1197                                   const std::string& title,
   1198                                   bool shared_with_me,
   1199                                   const GetResourceEntryCallback& callback) {
   1200   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
   1201   DCHECK(!callback.is_null());
   1202 
   1203   if (offline_) {
   1204     scoped_ptr<ResourceEntry> null;
   1205     base::MessageLoop::current()->PostTask(
   1206         FROM_HERE,
   1207         base::Bind(callback,
   1208                    GDATA_NO_CONNECTION,
   1209                    base::Passed(&null)));
   1210     return;
   1211   }
   1212 
   1213   // Prepare "kind" for hosted documents. This only supports Google Document.
   1214   std::string entry_kind;
   1215   if (content_type == "application/vnd.google-apps.document")
   1216     entry_kind = "document";
   1217   else
   1218     entry_kind = "file";
   1219 
   1220   const base::DictionaryValue* new_entry = AddNewEntry(content_type,
   1221                                                        content_data,
   1222                                                        parent_resource_id,
   1223                                                        title,
   1224                                                        shared_with_me,
   1225                                                        entry_kind);
   1226   if (!new_entry) {
   1227     scoped_ptr<ResourceEntry> null;
   1228     base::MessageLoop::current()->PostTask(
   1229         FROM_HERE,
   1230         base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
   1231     return;
   1232   }
   1233 
   1234   scoped_ptr<ResourceEntry> parsed_entry(
   1235       ResourceEntry::CreateFrom(*new_entry));
   1236   base::MessageLoop::current()->PostTask(
   1237       FROM_HERE,
   1238       base::Bind(callback, HTTP_CREATED, base::Passed(&parsed_entry)));
   1239 }
   1240 
   1241 void FakeDriveService::SetLastModifiedTime(
   1242     const std::string& resource_id,
   1243     const base::Time& last_modified_time,
   1244     const GetResourceEntryCallback& callback) {
   1245   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
   1246   DCHECK(!callback.is_null());
   1247 
   1248   if (offline_) {
   1249     scoped_ptr<ResourceEntry> null;
   1250     base::MessageLoop::current()->PostTask(
   1251         FROM_HERE,
   1252         base::Bind(callback,
   1253                    GDATA_NO_CONNECTION,
   1254                    base::Passed(&null)));
   1255     return;
   1256   }
   1257 
   1258   base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
   1259   if (!entry) {
   1260     scoped_ptr<ResourceEntry> null;
   1261     base::MessageLoop::current()->PostTask(
   1262         FROM_HERE,
   1263         base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
   1264     return;
   1265   }
   1266 
   1267   if (last_modified_time.is_null()) {
   1268     entry->Remove("updated.$t", NULL);
   1269   } else {
   1270     entry->SetString("updated.$t",
   1271                      util::FormatTimeAsString(last_modified_time));
   1272   }
   1273 
   1274   scoped_ptr<ResourceEntry> parsed_entry(
   1275       ResourceEntry::CreateFrom(*entry));
   1276   base::MessageLoop::current()->PostTask(
   1277       FROM_HERE,
   1278       base::Bind(callback, HTTP_SUCCESS, base::Passed(&parsed_entry)));
   1279 }
   1280 
   1281 base::DictionaryValue* FakeDriveService::FindEntryByResourceId(
   1282     const std::string& resource_id) {
   1283   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
   1284 
   1285   base::ListValue* entries = NULL;
   1286   // Go through entries and return the one that matches |resource_id|.
   1287   if (resource_list_value_->GetList("entry", &entries)) {
   1288     for (size_t i = 0; i < entries->GetSize(); ++i) {
   1289       base::DictionaryValue* entry = NULL;
   1290       std::string current_resource_id;
   1291       if (entries->GetDictionary(i, &entry) &&
   1292           entry->GetString("gd$resourceId.$t", &current_resource_id) &&
   1293           resource_id == current_resource_id) {
   1294         return entry;
   1295       }
   1296     }
   1297   }
   1298 
   1299   return NULL;
   1300 }
   1301 
   1302 base::DictionaryValue* FakeDriveService::FindEntryByContentUrl(
   1303     const GURL& content_url) {
   1304   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
   1305 
   1306   base::ListValue* entries = NULL;
   1307   // Go through entries and return the one that matches |content_url|.
   1308   if (resource_list_value_->GetList("entry", &entries)) {
   1309     for (size_t i = 0; i < entries->GetSize(); ++i) {
   1310       base::DictionaryValue* entry = NULL;
   1311       std::string current_content_url;
   1312       if (entries->GetDictionary(i, &entry) &&
   1313           entry->GetString("content.src", &current_content_url) &&
   1314           content_url == GURL(current_content_url)) {
   1315         return entry;
   1316       }
   1317     }
   1318   }
   1319 
   1320   return NULL;
   1321 }
   1322 
   1323 std::string FakeDriveService::GetNewResourceId() {
   1324   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
   1325 
   1326   ++resource_id_count_;
   1327   return base::StringPrintf("resource_id_%d", resource_id_count_);
   1328 }
   1329 
   1330 void FakeDriveService::AddNewChangestampAndETag(base::DictionaryValue* entry) {
   1331   ++largest_changestamp_;
   1332   entry->SetString("docs$changestamp.value",
   1333                    base::Int64ToString(largest_changestamp_));
   1334   entry->SetString("gd$etag",
   1335                    "etag_" + base::Int64ToString(largest_changestamp_));
   1336 }
   1337 
   1338 const base::DictionaryValue* FakeDriveService::AddNewEntry(
   1339     const std::string& content_type,
   1340     const std::string& content_data,
   1341     const std::string& parent_resource_id,
   1342     const std::string& title,
   1343     bool shared_with_me,
   1344     const std::string& entry_kind) {
   1345   DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
   1346 
   1347   if (parent_resource_id != GetRootResourceId() &&
   1348       !FindEntryByResourceId(parent_resource_id)) {
   1349     return NULL;
   1350   }
   1351 
   1352   std::string resource_id = GetNewResourceId();
   1353   GURL upload_url = GURL("https://xxx/upload/" + resource_id);
   1354 
   1355   scoped_ptr<base::DictionaryValue> new_entry(new base::DictionaryValue);
   1356   // Set the resource ID and the title
   1357   new_entry->SetString("gd$resourceId.$t", resource_id);
   1358   new_entry->SetString("title.$t", title);
   1359   new_entry->SetString("docs$filename", title);
   1360   // Set the contents, size and MD5 for a file.
   1361   if (entry_kind == "file") {
   1362     new_entry->Set("test$data",
   1363         base::BinaryValue::CreateWithCopiedBuffer(
   1364             content_data.c_str(), content_data.size()));
   1365     new_entry->SetString("docs$size.$t",
   1366                          base::Int64ToString(content_data.size()));
   1367     new_entry->SetString("docs$md5Checksum.$t",
   1368                          base::MD5String(content_data));
   1369   }
   1370 
   1371   // Add "category" which sets the resource type to |entry_kind|.
   1372   base::ListValue* categories = new base::ListValue;
   1373   base::DictionaryValue* category = new base::DictionaryValue;
   1374   category->SetString("scheme", "http://schemas.google.com/g/2005#kind");
   1375   category->SetString("term", "http://schemas.google.com/docs/2007#" +
   1376                       entry_kind);
   1377   categories->Append(category);
   1378   new_entry->Set("category", categories);
   1379   if (shared_with_me) {
   1380     base::DictionaryValue* shared_with_me_label = new base::DictionaryValue;
   1381     shared_with_me_label->SetString("label", "shared-with-me");
   1382     shared_with_me_label->SetString("scheme",
   1383                                     "http://schemas.google.com/g/2005/labels");
   1384     shared_with_me_label->SetString(
   1385         "term", "http://schemas.google.com/g/2005/labels#shared");
   1386     categories->Append(shared_with_me_label);
   1387   }
   1388 
   1389   std::string escaped_resource_id = net::EscapePath(resource_id);
   1390 
   1391   // Add "content" which sets the content URL.
   1392   base::DictionaryValue* content = new base::DictionaryValue;
   1393   content->SetString("src", "https://xxx/content/" + escaped_resource_id);
   1394   content->SetString("type", content_type);
   1395   new_entry->Set("content", content);
   1396 
   1397   // Add "link" which sets the parent URL, the edit URL and the upload URL.
   1398   base::ListValue* links = new base::ListValue;
   1399 
   1400   base::DictionaryValue* parent_link = new base::DictionaryValue;
   1401   parent_link->SetString("href", GetFakeLinkUrl(parent_resource_id).spec());
   1402   parent_link->SetString("rel",
   1403                          "http://schemas.google.com/docs/2007#parent");
   1404   links->Append(parent_link);
   1405 
   1406   base::DictionaryValue* edit_link = new base::DictionaryValue;
   1407   edit_link->SetString("href", "https://xxx/edit/" + escaped_resource_id);
   1408   edit_link->SetString("rel", "edit");
   1409   links->Append(edit_link);
   1410 
   1411   base::DictionaryValue* upload_link = new base::DictionaryValue;
   1412   upload_link->SetString("href", upload_url.spec());
   1413   upload_link->SetString("rel", kUploadUrlRel);
   1414   links->Append(upload_link);
   1415 
   1416   const GURL share_url = net::AppendOrReplaceQueryParameter(
   1417       share_url_base_, "name", title);
   1418   base::DictionaryValue* share_link = new base::DictionaryValue;
   1419   upload_link->SetString("href", share_url.spec());
   1420   upload_link->SetString("rel", kShareUrlRel);
   1421   links->Append(share_link);
   1422   new_entry->Set("link", links);
   1423 
   1424   AddNewChangestampAndETag(new_entry.get());
   1425 
   1426   base::Time published_date =
   1427       base::Time() + base::TimeDelta::FromMilliseconds(++published_date_seq_);
   1428   new_entry->SetString("published.$t",
   1429                        util::FormatTimeAsString(published_date));
   1430 
   1431   // If there are no entries, prepare an empty entry to add.
   1432   if (!resource_list_value_->HasKey("entry"))
   1433     resource_list_value_->Set("entry", new ListValue);
   1434 
   1435   base::DictionaryValue* raw_new_entry = new_entry.release();
   1436   base::ListValue* entries = NULL;
   1437   if (resource_list_value_->GetList("entry", &entries))
   1438     entries->Append(raw_new_entry);
   1439 
   1440   return raw_new_entry;
   1441 }
   1442 
   1443 void FakeDriveService::GetResourceListInternal(
   1444     int64 start_changestamp,
   1445     const std::string& search_query,
   1446     const std::string& directory_resource_id,
   1447     int start_offset,
   1448     int max_results,
   1449     int* load_counter,
   1450     const GetResourceListCallback& callback) {
   1451   if (offline_) {
   1452     base::MessageLoop::current()->PostTask(
   1453         FROM_HERE,
   1454         base::Bind(callback,
   1455                    GDATA_NO_CONNECTION,
   1456                    base::Passed(scoped_ptr<ResourceList>())));
   1457     return;
   1458   }
   1459 
   1460   scoped_ptr<ResourceList> resource_list =
   1461       ResourceList::CreateFrom(*resource_list_value_);
   1462 
   1463   // TODO(hashimoto): Drive API always provides largest changestamp. Remove this
   1464   // if-statement after API switch.
   1465   if (start_changestamp > 0 && start_offset == 0)
   1466     resource_list->set_largest_changestamp(largest_changestamp_);
   1467 
   1468   // Filter out entries per parameters like |directory_resource_id| and
   1469   // |search_query|.
   1470   ScopedVector<ResourceEntry>* entries = resource_list->mutable_entries();
   1471 
   1472   int num_entries_matched = 0;
   1473   for (size_t i = 0; i < entries->size();) {
   1474     ResourceEntry* entry = (*entries)[i];
   1475     bool should_exclude = false;
   1476 
   1477     // If |directory_resource_id| is set, exclude the entry if it's not in
   1478     // the target directory.
   1479     if (!directory_resource_id.empty()) {
   1480       // Get the parent resource ID of the entry.
   1481       std::string parent_resource_id;
   1482       const google_apis::Link* parent_link =
   1483           entry->GetLinkByType(Link::LINK_PARENT);
   1484       if (parent_link) {
   1485         parent_resource_id =
   1486             net::UnescapeURLComponent(parent_link->href().ExtractFileName(),
   1487                                       net::UnescapeRule::URL_SPECIAL_CHARS);
   1488       }
   1489       if (directory_resource_id != parent_resource_id)
   1490         should_exclude = true;
   1491     }
   1492 
   1493     // If |search_query| is set, exclude the entry if it does not contain the
   1494     // search query in the title.
   1495     if (!should_exclude && !search_query.empty() &&
   1496         !EntryMatchWithQuery(*entry, search_query)) {
   1497       should_exclude = true;
   1498     }
   1499 
   1500     // If |start_changestamp| is set, exclude the entry if the
   1501     // changestamp is older than |largest_changestamp|.
   1502     // See https://developers.google.com/google-apps/documents-list/
   1503     // #retrieving_all_changes_since_a_given_changestamp
   1504     if (start_changestamp > 0 && entry->changestamp() < start_changestamp)
   1505       should_exclude = true;
   1506 
   1507     // If the caller requests other list than change list by specifying
   1508     // zero-|start_changestamp|, exclude deleted entry from the result.
   1509     if (!start_changestamp && entry->deleted())
   1510       should_exclude = true;
   1511 
   1512     // The entry matched the criteria for inclusion.
   1513     if (!should_exclude)
   1514       ++num_entries_matched;
   1515 
   1516     // If |start_offset| is set, exclude the entry if the entry is before the
   1517     // start index. <= instead of < as |num_entries_matched| was
   1518     // already incremented.
   1519     if (start_offset > 0 && num_entries_matched <= start_offset)
   1520       should_exclude = true;
   1521 
   1522     if (should_exclude)
   1523       entries->erase(entries->begin() + i);
   1524     else
   1525       ++i;
   1526   }
   1527 
   1528   // If |max_results| is set, trim the entries if the number exceeded the max
   1529   // results.
   1530   if (max_results > 0 && entries->size() > static_cast<size_t>(max_results)) {
   1531     entries->erase(entries->begin() + max_results, entries->end());
   1532     // Adds the next URL.
   1533     // Here, we embed information which is needed for continuing the
   1534     // GetResourceList request in the next invocation into url query
   1535     // parameters.
   1536     GURL next_url(base::StringPrintf(
   1537         "http://localhost/?start-offset=%d&max-results=%d",
   1538         start_offset + max_results,
   1539         max_results));
   1540     if (start_changestamp > 0) {
   1541       next_url = net::AppendOrReplaceQueryParameter(
   1542           next_url, "changestamp",
   1543           base::Int64ToString(start_changestamp).c_str());
   1544     }
   1545     if (!search_query.empty()) {
   1546       next_url = net::AppendOrReplaceQueryParameter(
   1547           next_url, "q", search_query);
   1548     }
   1549     if (!directory_resource_id.empty()) {
   1550       next_url = net::AppendOrReplaceQueryParameter(
   1551           next_url, "parent", directory_resource_id);
   1552     }
   1553 
   1554     Link* link = new Link;
   1555     link->set_type(Link::LINK_NEXT);
   1556     link->set_href(next_url);
   1557     resource_list->mutable_links()->push_back(link);
   1558   }
   1559 
   1560   if (load_counter)
   1561     *load_counter += 1;
   1562   base::MessageLoop::current()->PostTask(
   1563       FROM_HERE,
   1564       base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_list)));
   1565 }
   1566 
   1567 GURL FakeDriveService::GetNewUploadSessionUrl() {
   1568   return GURL("https://upload_session_url/" +
   1569               base::Int64ToString(next_upload_sequence_number_++));
   1570 }
   1571 
   1572 }  // namespace drive
   1573