kiosk_app_data.cc revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
1// Copyright 2013 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/app_mode/kiosk_app_data.h"
6
7#include <vector>
8
9#include "base/bind.h"
10#include "base/file_util.h"
11#include "base/json/json_writer.h"
12#include "base/memory/ref_counted_memory.h"
13#include "base/prefs/pref_service.h"
14#include "base/prefs/scoped_user_pref_update.h"
15#include "base/threading/sequenced_worker_pool.h"
16#include "base/values.h"
17#include "chrome/browser/browser_process.h"
18#include "chrome/browser/chromeos/app_mode/kiosk_app_data_delegate.h"
19#include "chrome/browser/chromeos/app_mode/kiosk_app_manager.h"
20#include "chrome/browser/extensions/extension_service.h"
21#include "chrome/browser/extensions/image_loader.h"
22#include "chrome/browser/extensions/webstore_data_fetcher.h"
23#include "chrome/browser/extensions/webstore_install_helper.h"
24#include "chrome/browser/image_decoder.h"
25#include "chrome/browser/profiles/profile.h"
26#include "chrome/common/extensions/extension_constants.h"
27#include "chrome/common/extensions/manifest_handlers/icons_handler.h"
28#include "content/public/browser/browser_thread.h"
29#include "extensions/browser/extension_system.h"
30#include "extensions/common/manifest.h"
31#include "extensions/common/manifest_constants.h"
32#include "ui/gfx/codec/png_codec.h"
33#include "ui/gfx/image/image.h"
34
35using content::BrowserThread;
36
37namespace chromeos {
38
39namespace {
40
41// Keys for local state data. See sample layout in KioskAppManager.
42const char kKeyName[] = "name";
43const char kKeyIcon[] = "icon";
44
45const char kInvalidWebstoreResponseError[] = "Invalid Chrome Web Store reponse";
46
47// Icon file extension.
48const char kIconFileExtension[] = ".png";
49
50// Save |raw_icon| for given |app_id|.
51void SaveIconToLocalOnBlockingPool(
52    const base::FilePath& icon_path,
53    scoped_refptr<base::RefCountedString> raw_icon) {
54  DCHECK(BrowserThread::GetBlockingPool()->RunsTasksOnCurrentThread());
55
56  base::FilePath dir = icon_path.DirName();
57  if (!base::PathExists(dir))
58    CHECK(base::CreateDirectory(dir));
59
60  CHECK_EQ(static_cast<int>(raw_icon->size()),
61           base::WriteFile(icon_path,
62                           raw_icon->data().c_str(), raw_icon->size()));
63}
64
65// Returns true for valid kiosk app manifest.
66bool IsValidKioskAppManifest(const extensions::Manifest& manifest) {
67  bool kiosk_enabled;
68  if (manifest.GetBoolean(extensions::manifest_keys::kKioskEnabled,
69                          &kiosk_enabled)) {
70    return kiosk_enabled;
71  }
72
73  return false;
74}
75
76std::string ValueToString(const base::Value* value) {
77  std::string json;
78  base::JSONWriter::Write(value, &json);
79  return json;
80}
81
82}  // namespace
83
84////////////////////////////////////////////////////////////////////////////////
85// KioskAppData::IconLoader
86// Loads locally stored icon data and decode it.
87
88class KioskAppData::IconLoader : public ImageDecoder::Delegate {
89 public:
90  enum LoadResult {
91    SUCCESS,
92    FAILED_TO_LOAD,
93    FAILED_TO_DECODE,
94  };
95
96  IconLoader(const base::WeakPtr<KioskAppData>& client,
97             const base::FilePath& icon_path)
98      : client_(client),
99        icon_path_(icon_path),
100        load_result_(SUCCESS) {}
101
102  void Start() {
103    base::SequencedWorkerPool* pool = BrowserThread::GetBlockingPool();
104    base::SequencedWorkerPool::SequenceToken token = pool->GetSequenceToken();
105    task_runner_ = pool->GetSequencedTaskRunnerWithShutdownBehavior(
106        token,
107        base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN);
108    task_runner_->PostTask(FROM_HERE,
109                           base::Bind(&IconLoader::LoadOnBlockingPool,
110                                      base::Unretained(this)));
111  }
112
113 private:
114  friend class base::RefCountedThreadSafe<IconLoader>;
115
116  virtual ~IconLoader() {}
117
118  // Loads the icon from locally stored |icon_path_| on the blocking pool
119  void LoadOnBlockingPool() {
120    DCHECK(task_runner_->RunsTasksOnCurrentThread());
121
122    std::string data;
123    if (!base::ReadFileToString(base::FilePath(icon_path_), &data)) {
124      ReportResultOnBlockingPool(FAILED_TO_LOAD);
125      return;
126    }
127    raw_icon_ = base::RefCountedString::TakeString(&data);
128
129    scoped_refptr<ImageDecoder> image_decoder = new ImageDecoder(
130        this, raw_icon_->data(), ImageDecoder::DEFAULT_CODEC);
131    image_decoder->Start(task_runner_);
132  }
133
134  void ReportResultOnBlockingPool(LoadResult result) {
135    DCHECK(task_runner_->RunsTasksOnCurrentThread());
136
137    load_result_ = result;
138    BrowserThread::PostTask(
139        BrowserThread::UI,
140        FROM_HERE,
141        base::Bind(&IconLoader::ReportResultOnUIThread,
142                   base::Unretained(this)));
143  }
144
145  void NotifyClient() {
146    if (!client_)
147      return;
148
149    if (load_result_ == SUCCESS)
150      client_->OnIconLoadSuccess(raw_icon_, icon_);
151    else
152      client_->OnIconLoadFailure();
153  }
154
155  void ReportResultOnUIThread() {
156    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
157
158    NotifyClient();
159    delete this;
160  }
161
162  // ImageDecoder::Delegate overrides:
163  virtual void OnImageDecoded(const ImageDecoder* decoder,
164                              const SkBitmap& decoded_image) OVERRIDE {
165    icon_ = gfx::ImageSkia::CreateFrom1xBitmap(decoded_image);
166    icon_.MakeThreadSafe();
167    ReportResultOnBlockingPool(SUCCESS);
168  }
169
170  virtual void OnDecodeImageFailed(const ImageDecoder* decoder) OVERRIDE {
171    ReportResultOnBlockingPool(FAILED_TO_DECODE);
172  }
173
174  base::WeakPtr<KioskAppData> client_;
175  base::FilePath icon_path_;
176
177  LoadResult load_result_;
178  scoped_refptr<base::SequencedTaskRunner> task_runner_;
179
180  gfx::ImageSkia icon_;
181  scoped_refptr<base::RefCountedString> raw_icon_;
182
183  DISALLOW_COPY_AND_ASSIGN(IconLoader);
184};
185
186////////////////////////////////////////////////////////////////////////////////
187// KioskAppData::WebstoreDataParser
188// Use WebstoreInstallHelper to parse the manifest and decode the icon.
189
190class KioskAppData::WebstoreDataParser
191    : public extensions::WebstoreInstallHelper::Delegate {
192 public:
193  explicit WebstoreDataParser(const base::WeakPtr<KioskAppData>& client)
194      : client_(client) {}
195
196  void Start(const std::string& app_id,
197             const std::string& manifest,
198             const GURL& icon_url,
199             net::URLRequestContextGetter* context_getter) {
200    scoped_refptr<extensions::WebstoreInstallHelper> webstore_helper =
201        new extensions::WebstoreInstallHelper(this,
202                                              app_id,
203                                              manifest,
204                                              "",  // No icon data.
205                                              icon_url,
206                                              context_getter);
207    webstore_helper->Start();
208  }
209
210 private:
211  friend class base::RefCounted<WebstoreDataParser>;
212
213  virtual ~WebstoreDataParser() {}
214
215  void ReportFailure() {
216    if (client_)
217      client_->OnWebstoreParseFailure();
218
219    delete this;
220  }
221
222  // WebstoreInstallHelper::Delegate overrides:
223  virtual void OnWebstoreParseSuccess(
224      const std::string& id,
225      const SkBitmap& icon,
226      base::DictionaryValue* parsed_manifest) OVERRIDE {
227    // Takes ownership of |parsed_manifest|.
228    extensions::Manifest manifest(
229        extensions::Manifest::INVALID_LOCATION,
230        scoped_ptr<base::DictionaryValue>(parsed_manifest));
231
232    if (!IsValidKioskAppManifest(manifest)) {
233      ReportFailure();
234      return;
235    }
236
237    if (client_)
238      client_->OnWebstoreParseSuccess(icon);
239    delete this;
240  }
241  virtual void OnWebstoreParseFailure(
242      const std::string& id,
243      InstallHelperResultCode result_code,
244      const std::string& error_message) OVERRIDE {
245    ReportFailure();
246  }
247
248  base::WeakPtr<KioskAppData> client_;
249
250  DISALLOW_COPY_AND_ASSIGN(WebstoreDataParser);
251};
252
253////////////////////////////////////////////////////////////////////////////////
254// KioskAppData
255
256KioskAppData::KioskAppData(KioskAppDataDelegate* delegate,
257                           const std::string& app_id,
258                           const std::string& user_id)
259    : delegate_(delegate),
260      status_(STATUS_INIT),
261      app_id_(app_id),
262      user_id_(user_id) {
263}
264
265KioskAppData::~KioskAppData() {}
266
267void KioskAppData::Load() {
268  SetStatus(STATUS_LOADING);
269
270  if (LoadFromCache())
271    return;
272
273  StartFetch();
274}
275
276void KioskAppData::ClearCache() {
277  PrefService* local_state = g_browser_process->local_state();
278
279  DictionaryPrefUpdate dict_update(local_state,
280                                   KioskAppManager::kKioskDictionaryName);
281
282  std::string app_key = std::string(KioskAppManager::kKeyApps) + '.' + app_id_;
283  dict_update->Remove(app_key, NULL);
284
285  if (!icon_path_.empty()) {
286    BrowserThread::PostBlockingPoolTask(
287        FROM_HERE,
288        base::Bind(base::IgnoreResult(&base::DeleteFile), icon_path_, false));
289  }
290}
291
292void KioskAppData::LoadFromInstalledApp(Profile* profile,
293                                        const extensions::Extension* app) {
294  SetStatus(STATUS_LOADING);
295
296  if (!app) {
297    app = extensions::ExtensionSystem::Get(profile)
298              ->extension_service()
299              ->GetInstalledExtension(app_id_);
300  }
301
302  DCHECK_EQ(app_id_, app->id());
303
304  name_ = app->name();
305
306  const int kIconSize = extension_misc::EXTENSION_ICON_LARGE;
307  extensions::ExtensionResource image = extensions::IconsInfo::GetIconResource(
308      app, kIconSize, ExtensionIconSet::MATCH_BIGGER);
309  extensions::ImageLoader::Get(profile)->LoadImageAsync(
310      app, image, gfx::Size(kIconSize, kIconSize),
311      base::Bind(&KioskAppData::OnExtensionIconLoaded, AsWeakPtr()));
312}
313
314bool KioskAppData::IsLoading() const {
315  return status_ == STATUS_LOADING;
316}
317
318void KioskAppData::SetStatus(Status status) {
319  if (status_ == status)
320    return;
321
322  status_ = status;
323
324  if (!delegate_)
325    return;
326
327  switch (status_) {
328    case STATUS_INIT:
329      break;
330    case STATUS_LOADING:
331    case STATUS_LOADED:
332      delegate_->OnKioskAppDataChanged(app_id_);
333      break;
334    case STATUS_ERROR:
335      delegate_->OnKioskAppDataLoadFailure(app_id_);
336      break;
337  };
338}
339
340net::URLRequestContextGetter* KioskAppData::GetRequestContextGetter() {
341  return g_browser_process->system_request_context();
342}
343
344bool KioskAppData::LoadFromCache() {
345  std::string app_key = std::string(KioskAppManager::kKeyApps) + '.' + app_id_;
346  std::string name_key = app_key + '.' + kKeyName;
347  std::string icon_path_key = app_key + '.' + kKeyIcon;
348
349  PrefService* local_state = g_browser_process->local_state();
350  const base::DictionaryValue* dict =
351      local_state->GetDictionary(KioskAppManager::kKioskDictionaryName);
352
353  icon_path_.clear();
354  std::string icon_path_string;
355  if (!dict->GetString(name_key, &name_) ||
356      !dict->GetString(icon_path_key, &icon_path_string)) {
357    return false;
358  }
359  icon_path_ = base::FilePath(icon_path_string);
360
361  // IconLoader deletes itself when done.
362  (new IconLoader(AsWeakPtr(), icon_path_))->Start();
363  return true;
364}
365
366void KioskAppData::SetCache(const std::string& name,
367                            const base::FilePath& icon_path) {
368  std::string app_key = std::string(KioskAppManager::kKeyApps) + '.' + app_id_;
369  std::string name_key = app_key + '.' + kKeyName;
370  std::string icon_path_key = app_key + '.' + kKeyIcon;
371
372  PrefService* local_state = g_browser_process->local_state();
373  DictionaryPrefUpdate dict_update(local_state,
374                                   KioskAppManager::kKioskDictionaryName);
375  dict_update->SetString(name_key, name);
376  dict_update->SetString(icon_path_key, icon_path.value());
377  icon_path_ = icon_path;
378}
379
380void KioskAppData::SetCache(const std::string& name, const SkBitmap& icon) {
381  icon_ = gfx::ImageSkia::CreateFrom1xBitmap(icon);
382  icon_.MakeThreadSafe();
383
384  std::vector<unsigned char> image_data;
385  CHECK(gfx::PNGCodec::EncodeBGRASkBitmap(icon, false, &image_data));
386  raw_icon_ = new base::RefCountedString;
387  raw_icon_->data().assign(image_data.begin(), image_data.end());
388
389  base::FilePath cache_dir;
390  if (delegate_)
391    delegate_->GetKioskAppIconCacheDir(&cache_dir);
392
393  base::FilePath icon_path =
394      cache_dir.AppendASCII(app_id_).AddExtension(kIconFileExtension);
395  BrowserThread::GetBlockingPool()->PostTask(
396      FROM_HERE,
397      base::Bind(&SaveIconToLocalOnBlockingPool, icon_path, raw_icon_));
398
399  SetCache(name, icon_path);
400}
401
402void KioskAppData::OnExtensionIconLoaded(const gfx::Image& icon) {
403  if (icon.IsEmpty()) {
404    LOG(WARNING) << "Failed to load icon from installed app"
405                 << ", id=" << app_id_;
406    SetCache(name_, *extensions::IconsInfo::GetDefaultAppIcon().bitmap());
407  } else {
408    SetCache(name_, icon.AsBitmap());
409  }
410
411  SetStatus(STATUS_LOADED);
412}
413
414void KioskAppData::OnIconLoadSuccess(
415    const scoped_refptr<base::RefCountedString>& raw_icon,
416    const gfx::ImageSkia& icon) {
417  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
418  raw_icon_ = raw_icon;
419  icon_ = icon;
420  SetStatus(STATUS_LOADED);
421}
422
423void KioskAppData::OnIconLoadFailure() {
424  // Re-fetch data from web store when failed to load cached data.
425  StartFetch();
426}
427
428void KioskAppData::OnWebstoreParseSuccess(const SkBitmap& icon) {
429  SetCache(name_, icon);
430  SetStatus(STATUS_LOADED);
431}
432
433void KioskAppData::OnWebstoreParseFailure() {
434  SetStatus(STATUS_ERROR);
435}
436
437void KioskAppData::StartFetch() {
438  webstore_fetcher_.reset(new extensions::WebstoreDataFetcher(
439      this,
440      GetRequestContextGetter(),
441      GURL(),
442      app_id_));
443  webstore_fetcher_->Start();
444}
445
446void KioskAppData::OnWebstoreRequestFailure() {
447  SetStatus(STATUS_ERROR);
448}
449
450void KioskAppData::OnWebstoreResponseParseSuccess(
451      scoped_ptr<base::DictionaryValue> webstore_data) {
452  // Takes ownership of |webstore_data|.
453  webstore_fetcher_.reset();
454
455  std::string manifest;
456  if (!CheckResponseKeyValue(webstore_data.get(), kManifestKey, &manifest))
457    return;
458
459  if (!CheckResponseKeyValue(webstore_data.get(), kLocalizedNameKey, &name_))
460    return;
461
462  std::string icon_url_string;
463  if (!CheckResponseKeyValue(webstore_data.get(), kIconUrlKey,
464                             &icon_url_string))
465    return;
466
467  GURL icon_url = GURL(extension_urls::GetWebstoreLaunchURL()).Resolve(
468      icon_url_string);
469  if (!icon_url.is_valid()) {
470    LOG(ERROR) << "Webstore response error (icon url): "
471               << ValueToString(webstore_data.get());
472    OnWebstoreResponseParseFailure(kInvalidWebstoreResponseError);
473    return;
474  }
475
476  // WebstoreDataParser deletes itself when done.
477  (new WebstoreDataParser(AsWeakPtr()))->Start(app_id_,
478                                               manifest,
479                                               icon_url,
480                                               GetRequestContextGetter());
481}
482
483void KioskAppData::OnWebstoreResponseParseFailure(const std::string& error) {
484  LOG(ERROR) << "Webstore failed for kiosk app " << app_id_
485             << ", " << error;
486  webstore_fetcher_.reset();
487  SetStatus(STATUS_ERROR);
488}
489
490bool KioskAppData::CheckResponseKeyValue(const base::DictionaryValue* response,
491                                         const char* key,
492                                         std::string* value) {
493  if (!response->GetString(key, value)) {
494    LOG(ERROR) << "Webstore response error (" << key
495               << "): " << ValueToString(response);
496    OnWebstoreResponseParseFailure(kInvalidWebstoreResponseError);
497    return false;
498  }
499  return true;
500}
501
502}  // namespace chromeos
503