Commit 095d5ef2 authored by bshe@chromium.org's avatar bshe@chromium.org

Make wallpaper picker manifest and thumbnails available when offline.

This CL did the following:

1. Save/update manifest in chrome.storage.local when user successfully
requested latest manifest from server. If user failed to get manifest
from server(offline or server error), fallback to the manifest saved
in chrome.storage.local last time.

2. Lazily saves all requested thumbnails to thumbnails directory. And
load from that directory when user open wallpaper picker next time. Note
that thumbnails are shared across user session. So after one user saved
thumbnails, all other users will directly use the thumbnails that saved
in thumbnail directory.


BUG=158668


Review URL: https://chromiumcodereview.appspot.com/11348215

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@171011 0039d316-1c4b-4281-b951-d872f2087c98
parent c4d1d311
......@@ -13,6 +13,7 @@
#include "base/json/json_writer.h"
#include "base/memory/scoped_ptr.h"
#include "base/path_service.h"
#include "base/stringprintf.h"
#include "base/synchronization/cancellation_flag.h"
#include "base/threading/sequenced_worker_pool.h"
#include "base/threading/worker_pool.h"
......@@ -59,6 +60,39 @@ ash::WallpaperLayout GetLayoutEnum(const std::string& layout) {
return ash::WALLPAPER_LAYOUT_CENTER;
}
// Saves |data| as |file_name| to directory with |key|. Return false if the
// directory can not be found/created or failed to write file.
bool SaveData(int key, const std::string& file_name, const std::string& data) {
FilePath data_dir;
CHECK(PathService::Get(key, &data_dir));
if (!file_util::DirectoryExists(data_dir) &&
!file_util::CreateDirectory(data_dir)) {
return false;
}
FilePath file_path = data_dir.Append(file_name);
return file_util::PathExists(file_path) ||
(file_util::WriteFile(file_path, data.c_str(),
data.size()) != -1);
}
// Gets |file_name| from directory with |key|. Return false if the directory can
// not be found or failed to read file to |data|. If the |file_name| can not be
// found in the directory, return true with empty |data|. It is expected that we
// may try to access file which did not saved yet.
bool GetData(int key, const std::string& file_name, std::string* data) {
FilePath data_dir;
CHECK(PathService::Get(key, &data_dir));
if (!file_util::DirectoryExists(data_dir) &&
!file_util::CreateDirectory(data_dir))
return false;
FilePath file_path = data_dir.Append(file_name);
return !file_util::PathExists(file_path) ||
(file_util::ReadFileToString(file_path, data) != -1);
}
class WindowStateManager;
// static
......@@ -256,16 +290,15 @@ WallpaperSetWallpaperFunction::~WallpaperSetWallpaperFunction() {
bool WallpaperSetWallpaperFunction::RunImpl() {
BinaryValue* input = NULL;
if (args_ == NULL || !args_->GetBinary(0, &input)) {
return false;
}
EXTENSION_FUNCTION_VALIDATE(args_->GetBinary(0, &input));
std::string layout_string;
if (!args_->GetString(1, &layout_string) || layout_string.empty()) {
return false;
}
EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &layout_string));
EXTENSION_FUNCTION_VALIDATE(!layout_string.empty());
layout_ = GetLayoutEnum(layout_string);
if (!args_->GetString(2, &url_) || url_.empty())
return false;
EXTENSION_FUNCTION_VALIDATE(args_->GetString(2, &url_));
EXTENSION_FUNCTION_VALIDATE(!url_.empty());
// Gets email address while at UI thread.
email_ = chromeos::UserManager::Get()->GetLoggedInUser()->email();
......@@ -293,29 +326,14 @@ void WallpaperSetWallpaperFunction::OnWallpaperDecoded(
base::SequencedWorkerPool::BLOCK_SHUTDOWN);
task_runner->PostTask(FROM_HERE,
base::Bind(&WallpaperSetWallpaperFunction::SaveToFile,
this));
base::Bind(&WallpaperSetWallpaperFunction::SaveToFile, this));
}
void WallpaperSetWallpaperFunction::SaveToFile() {
DCHECK(BrowserThread::GetBlockingPool()->IsRunningSequenceOnCurrentThread(
sequence_token_));
FilePath wallpaper_dir;
CHECK(PathService::Get(chrome::DIR_CHROMEOS_WALLPAPERS, &wallpaper_dir));
if (!file_util::DirectoryExists(wallpaper_dir) &&
!file_util::CreateDirectory(wallpaper_dir)) {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&WallpaperSetWallpaperFunction::OnFailureOrCancel,
this, ""));
LOG(ERROR) << "Failed to create wallpaper directory.";
return;
}
std::string file_name = GURL(url_).ExtractFileName();
FilePath file_path = wallpaper_dir.Append(file_name);
if (file_util::PathExists(file_path) ||
file_util::WriteFile(file_path, image_data_.c_str(),
image_data_.size()) != -1 ) {
if (SaveData(chrome::DIR_CHROMEOS_WALLPAPERS, file_name, image_data_)) {
wallpaper_.EnsureRepsForSupportedScaleFactors();
scoped_ptr<gfx::ImageSkia> deep_copy(wallpaper_.DeepCopy());
// ImageSkia is not RefCountedThreadSafe. Use a deep copied ImageSkia if
......@@ -326,20 +344,27 @@ void WallpaperSetWallpaperFunction::SaveToFile() {
this, base::Passed(&deep_copy)));
chromeos::UserImage wallpaper(wallpaper_);
FilePath wallpaper_dir;
CHECK(PathService::Get(chrome::DIR_CHROMEOS_WALLPAPERS, &wallpaper_dir));
FilePath file_path = wallpaper_dir.Append(file_name).InsertBeforeExtension(
chromeos::kSmallWallpaperSuffix);
if (file_util::PathExists(file_path))
return;
// Generates and saves small resolution wallpaper. Uses CENTER_CROPPED to
// maintain the aspect ratio after resize.
chromeos::WallpaperManager::Get()->ResizeAndSaveWallpaper(
wallpaper,
file_path.InsertBeforeExtension(chromeos::kSmallWallpaperSuffix),
file_path,
ash::WALLPAPER_LAYOUT_CENTER_CROPPED,
ash::kSmallWallpaperMaxWidth,
ash::kSmallWallpaperMaxHeight);
} else {
std::string error = base::StringPrintf(
"Failed to create/write wallpaper to %s.", file_name.c_str());
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&WallpaperSetWallpaperFunction::OnFailureOrCancel,
this, ""));
LOG(ERROR) << "Failed to save downloaded wallpaper.";
this, error));
}
}
......@@ -368,13 +393,11 @@ WallpaperSetCustomWallpaperFunction::~WallpaperSetCustomWallpaperFunction() {
bool WallpaperSetCustomWallpaperFunction::RunImpl() {
BinaryValue* input = NULL;
if (args_ == NULL || !args_->GetBinary(0, &input)) {
return false;
}
EXTENSION_FUNCTION_VALIDATE(args_->GetBinary(0, &input));
std::string layout_string;
if (!args_->GetString(1, &layout_string) || layout_string.empty()) {
return false;
}
EXTENSION_FUNCTION_VALIDATE(args_->GetString(1, &layout_string));
EXTENSION_FUNCTION_VALIDATE(!layout_string.empty());
layout_ = GetLayoutEnum(layout_string);
// Gets email address while at UI thread.
......@@ -427,3 +450,120 @@ bool WallpaperRestoreMinimizedWindowsFunction::RunImpl() {
WindowStateManager::RestoreWindows();
return true;
}
WallpaperGetThumbnailFunction::WallpaperGetThumbnailFunction() {
}
WallpaperGetThumbnailFunction::~WallpaperGetThumbnailFunction() {
}
bool WallpaperGetThumbnailFunction::RunImpl() {
std::string url;
EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &url));
EXTENSION_FUNCTION_VALIDATE(!url.empty());
std::string file_name = GURL(url).ExtractFileName();
sequence_token_ = BrowserThread::GetBlockingPool()->
GetNamedSequenceToken(chromeos::kWallpaperSequenceTokenName);
scoped_refptr<base::SequencedTaskRunner> task_runner =
BrowserThread::GetBlockingPool()->
GetSequencedTaskRunnerWithShutdownBehavior(sequence_token_,
base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN);
task_runner->PostTask(FROM_HERE,
base::Bind(&WallpaperGetThumbnailFunction::Get, this, file_name));
return true;
}
void WallpaperGetThumbnailFunction::Failure(const std::string& file_name) {
SetError(base::StringPrintf("Failed to access wallpaper thumbnails for %s.",
file_name.c_str()));
SendResponse(false);
}
void WallpaperGetThumbnailFunction::FileNotLoaded() {
SendResponse(true);
}
void WallpaperGetThumbnailFunction::FileLoaded(const std::string& data) {
BinaryValue* thumbnail = BinaryValue::CreateWithCopiedBuffer(data.c_str(),
data.size());
SetResult(thumbnail);
SendResponse(true);
}
void WallpaperGetThumbnailFunction::Get(const std::string& file_name) {
DCHECK(BrowserThread::GetBlockingPool()->IsRunningSequenceOnCurrentThread(
sequence_token_));
std::string data;
if (GetData(chrome::DIR_CHROMEOS_WALLPAPER_THUMBNAILS, file_name, &data)) {
if (data.empty()) {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&WallpaperGetThumbnailFunction::FileNotLoaded, this));
} else {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&WallpaperGetThumbnailFunction::FileLoaded, this, data));
}
} else {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&WallpaperGetThumbnailFunction::Failure, this, file_name));
}
}
WallpaperSaveThumbnailFunction::WallpaperSaveThumbnailFunction() {
}
WallpaperSaveThumbnailFunction::~WallpaperSaveThumbnailFunction() {
}
bool WallpaperSaveThumbnailFunction::RunImpl() {
std::string url;
EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &url));
EXTENSION_FUNCTION_VALIDATE(!url.empty());
BinaryValue* input = NULL;
EXTENSION_FUNCTION_VALIDATE(args_->GetBinary(1, &input));
std::string file_name = GURL(url).ExtractFileName();
std::string data(input->GetBuffer(), input->GetSize());
sequence_token_ = BrowserThread::GetBlockingPool()->
GetNamedSequenceToken(chromeos::kWallpaperSequenceTokenName);
scoped_refptr<base::SequencedTaskRunner> task_runner =
BrowserThread::GetBlockingPool()->
GetSequencedTaskRunnerWithShutdownBehavior(sequence_token_,
base::SequencedWorkerPool::CONTINUE_ON_SHUTDOWN);
task_runner->PostTask(FROM_HERE,
base::Bind(&WallpaperSaveThumbnailFunction::Save,
this, data, file_name));
return true;
}
void WallpaperSaveThumbnailFunction::Failure(const std::string& file_name) {
SetError(base::StringPrintf("Failed to create/write thumbnail of %s.",
file_name.c_str()));
SendResponse(false);
}
void WallpaperSaveThumbnailFunction::Success() {
SendResponse(true);
}
void WallpaperSaveThumbnailFunction::Save(const std::string& data,
const std::string& file_name) {
DCHECK(BrowserThread::GetBlockingPool()->IsRunningSequenceOnCurrentThread(
sequence_token_));
if (SaveData(chrome::DIR_CHROMEOS_WALLPAPER_THUMBNAILS, file_name, data)) {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&WallpaperSaveThumbnailFunction::Success, this));
} else {
BrowserThread::PostTask(
BrowserThread::UI, FROM_HERE,
base::Bind(&WallpaperSaveThumbnailFunction::Failure,
this, file_name));
}
}
......@@ -138,4 +138,65 @@ class WallpaperRestoreMinimizedWindowsFunction : public AsyncExtensionFunction {
virtual bool RunImpl() OVERRIDE;
};
class WallpaperGetThumbnailFunction : public AsyncExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION_NAME("wallpaperPrivate.getThumbnail");
WallpaperGetThumbnailFunction();
protected:
virtual ~WallpaperGetThumbnailFunction();
// AsyncExtensionFunction overrides.
virtual bool RunImpl() OVERRIDE;
private:
// Failed to get thumbnail for |file_name|.
void Failure(const std::string& file_name);
// Sets success field in the results to false. Called when the requested
// thumbnail is not found or corrupted in thumbnail directory.
void FileNotLoaded();
// Sets success field to true and data field to the loaded thumbnail binary
// data in the results. Called when requested wallpaper thumbnail loaded
// successfully.
void FileLoaded(const std::string& data);
// Gets thumbnail with |file_name| from thumbnail directory. If |file_name|
// does not exist, call FileNotLoaded().
void Get(const std::string& file_name);
// Sequence token associated with wallpaper operations. Shared with
// WallpaperManager.
base::SequencedWorkerPool::SequenceToken sequence_token_;
};
class WallpaperSaveThumbnailFunction : public AsyncExtensionFunction {
public:
DECLARE_EXTENSION_FUNCTION_NAME("wallpaperPrivate.saveThumbnail");
WallpaperSaveThumbnailFunction();
protected:
virtual ~WallpaperSaveThumbnailFunction();
// AsyncExtensionFunction overrides.
virtual bool RunImpl() OVERRIDE;
private:
// Failed to save thumbnail for |file_name|.
void Failure(const std::string& file_name);
// Saved thumbnail to thumbnail directory.
void Success();
// Saves thumbnail to thumbnail directory as |file_name|.
void Save(const std::string& data, const std::string& file_name);
// Sequence token associated with wallpaper operations. Shared with
// WallpaperManager.
base::SequencedWorkerPool::SequenceToken sequence_token_;
};
#endif // CHROME_BROWSER_CHROMEOS_EXTENSIONS_WALLPAPER_PRIVATE_API_H_
......@@ -407,6 +407,8 @@ void ExtensionFunctionRegistry::ResetFunctions() {
RegisterFunction<WallpaperSetCustomWallpaperFunction>();
RegisterFunction<WallpaperMinimizeInactiveWindowsFunction>();
RegisterFunction<WallpaperRestoreMinimizedWindowsFunction>();
RegisterFunction<WallpaperGetThumbnailFunction>();
RegisterFunction<WallpaperSaveThumbnailFunction>();
// InputMethod
RegisterFunction<extensions::GetInputMethodFunction>();
......
......@@ -32,21 +32,38 @@ cr.define('wallpapers', function() {
decorate: function() {
GridItem.prototype.decorate.call(this);
var imageEl = cr.doc.createElement('img');
var self = this;
chrome.wallpaperPrivate.getThumbnail(this.dataItem.baseURL,
function(data) {
if (data) {
var blob = new Blob([new Int8Array(data)]);
imageEl.src = window.URL.createObjectURL(blob);
imageEl.addEventListener('load', function(e) {
window.URL.revokeObjectURL(this.src);
});
self.appendChild(imageEl);
} else {
var xhr = new XMLHttpRequest();
xhr.open('GET', this.dataItem.baseURL + ThumbnailSuffix, true);
xhr.responseType = 'blob';
xhr.open('GET', self.dataItem.baseURL + ThumbnailSuffix, true);
xhr.responseType = 'arraybuffer';
xhr.send(null);
var self = this;
xhr.addEventListener('load', function(e) {
if (xhr.status === 200) {
self.textContent = '';
imageEl.src = window.URL.createObjectURL(xhr.response);
chrome.wallpaperPrivate.saveThumbnail(self.dataItem.baseURL,
xhr.response);
var blob = new Blob([new Int8Array(xhr.response)]);
imageEl.src = window.URL.createObjectURL(blob);
// TODO(bshe): We currently use empty div to reserve space for
// thumbnail. Use a placeholder like "loading" image may better.
imageEl.addEventListener('load', function(e) {
window.URL.revokeObjectURL(this.src);
});
self.appendChild(imageEl);
}
});
}
});
},
};
......
......@@ -15,6 +15,7 @@
function WallpaperManager(dialogDom) {
this.dialogDom_ = dialogDom;
this.storage_ = chrome.storage.local;
this.document_ = dialogDom.ownerDocument;
this.selectedCategory = null;
this.butterBar_ = new ButterBar(this.dialogDom_);
......@@ -39,6 +40,11 @@ function WallpaperManager(dialogDom) {
*/
/** @const */ var HighResolutionSuffix = '_high_resolution.jpg';
/**
* Key to access wallpaper manifest in chrome.local.storage.
*/
/** @const */ var AccessManifestKey = 'wallpaper-picker-manifest-key';
/**
* Returns a translated string.
*
......@@ -119,9 +125,16 @@ function WallpaperManager(dialogDom) {
}
};
if (navigator.onLine) {
asyncFetchManifestFromUrls(urls, fetchManifestAsync,
this.onLoadManifestSuccess_.bind(this),
this.onLoadManifestFailed_.bind(this));
} else {
// If device is offline, fetches manifest from local storage.
// TODO(bshe): Always loading the offline manifest first and replacing
// with the online one when available.
this.onLoadManifestFailed_();
}
};
/**
......@@ -131,18 +144,22 @@ function WallpaperManager(dialogDom) {
*/
WallpaperManager.prototype.onLoadManifestSuccess_ = function(manifest) {
this.manifest_ = manifest;
var items = {};
items[AccessManifestKey] = manifest;
this.storage_.set(items, function() {});
this.initDom_();
};
// Sets manifest to an empty object and shows connection error. Called after
// manifest failed to load.
// Sets manifest to previously saved object if any and shows connection error.
// Called after manifest failed to load.
WallpaperManager.prototype.onLoadManifestFailed_ = function() {
// TODO(bshe): Fall back to saved manifest if there is a problem fetching
// manifest from server.
this.manifest_ = {};
this.butterBar_.showError_(str('connectionFailed'),
var self = this;
this.storage_.get(AccessManifestKey, function(items) {
self.manifest_ = items[AccessManifestKey] ? items[AccessManifestKey] : {};
self.butterBar_.showError_(str('connectionFailed'),
{help_url: LEARN_MORE_URL});
this.initDom_();
self.initDom_();
});
};
/**
......
......@@ -8,6 +8,7 @@
"manifest_version": 2,
"description": "An experimental wallpaper picker UI",
"permissions": [
"storage",
"wallpaperPrivate",
"https://commondatastorage.googleapis.com/",
"https://storage.googleapis.com/"
......
......@@ -367,6 +367,11 @@ bool PathProvider(int key, FilePath* result) {
return false;
cur = cur.Append(FILE_PATH_LITERAL("wallpapers"));
break;
case chrome::DIR_CHROMEOS_WALLPAPER_THUMBNAILS:
if (!PathService::Get(chrome::DIR_USER_DATA, &cur))
return false;
cur = cur.Append(FILE_PATH_LITERAL("wallpaper_thumbnails"));
break;
case chrome::FILE_DEFAULT_APP_ORDER:
cur = FilePath(FILE_PATH_LITERAL(kDefaultAppOrderFileName));
break;
......
......@@ -101,6 +101,8 @@ enum {
#if defined(OS_CHROMEOS)
DIR_CHROMEOS_WALLPAPERS, // Directory where downloaded chromeos
// wallpapers reside.
DIR_CHROMEOS_WALLPAPER_THUMBNAILS, // Directory where downloaded chromeos
// wallpaper thumbnails reside.
FILE_DEFAULT_APP_ORDER, // Full path to the json file that defines the
// default app order.
#endif
......
......@@ -87,6 +87,57 @@
"description": "Restores all previously minimized windows.",
"nodoc": "true",
"parameters": []
},
{
"name": "getThumbnail",
"type": "function",
"description": "Gets thumbnail of wallpaper from thumbnail directory.",
"nodoc": "true",
"parameters": [
{
"type": "string",
"name": "url",
"description": "Wallpaper url."
},
{
"type": "function",
"name": "callback",
"description": "Function called upon completion.",
"parameters": [
{
"type": "binary",
"name": "data",
"optional": true,
"description": "The binary data of loaded thumbnail."
}
]
}
]
},
{
"name": "saveThumbnail",
"type": "function",
"description": "Saves thumbnail to thumbnail directory.",
"nodoc": "true",
"parameters": [
{
"type": "string",
"name": "url",
"description": "Wallpaper url."
},
{
"type": "binary",
"name": "data",
"description": "The binary data of downloaded thumbnail."
},
{
"type": "function",
"name": "callback",
"description": "Function called upon completion.",
"parameters": [],
"optional": true
}
]
}
]
}
......
......@@ -11,6 +11,20 @@ var fail = chrome.test.callbackFail;
chrome.test.getConfig(function(config) {
var wallpaper;
var wallpaperStrings;
var requestImage = function(url, onLoadCallback) {
var wallpaperRequest = new XMLHttpRequest();
wallpaperRequest.open('GET', url, true);
wallpaperRequest.responseType = 'arraybuffer';
try {
wallpaperRequest.send(null);
wallpaperRequest.onloadend = function(e) {
onLoadCallback(wallpaperRequest.status, wallpaperRequest.response);
};
} catch (e) {
console.error(e);
chrome.test.fail('An error thrown when requesting wallpaper.');
};
};
chrome.test.runTests([
function getWallpaperStrings() {
chrome.wallpaperPrivate.getStrings(pass(function(strings) {
......@@ -18,17 +32,12 @@ chrome.test.getConfig(function(config) {
}));
},
function setOnlineJpegWallpaper() {
var wallpaperRequest = new XMLHttpRequest();
var url = "http://a.com:PORT/files/extensions/api_test" +
"/wallpaper_manager/test.jpg";
url = url.replace(/PORT/, config.testServer.port);
wallpaperRequest.open('GET', url, true);
wallpaperRequest.responseType = 'arraybuffer';
try {
wallpaperRequest.send(null);
wallpaperRequest.onload = function (e) {
if (wallpaperRequest.status === 200) {
wallpaper = wallpaperRequest.response;
requestImage(url, function(requestStatus, response) {
if (requestStatus === 200) {
wallpaper = response;
chrome.wallpaperPrivate.setWallpaper(wallpaper,
'CENTER_CROPPED',
url,
......@@ -36,11 +45,7 @@ chrome.test.getConfig(function(config) {
} else {
chrome.test.fail('Failed to load test.jpg from local server.');
}
};
} catch (e) {
console.error(e);
chrome.test.fail('An error thrown when requesting wallpaper.');
};
});
},
function setCustomJpegWallpaper() {
chrome.wallpaperPrivate.setCustomWallpaper(wallpaper,
......@@ -48,27 +53,37 @@ chrome.test.getConfig(function(config) {
pass());
},
function setCustomJepgBadWallpaper() {
var wallpaperRequest = new XMLHttpRequest();
var url = "http://a.com:PORT/files/extensions/api_test" +
"/wallpaper_manager/test_bad.jpg";
url = url.replace(/PORT/, config.testServer.port);
wallpaperRequest.open('GET', url, true);
wallpaperRequest.responseType = 'arraybuffer';
try {
wallpaperRequest.send(null);
wallpaperRequest.onload = function (e) {
if (wallpaperRequest.status === 200) {
var badWallpaper = wallpaperRequest.response;
requestImage(url, function(requestStatus, response) {
if (requestStatus === 200) {
var badWallpaper = response;
chrome.wallpaperPrivate.setCustomWallpaper(badWallpaper,
'CENTER_CROPPED', fail(wallpaperStrings.invalidWallpaper));
} else {
chrome.test.fail('Failed to load test_bad.jpg from local server.');
}
};
} catch (e) {
console.error(e);
chrome.test.fail('An error thrown when requesting wallpaper.');
};
});
},
function getAndSetThumbnail() {
var url = "http://a.com:PORT/files/extensions/api_test" +
"/wallpaper_manager/test.jpg";
url = url.replace(/PORT/, config.testServer.port);
chrome.wallpaperPrivate.getThumbnail(url, pass(function(data) {
chrome.test.assertNoLastError();
if (data)
chrome.test.fail('Thumbnail is not found. getThumbnail should not ' +
'return any data.');
chrome.wallpaperPrivate.saveThumbnail(url, wallpaper, pass(function() {
chrome.test.assertNoLastError();
chrome.wallpaperPrivate.getThumbnail(url, pass(function(data) {
chrome.test.assertNoLastError();
// Thumbnail should already be saved to thumbnail directory.
chrome.test.assertEq(wallpaper, data);
}));
}));
}));
}
]);
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment