0

personalization: allow selecting local image

When a user selects a local image file, display it as the wallpaper.
Call into WallpaperController->SetCustomWallpaper with the selected
image.

BUG=b/191086436

Cq-Include-Trybots: luci.chrome.try:linux-chromeos-chrome
Change-Id: I22c66eabc0ca0483fc0966d01ec3870f87159c1a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2964369
Commit-Queue: Jeffrey Young <cowmoo@chromium.org>
Reviewed-by: Xiaohui Chen <xiaohuic@chromium.org>
Reviewed-by: Sam McNally <sammc@chromium.org>
Cr-Commit-Position: refs/heads/master@{#894350}
This commit is contained in:
Jeffrey Young
2021-06-21 19:31:43 +00:00
committed by Chromium LUCI CQ
parent 3408c982f9
commit a040384d98
22 changed files with 454 additions and 89 deletions

@ -45,6 +45,24 @@ class ASH_PUBLIC_EXPORT WallpaperController {
const base::FilePath& custom_wallpapers,
const base::FilePath& device_policy_wallpaper) = 0;
// Sets wallpaper from a local file and updates the saved wallpaper info for
// the user.
// |account_id|: The user's account id.
// |wallpaper_files_id|: The file id for |account_id|.
// |file_path|: The path of the image file to read.
// |layout|: The layout of the wallpaper, used for wallpaper resizing.
// |preview_mode|: If true, show the wallpaper immediately but doesn't change
// the user wallpaper info until |ConfirmPreviewWallpaper| is
// called.
// |callback|: called when the image is read from file and decoded.
using SetCustomWallpaperCallback = base::OnceCallback<void(bool success)>;
virtual void SetCustomWallpaper(const AccountId& account_id,
const std::string& wallpaper_files_id,
const base::FilePath& file_path,
WallpaperLayout layout,
bool preview_mode,
SetCustomWallpaperCallback callback) = 0;
// Sets wallpaper from a local file and updates the saved wallpaper info for
// the user.
// |account_id|: The user's account id.

@ -16,6 +16,7 @@
#include "ash/public/cpp/ash_pref_names.h"
#include "ash/public/cpp/image_downloader.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/wallpaper_controller.h"
#include "ash/public/cpp/wallpaper_controller_client.h"
#include "ash/public/cpp/wallpaper_controller_observer.h"
#include "ash/public/cpp/wallpaper_types.h"
@ -289,22 +290,6 @@ ColorProfileType GetColorProfileType(ColorProfile color_profile) {
return ColorProfileType::DARK_MUTED;
}
// If |read_is_successful| is true, start decoding the image, which will run
// |callback| upon completion; if it's false, run |callback| directly with an
// empty image.
void OnWallpaperDataRead(LoadedCallback callback,
std::unique_ptr<std::string> data,
bool read_is_successful) {
if (!read_is_successful) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), gfx::ImageSkia()));
return;
}
// This image was once encoded to JPEG by |ResizeAndEncodeImage|.
DecodeWallpaper(*data, data_decoder::mojom::ImageCodec::kDefault,
std::move(callback));
}
// Deletes a list of wallpaper files in |file_list|.
void DeleteWallpaperInList(std::vector<base::FilePath> file_list) {
for (const base::FilePath& path : file_list) {
@ -984,6 +969,20 @@ void WallpaperControllerImpl::Init(
SetDevicePolicyWallpaperPath(device_policy_wallpaper_path);
}
void WallpaperControllerImpl::SetCustomWallpaper(
const AccountId& account_id,
const std::string& wallpaper_files_id,
const base::FilePath& file_path,
WallpaperLayout layout,
bool preview_mode,
SetCustomWallpaperCallback callback) {
ReadAndDecodeWallpaper(
base::BindOnce(&WallpaperControllerImpl::OnCustomWallpaperDecoded,
weak_factory_.GetWeakPtr(), account_id, wallpaper_files_id,
file_path, layout, preview_mode, std::move(callback)),
sequenced_task_runner_, file_path);
}
void WallpaperControllerImpl::SetCustomWallpaper(
const AccountId& account_id,
const std::string& wallpaper_files_id,
@ -1083,7 +1082,7 @@ void WallpaperControllerImpl::SetOnlineWallpaperFromData(
}
const OnlineWallpaperParams params = {account_id, url, layout, preview_mode};
LoadedCallback decoded_callback =
DecodeImageCallback decoded_callback =
base::BindOnce(&WallpaperControllerImpl::OnOnlineWallpaperDecoded,
weak_factory_.GetWeakPtr(), params, /*save_file=*/true,
std::move(callback));
@ -1092,12 +1091,7 @@ void WallpaperControllerImpl::SetOnlineWallpaperFromData(
.Run(CreateSolidColorWallpaper(kDefaultWallpaperColor));
return;
}
// Use default codec because 1) online wallpapers may have various formats,
// 2) the image data comes from the Chrome OS wallpaper picker and is
// trusted (third-party wallpaper apps use |SetThirdPartyWallpaper|), 3) the
// code path is never used on login screen (enforced by the check above).
DecodeWallpaper(image_data, data_decoder::mojom::ImageCodec::kDefault,
std::move(decoded_callback));
DecodeImageData(std::move(decoded_callback), image_data);
}
void WallpaperControllerImpl::SetDefaultWallpaper(
@ -1142,7 +1136,7 @@ void WallpaperControllerImpl::SetPolicyWallpaper(
// Updates the screen only when the user with this account_id has logged in.
const bool show_wallpaper = IsActiveUser(account_id);
LoadedCallback callback = base::BindOnce(
DecodeImageCallback callback = base::BindOnce(
&WallpaperControllerImpl::SaveAndSetWallpaper, weak_factory_.GetWeakPtr(),
account_id, wallpaper_files_id, kPolicyWallpaperFile, POLICY,
WALLPAPER_LAYOUT_CENTER_CROPPED, show_wallpaper);
@ -1151,8 +1145,7 @@ void WallpaperControllerImpl::SetPolicyWallpaper(
std::move(callback).Run(CreateSolidColorWallpaper(kDefaultWallpaperColor));
return;
}
DecodeWallpaper(data, data_decoder::mojom::ImageCodec::kDefault,
std::move(callback));
DecodeImageData(std::move(callback), data);
}
void WallpaperControllerImpl::SetDevicePolicyWallpaperPath(
@ -1832,7 +1825,7 @@ bool WallpaperControllerImpl::WallpaperIsAlreadyLoaded(
}
void WallpaperControllerImpl::ReadAndDecodeWallpaper(
LoadedCallback callback,
DecodeImageCallback callback,
scoped_refptr<base::SequencedTaskRunner> task_runner,
const base::FilePath& file_path) {
decode_requests_for_testing_.push_back(file_path);
@ -1840,12 +1833,7 @@ void WallpaperControllerImpl::ReadAndDecodeWallpaper(
std::move(callback).Run(CreateSolidColorWallpaper(kDefaultWallpaperColor));
return;
}
std::string* data = new std::string;
base::PostTaskAndReplyWithResult(
task_runner.get(), FROM_HERE,
base::BindOnce(&base::ReadFileToString, file_path, data),
base::BindOnce(&OnWallpaperDataRead, std::move(callback),
base::WrapUnique(data)));
DecodeImageFile(std::move(callback), file_path);
}
bool WallpaperControllerImpl::InitializeUserWallpaperInfo(
@ -2083,6 +2071,22 @@ void WallpaperControllerImpl::SaveAndSetWallpaperWithCompletion(
CustomWallpaperElement(wallpaper_path, image);
}
void WallpaperControllerImpl::OnCustomWallpaperDecoded(
const AccountId& account_id,
const std::string& wallpaper_files_id,
const base::FilePath& path,
WallpaperLayout layout,
bool preview_mode,
SetCustomWallpaperCallback callback,
const gfx::ImageSkia& image) {
bool success = !image.isNull();
if (success) {
SetCustomWallpaper(account_id, wallpaper_files_id, path.BaseName().value(),
layout, image, preview_mode);
}
std::move(callback).Run(success);
}
void WallpaperControllerImpl::OnWallpaperDecoded(const AccountId& account_id,
const base::FilePath& path,
const WallpaperInfo& info,

@ -20,6 +20,7 @@
#include "ash/public/cpp/wallpaper_types.h"
#include "ash/shell_observer.h"
#include "ash/wallpaper/wallpaper_utils/wallpaper_color_calculator_observer.h"
#include "ash/wallpaper/wallpaper_utils/wallpaper_decoder.h"
#include "ash/wallpaper/wallpaper_utils/wallpaper_resizer_observer.h"
#include "ash/wm/overview/overview_observer.h"
#include "base/files/file_path.h"
@ -59,8 +60,6 @@ class WallpaperWindowStateManager;
using CustomWallpaperElement = std::pair<base::FilePath, gfx::ImageSkia>;
using CustomWallpaperMap = std::map<AccountId, CustomWallpaperElement>;
using LoadedCallback = base::OnceCallback<void(const gfx::ImageSkia& image)>;
// Controls the desktop background wallpaper:
// - Sets a wallpaper image and layout;
// - Handles display change (add/remove display, configuration change etc);
@ -233,6 +232,12 @@ class ASH_EXPORT WallpaperControllerImpl
const base::FilePath& wallpapers,
const base::FilePath& custom_wallpapers,
const base::FilePath& device_policy_wallpaper) override;
void SetCustomWallpaper(const AccountId& account_id,
const std::string& wallpaper_files_id,
const base::FilePath& file_path,
WallpaperLayout layout,
bool preview_mode,
SetCustomWallpaperCallback callback) override;
void SetCustomWallpaper(const AccountId& account_id,
const std::string& wallpaper_files_id,
const std::string& file_name,
@ -427,7 +432,7 @@ class ASH_EXPORT WallpaperControllerImpl
// Reads image from |file_path| on disk, and calls |OnWallpaperDataRead|
// with the result of |ReadFileToString|.
void ReadAndDecodeWallpaper(
LoadedCallback callback,
DecodeImageCallback callback,
scoped_refptr<base::SequencedTaskRunner> task_runner,
const base::FilePath& file_path);
@ -493,10 +498,18 @@ class ASH_EXPORT WallpaperControllerImpl
const gfx::ImageSkia& image,
base::OnceCallback<void(const base::FilePath&)> image_saved_callback);
void OnCustomWallpaperDecoded(const AccountId& account_id,
const std::string& wallpaper_files_id,
const base::FilePath& path,
WallpaperLayout layout,
bool preview_mode,
SetCustomWallpaperCallback callback,
const gfx::ImageSkia& image);
// Used as the callback of wallpaper decoding. (Wallpapers of type ONLINE,
// DEFAULT and DEVICE should use their corresponding |*Decoded|, and all other
// types should use this.) Shows the wallpaper immediately if |show_wallpaper|
// is true. Otherwise, only updates the cache.
// DEFAULT, CUSTOM, and DEVICE should use their corresponding |*Decoded|,
// and all other types should use this.) Shows the wallpaper immediately if
// |show_wallpaper| is true. Otherwise, only updates the cache.
void OnWallpaperDecoded(const AccountId& account_id,
const base::FilePath& path,
const WallpaperInfo& info,

@ -1,44 +1,71 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/wallpaper/wallpaper_utils/wallpaper_decoder.h"
#include "ash/shell.h"
#include "ash/shell_delegate.h"
#include <string>
#include "base/bind.h"
#include "base/callback.h"
#include "base/files/file_util.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "ipc/ipc_channel.h"
#include "services/data_decoder/public/cpp/decode_image.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/image/image_skia.h"
namespace ash {
namespace {
const int64_t kMaxImageSizeInBytes =
static_cast<int64_t>(IPC::Channel::kMaximumMessageSize);
void ConvertToImageSkia(OnWallpaperDecoded callback, const SkBitmap& image) {
if (image.isNull()) {
std::string ReadFileToString(const base::FilePath& path) {
std::string result;
if (!base::ReadFileToString(path, &result)) {
LOG(WARNING) << "Failed reading file";
result.clear();
}
return result;
}
void ToImageSkia(DecodeImageCallback callback, const SkBitmap& bitmap) {
if (bitmap.empty()) {
LOG(WARNING) << "Failed to decode image";
std::move(callback).Run(gfx::ImageSkia());
return;
}
SkBitmap final_image = image;
final_image.setImmutable();
gfx::ImageSkia image_skia = gfx::ImageSkia::CreateFrom1xBitmap(final_image);
image_skia.MakeThreadSafe();
std::move(callback).Run(image_skia);
gfx::ImageSkia image = gfx::ImageSkia::CreateFrom1xBitmap(bitmap);
std::move(callback).Run(image);
}
} // namespace
void DecodeWallpaper(const std::string& image_data,
const data_decoder::mojom::ImageCodec& image_codec,
OnWallpaperDecoded callback) {
std::vector<uint8_t> image_bytes(image_data.begin(), image_data.end());
void DecodeImageFile(DecodeImageCallback callback,
const base::FilePath& file_path) {
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE,
{base::MayBlock(), base::TaskPriority::USER_VISIBLE,
base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
base::BindOnce(&ReadFileToString, file_path),
base::BindOnce(&DecodeImageData, std::move(callback)));
}
void DecodeImageData(DecodeImageCallback callback, const std::string& data) {
if (data.empty()) {
std::move(callback).Run(gfx::ImageSkia());
return;
}
data_decoder::DecodeImageIsolated(
std::move(image_bytes), image_codec, /*shrink_to_fit=*/true,
kMaxImageSizeInBytes, /*desired_image_frame_size=*/gfx::Size(),
base::BindOnce(&ConvertToImageSkia, std::move(callback)));
std::vector<uint8_t>(data.begin(), data.end()),
data_decoder::mojom::ImageCodec::kDefault,
/*shrink_to_fit=*/false, kMaxImageSizeInBytes,
/*desired_image_frame_size=*/gfx::Size(),
base::BindOnce(&ToImageSkia, std::move(callback)));
}
} // namespace ash

@ -1,13 +1,17 @@
// Copyright 2018 The Chromium Authors. All rights reserved.
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_WALLPAPER_WALLPAPER_UTILS_WALLPAPER_DECODER_H_
#define ASH_WALLPAPER_WALLPAPER_UTILS_WALLPAPER_DECODER_H_
#include <string>
#include "base/callback_forward.h"
#include "base/memory/scoped_refptr.h"
#include "services/data_decoder/public/cpp/decode_image.h"
namespace base {
class FilePath;
} // namespace base
namespace gfx {
class ImageSkia;
@ -15,14 +19,12 @@ class ImageSkia;
namespace ash {
using OnWallpaperDecoded =
base::OnceCallback<void(const gfx::ImageSkia& image)>;
using DecodeImageCallback = base::OnceCallback<void(const gfx::ImageSkia&)>;
// Do an async wallpaper decode; |on_decoded| is run on the calling thread when
// the decode has finished.
void DecodeWallpaper(const std::string& image_data,
const data_decoder::mojom::ImageCodec& image_codec,
OnWallpaperDecoded callback);
void DecodeImageFile(DecodeImageCallback callback,
const base::FilePath& file_path);
void DecodeImageData(DecodeImageCallback callback, const std::string& data);
} // namespace ash

@ -12,6 +12,7 @@
#include <string>
#include <vector>
#include "ash/public/cpp/wallpaper_controller.h"
#include "ash/public/cpp/wallpaper_info.h"
#include "ash/public/cpp/wallpaper_types.h"
#include "base/bind.h"
@ -237,6 +238,31 @@ void ChromePersonalizationAppUiDelegate::SelectWallpaper(
/*preview_mode=*/false, std::move(callback));
}
void ChromePersonalizationAppUiDelegate::SelectLocalImage(
const base::UnguessableToken& id,
SelectLocalImageCallback callback) {
const auto& it = local_image_id_map_.find(id);
if (it == local_image_id_map_.end()) {
mojo::ReportBadMessage("Invalid local image id selected");
return;
}
const user_manager::User* user =
chromeos::ProfileHelper::Get()->GetUserByProfile(profile_);
DCHECK(user);
auto* controller = ash::WallpaperController::Get();
auto* client = WallpaperControllerClientImpl::Get();
const auto& account_id = user->GetAccountId();
controller->SetCustomWallpaper(
account_id, client->GetFilesId(account_id), it->second,
ash::WallpaperLayout::WALLPAPER_LAYOUT_CENTER_CROPPED,
/*preview_mode=*/false, std::move(callback));
}
void ChromePersonalizationAppUiDelegate::OnFetchCollections(
FetchCollectionsCallback callback,
bool success,

@ -77,6 +77,9 @@ class ChromePersonalizationAppUiDelegate : public PersonalizationAppUiDelegate {
void SelectWallpaper(uint64_t image_asset_id,
SelectWallpaperCallback callback) override;
void SelectLocalImage(const base::UnguessableToken& id,
SelectLocalImageCallback callback) override;
private:
mojo::Receiver<chromeos::personalization_app::mojom::WallpaperProvider>
wallpaper_receiver_{this};

@ -38,6 +38,17 @@ void TestWallpaperController::Init(
NOTIMPLEMENTED();
}
void TestWallpaperController::SetCustomWallpaper(
const AccountId& account_id,
const std::string& wallpaper_files_id,
const base::FilePath& file_path,
ash::WallpaperLayout layout,
bool preview_mode,
SetCustomWallpaperCallback callback) {
++set_custom_wallpaper_count_;
std::move(callback).Run(true);
}
void TestWallpaperController::SetCustomWallpaper(
const AccountId& account_id,
const std::string& wallpaper_files_id,

@ -45,6 +45,12 @@ class TestWallpaperController : public ash::WallpaperController {
const base::FilePath& wallpapers,
const base::FilePath& custom_wallpapers,
const base::FilePath& device_policy_wallpaper) override;
void SetCustomWallpaper(const AccountId& account_id,
const std::string& wallpaper_files_id,
const base::FilePath& file_path,
ash::WallpaperLayout layout,
bool preview_mode,
SetCustomWallpaperCallback callback) override;
void SetCustomWallpaper(const AccountId& account_id,
const std::string& wallpaper_files_id,
const std::string& file_name,

@ -13,6 +13,7 @@ js_type_check("closure_compile") {
"js_module_root=./gen/chrome/test/data/webui/",
]
deps = [
":local_images_element_test",
":personalization_app_test_utils",
":personalization_app_unified_test",
":test_mojo_interface_provider",
@ -23,6 +24,18 @@ js_type_check("closure_compile") {
]
}
js_library("local_images_element_test") {
deps = [
":personalization_app_test_utils",
":test_mojo_interface_provider",
":test_personalization_store",
"../..:chai_assert",
"../..:test_util.m",
"//chromeos/components/personalization_app/resources/trusted:local_images_element",
]
externs_list = [ "$externs_path/mocha-2.5.js" ]
}
js_library("personalization_app_test_utils") {
deps = [
":test_mojo_interface_provider",
@ -36,6 +49,7 @@ js_library("personalization_app_test_utils") {
js_library("personalization_app_unified_test") {
deps = [
":local_images_element_test",
":wallpaper_collections_element_test",
":wallpaper_images_element_test",
":wallpaper_selected_element_test",

@ -0,0 +1,118 @@
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {LocalImages} from 'chrome://personalization/trusted/local_images_element.js';
import {assertEquals, assertFalse, assertTrue} from '../../chai_assert.js';
import {flushTasks, waitAfterNextRender} from '../../test_util.m.js';
import {baseSetup, initElement, teardownElement} from './personalization_app_test_utils.js';
import {TestWallpaperProvider} from './test_mojo_interface_provider.js';
import {TestPersonalizationStore} from './test_personalization_store.js';
export function LocalImagesTest() {
/** @type {?HTMLElement} */
let localImagesElement = null;
/** @type {?TestWallpaperProvider} */
let wallpaperProvider = null;
/** @type {?TestPersonalizationStore} */
let personalizationStore = null;
setup(() => {
const mocks = baseSetup();
wallpaperProvider = mocks.wallpaperProvider;
personalizationStore = mocks.personalizationStore;
});
teardown(async () => {
await teardownElement(localImagesElement);
localImagesElement = null;
await flushTasks();
});
test(
'displays loading spinner while local image list is loading',
async () => {
personalizationStore.data.loading.local = {images: true, data: {}};
localImagesElement = initElement(LocalImages.is, {hidden: false});
const spinner =
localImagesElement.shadowRoot.querySelector('paper-spinner-lite');
assertTrue(spinner.active);
personalizationStore.data.loading.local = {images: false, data: {}};
personalizationStore.notifyObservers();
await waitAfterNextRender(localImagesElement);
assertFalse(spinner.active);
});
test(
'displays images for current local images that have successfully loaded',
async () => {
personalizationStore.data.local = {
images: wallpaperProvider.localImages,
data: wallpaperProvider.localImageData,
};
personalizationStore.data.loading.local = {images: false, data: {}};
localImagesElement = initElement(LocalImages.is, {hidden: false});
const ironList =
localImagesElement.shadowRoot.querySelector('iron-list');
assertTrue(!!ironList);
// Both items are sent. No images are rendered yet because they are not
// done loading thumbnails.
assertEquals(2, ironList.items.length);
assertEquals(0, ironList.shadowRoot.querySelectorAll('img').length);
// Set loading finished for first thumbnail.
personalizationStore.data.loading.local.data = {'100,10': false};
personalizationStore.notifyObservers();
await waitAfterNextRender(localImagesElement);
assertEquals(2, ironList.items.length);
let imgTags = localImagesElement.shadowRoot.querySelectorAll('img');
assertEquals(1, imgTags.length);
assertEquals('data://localimage0data', imgTags[0].src);
// Set loading failed for second thumbnail.
personalizationStore.data.loading.local.data = {
'100,10': false,
'200,20': false
};
personalizationStore.data.local.data = {
'100,10': 'data://localimage0data',
'200,20': null
};
personalizationStore.notifyObservers();
await waitAfterNextRender(localImagesElement);
// Still only first thumbnail displayed.
imgTags = localImagesElement.shadowRoot.querySelectorAll('img');
assertEquals(1, imgTags.length);
assertEquals('data://localimage0data', imgTags[0].src);
});
test('displays error on loading failure', async () => {
personalizationStore.data.local = {images: null, data: {}};
personalizationStore.data.loading.local = {images: false, data: {}};
localImagesElement = initElement(LocalImages.is, {hidden: false});
// Spinner is not active because loading is finished.
const spinner =
localImagesElement.shadowRoot.querySelector('paper-spinner-lite');
assertFalse(spinner.active);
// Error should be visible.
const error = localImagesElement.shadowRoot.querySelector('#error');
assertFalse(error.hidden);
// No iron-list displayed.
const ironList = localImagesElement.shadowRoot.querySelector('iron-list');
assertFalse(!!ironList);
});
}

@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {LocalImagesTest} from './local_images_element_test.js';
import {WallpaperBreadcrumbTest} from './wallpaper_breadcrumb_element_test.js';
import {WallpaperCollectionsTest} from './wallpaper_collections_element_test.js';
import {WallpaperImagesTest} from './wallpaper_images_element_test.js';
@ -12,6 +13,7 @@ import {WallpaperSelectedTest} from './wallpaper_selected_element_test.js';
window.console.warn = () => {};
const testCases = [
LocalImagesTest,
WallpaperBreadcrumbTest,
WallpaperCollectionsTest,
WallpaperImagesTest,

@ -71,8 +71,8 @@ export class TestWallpaperProvider extends TestBrowserProxy {
/** @type {!Object<string, string>} */
this.localImageData = {
'100,10': 'localimage0data',
'200,20': 'localimage1data',
'100,10': 'data://localimage0data',
'200,20': 'data://localimage1data',
};
/**
@ -83,6 +83,8 @@ export class TestWallpaperProvider extends TestBrowserProxy {
/** @public */
this.selectWallpaperResponse = true;
this.selectLocalImageResponse = true;
}
/**
@ -140,6 +142,12 @@ export class TestWallpaperProvider extends TestBrowserProxy {
return Promise.resolve({success: this.selectWallpaperResponse});
}
/** @override */
selectLocalImage(id) {
this.methodCalled('selectLocalImage', id);
return Promise.resolve({success: this.selectLocalImageResponse});
}
/**
* @param {!Array<!chromeos.personalizationApp.mojom.WallpaperCollection>}
* collections

@ -32,7 +32,8 @@ struct WallpaperImage {
};
struct LocalImage {
// Id to be used for fetching the image thumbnail data.
// Unique ID for this image for fetching thumbnail data and setting as user
// background.
mojo_base.mojom.UnguessableToken id;
// Name of this local asset to be displayed to the user.
@ -67,6 +68,9 @@ interface WallpaperProvider {
// on failure.
GetCurrentWallpaper() => (WallpaperImage? image);
// Sets the given wallpaper as the user's background.
// Sets the given backdrop wallpaper as the user's background.
SelectWallpaper(uint64 image_asset_id) => (bool success);
// Sets the given local wallpaper as the user's background.
SelectLocalImage(mojo_base.mojom.UnguessableToken id) => (bool success);
};

@ -44,3 +44,22 @@ export function promisifyOnload(element, id, afterNextRender) {
export function unguessableTokenToString({high, low}) {
return `${high},${low}`;
}
/**
* @param {!string} str
* @return {!mojoBase.mojom.UnguessableToken}
*/
export function stringToUnguessableToken(str) {
const [high, low] = str.split(',').map(BigInt);
return {high, low};
}
/**
* @param {!mojoBase.mojom.UnguessableToken} a
* @param {!mojoBase.mojom.UnguessableToken} b
* @return {boolean}
*/
export function unguessableTokensEqual(a, b) {
return a.hasOwnProperty('high') && a.high === b.high &&
a.hasOwnProperty('low') && a.low === b.low;
}

@ -1,14 +1,20 @@
<style include="trusted-style common-style"></style>
<paper-spinner-lite active="[[imagesLoading_]]">
</paper-spinner-lite>
<iron-list items="[[getImages_(hidden, images_)]]" grid>
<template>
<div class="photo-container">
<template is="dom-if"
<!-- TODO(b/181697575) handle error cases and update error string to UI spec -->
<p hidden$="[[!hasError_]]" id="error">error</p>
<template is="dom-if" if="[[showImages_]]">
<iron-list items="[[getImages_(hidden, images_)]]" grid>
<template>
<div class="photo-container">
<template is="dom-if"
if="[[shouldShowImage_(item, imageData_, imageDataLoading_)]]">
<img src="[[getImageData_(item, imageData_)]]">
</template>
<p class="image-name">[[item.name]]</p>
</div>
</template>
</iron-list>
<div on-click="onClickImage_" data-id$="[[getImageKey_(item)]]">
<img src="[[getImageData_(item, imageData_)]]">
</div>
</template>
<p class="image-name">[[item.name]]</p>
</div>
</template>
</iron-list>
</template>

@ -13,8 +13,11 @@ import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import './styles.js';
import '../common/styles.js';
import {assert} from '/assert.m.js';
import {html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {unguessableTokenToString} from '../common/utils.js';
import {isNonEmptyArray, stringToUnguessableToken, unguessableTokensEqual, unguessableTokenToString} from '../common/utils.js';
import {getWallpaperProvider} from './mojo_interface_provider.js';
import {selectWallpaper} from './personalization_controller.js';
import {WithPersonalizationStore} from './personalization_store.js';
/** @polymer */
@ -65,6 +68,18 @@ export class LocalImages extends WithPersonalizationStore {
imageDataLoading_: {
type: Object,
},
/** @private */
hasError_: {
type: Boolean,
computed: 'computeHasError_(imagesLoading_, images_)',
},
/** @private */
showImages_: {
type: Boolean,
computed: 'computeShowImages_(imagesLoading_, images_)'
}
};
}
@ -78,6 +93,26 @@ export class LocalImages extends WithPersonalizationStore {
this.updateFromStore();
}
/**
* @private
* @param {boolean} imagesLoading
* @param {?Array<!chromeos.personalizationApp.mojom.LocalImage>} images
* @return {boolean}
*/
computeHasError_(imagesLoading, images) {
return !imagesLoading && !isNonEmptyArray(images);
}
/**
* @private
* @param {boolean} imagesLoading
* @param {?Array<!chromeos.personalizationApp.mojom.LocalImage>} images
* @return {boolean}
*/
computeShowImages_(imagesLoading, images) {
return !imagesLoading && isNonEmptyArray(images);
}
/**
* Forces iron-list to re-evaluate when hidden changes.
* @private
@ -114,6 +149,29 @@ export class LocalImages extends WithPersonalizationStore {
const key = unguessableTokenToString(image.id);
return imageData[key];
}
/**
* @private
* @param {!chromeos.personalizationApp.mojom.LocalImage} image
* @return {string}
*/
getImageKey_(image) {
return unguessableTokenToString(image.id);
}
/**
* @private
* @param {!Event} event
*/
onClickImage_(event) {
const id = stringToUnguessableToken(event.currentTarget.dataset.id);
const image =
this.images_.find(image => unguessableTokensEqual(id, image.id));
assert(!!image, 'Image with that id not found');
selectWallpaper(
/** @type {!chromeos.personalizationApp.mojom.LocalImage} */ (image),
getWallpaperProvider(), this.getStore());
}
}
customElements.define(LocalImages.is, LocalImages);

@ -4,6 +4,7 @@
import {Action} from 'chrome://resources/js/cr/ui/store.m.js';
import {unguessableTokenToString} from '../common/utils.js';
import {DisplayableImage} from './personalization_reducers.js';
/**
* @fileoverview Defines the actions to change state.
@ -49,7 +50,7 @@ export function beginLoadLocalImageDataAction(image) {
/**
* Notify that a user has clicked on an image to set as wallpaper.
* @param {!chromeos.personalizationApp.mojom.WallpaperImage} image
* @param {!DisplayableImage} image
* @return {!Action}
*/
export function beginSelectImageAction(image) {
@ -114,7 +115,7 @@ export function setLocalImagesAction(images) {
* Returns an action to set the current image as currently selected across the
* app. Can be called with null to represent no image currently selected or that
* an error occurred.
* @param {?chromeos.personalizationApp.mojom.WallpaperImage} image
* @param {?DisplayableImage} image
* @return {!Action}
*/
export function setSelectedImageAction(image) {

@ -102,7 +102,8 @@ export async function getCurrentWallpaper(provider, store) {
}
/**
* @param {!chromeos.personalizationApp.mojom.WallpaperImage} image
* @param {!chromeos.personalizationApp.mojom.WallpaperImage |
* !chromeos.personalizationApp.mojom.LocalImage} image
* @param {!chromeos.personalizationApp.mojom.WallpaperProviderInterface}
* provider
* @param {!PersonalizationStore} store
@ -110,7 +111,16 @@ export async function getCurrentWallpaper(provider, store) {
export async function selectWallpaper(image, provider, store) {
const oldImage = store.data.selected;
store.dispatch(beginSelectImageAction(image));
const {success} = await provider.selectWallpaper(image.assetId);
const {success} = await (() => {
if (image.assetId) {
return provider.selectWallpaper(image.assetId);
} else if (image.id) {
return provider.selectLocalImage(image.id);
} else {
console.warn('Image must be a LocalImage or a WallpaperImage');
return {success: false};
}
})();
if (!success) {
console.warn('Error setting wallpaper');
}

@ -11,6 +11,12 @@
import {Action} from 'chrome://resources/js/cr/ui/store.m.js';
import {ActionName} from './personalization_actions.js';
/**
* @typedef {chromeos.personalizationApp.mojom.LocalImage|
* chromeos.personalizationApp.mojom.WallpaperImage}
*/
export let DisplayableImage;
/**
* Stores collections and images from backdrop server.
* |images| is a mapping of collection id to the list of images.
@ -61,7 +67,7 @@ export let LocalState;
* backdrop: !BackdropState,
* loading: !LoadingState,
* local: !LocalState,
* selected: ?chromeos.personalizationApp.mojom.WallpaperImage,
* selected: ?DisplayableImage,
* }}
*/
export let PersonalizationState;
@ -217,9 +223,9 @@ function localReducer(state, action) {
}
/**
* @param {?chromeos.personalizationApp.mojom.WallpaperImage} state
* @param {?DisplayableImage} state
* @param {!Action} action
* @return {?chromeos.personalizationApp.mojom.WallpaperImage}
* @return {?DisplayableImage}
*/
function selectedReducer(state, action) {
switch (action.name) {

@ -73,3 +73,9 @@ void FakePersonalizationAppUiDelegate::SelectWallpaper(
SelectWallpaperCallback callback) {
std::move(callback).Run(/*success=*/true);
}
void FakePersonalizationAppUiDelegate::SelectLocalImage(
const base::UnguessableToken& token,
SelectLocalImageCallback callback) {
std::move(callback).Run(/*success=*/true);
}

@ -50,6 +50,9 @@ class FakePersonalizationAppUiDelegate : public PersonalizationAppUiDelegate {
void SelectWallpaper(uint64_t image_asset_id,
SelectWallpaperCallback callback) override;
void SelectLocalImage(const base::UnguessableToken& token,
SelectLocalImageCallback callback) override;
private:
mojo::Receiver<chromeos::personalization_app::mojom::WallpaperProvider>
wallpaper_receiver_{this};