Skip to content

Commit

Permalink
apphome: support install locally
Browse files Browse the repository at this point in the history
- Add install locally as a menu item to the app home frontend.
- Add `is_locally_installed` field to `AppInfo`. This controls a set of
  menu items visibility, including `open_in_window`, so
  `may_show_open_in_window` is not needed anymore.
- Listens to `OnWebAppInstallLocallyWithOsHooks` and update frontend.

Bug: 1350406
Change-Id: I9c2aca4af65da865a1d09ff570210358b715b820
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4193146
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Reviewed-by: Dibyajyoti Pal <dibyapal@chromium.org>
Reviewed-by: Camden King <camdenking@google.com>
Commit-Queue: Phillis Tang <phillis@chromium.org>
Reviewed-by: Mustafa Emre Acer <meacer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1097019}
  • Loading branch information
philloooo authored and Chromium LUCI CQ committed Jan 25, 2023
1 parent 7488ce9 commit ac40cb4
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 25 deletions.
4 changes: 4 additions & 0 deletions chrome/app/generated_resources.grd
Expand Up @@ -5908,6 +5908,10 @@ Keep your key file in a safe place. You will need it to create new versions of y
Create shortcut
</message>

<message name="IDS_APP_HOME_INSTALL_LOCALLY" desc="Menu entry label for installing the app on this device from context menu on App Home">
Install on this device
</message>

<message name="IDS_APP_HOME_UNINSTALL_APP" desc="Menu entry label for uninstalling an app from context menu on App Home">
Uninstall
</message>
Expand Down
@@ -0,0 +1 @@
6bc27550b41495ff6b48e18599d57e9b4f9d6d4d
2 changes: 1 addition & 1 deletion chrome/browser/resources/app_home/app_item.html
Expand Up @@ -32,6 +32,6 @@
</style>

<div class="icon-container">
<img src="[[appInfo.iconUrl.url]]" class="icon-image" alt="App's icon"></img>
<img src="[[getIconUrl_(appInfo)]]" class="icon-image" alt="App's icon"></img>
</div>
<div class="text-container">[[appInfo.name]]</div>
11 changes: 11 additions & 0 deletions chrome/browser/resources/app_home/app_item.ts
Expand Up @@ -43,6 +43,17 @@ export class AppItemElement extends PolymerElement {
this.dispatchEvent(
new CustomEvent(eventName, {bubbles: true, composed: true, detail}));
}

private getIconUrl_() {
const url = new URL(this.appInfo.iconUrl.url);
// For web app, the backend serves grayscale image when the app is not
// locally installed automatically and doesn't recognize this query param,
// but we add a query param here to force browser to refetch the image.
if (!this.appInfo.isLocallyInstalled) {
url.searchParams.append('grayscale', 'true');
}
return url;
}
}

declare global {
Expand Down
13 changes: 10 additions & 3 deletions chrome/browser/resources/app_home/app_list.html
Expand Up @@ -41,7 +41,7 @@
<cr-action-menu id="menu" hidden="{{!selectedActionMenuModel_}}">
<div id="open-in-window" tabindex="0" class="dropdown-item"
on-click="onOpenInWindowItemClick_"
hidden="[[isOpenInWindowHidden_(selectedActionMenuModel_)]]">
hidden="[[!isLocallyInstalled_(selectedActionMenuModel_)]]">
<div class="dropdown-item-label">
$i18n{appWindowOpenLabel}
</div>
Expand All @@ -65,16 +65,23 @@
</cr-checkbox>
</div>
<button id="create-shortcut" class="dropdown-item"
on-click="onCreateShortcutItemClick_">
on-click="onCreateShortcutItemClick_"
hidden="[[!isLocallyInstalled_(selectedActionMenuModel_)]]">
$i18n{createShortcutForAppLabel}
</button>
<button id="install-locally" class="dropdown-item"
on-click="onInstallLocallyItemClick_"
hidden="[[isLocallyInstalled_(selectedActionMenuModel_)]]">
$i18n{installLocallyLabel}
</button>
<hr>
<button id="uninstall" class="dropdown-item"
on-click="onUninstallItemClick_">
$i18n{uninstallAppLabel}
</button>
<button id="app-settings" class="dropdown-item"
on-click="onAppSettingsItemClick_">
on-click="onAppSettingsItemClick_"
hidden="[[!isLocallyInstalled_(selectedActionMenuModel_)]]">
$i18n{appSettingsLabel}
</button>
</cr-action-menu>
14 changes: 11 additions & 3 deletions chrome/browser/resources/app_home/app_list.ts
Expand Up @@ -111,10 +111,10 @@ export class AppListElement extends PolymerElement {
}
}

private isOpenInWindowHidden_() {
private isLocallyInstalled_() {
return this.selectedActionMenuModel_ ?
!this.selectedActionMenuModel_.appInfo.mayShowOpenInWindow :
true;
this.selectedActionMenuModel_.appInfo.isLocallyInstalled :
false;
}

private isLaunchOnStartupHidden_() {
Expand Down Expand Up @@ -173,6 +173,14 @@ export class AppListElement extends PolymerElement {
this.closeMenu_();
}

private onInstallLocallyItemClick_() {
if (this.selectedActionMenuModel_?.appInfo.id) {
BrowserProxy.getInstance().handler.installAppLocally(
this.selectedActionMenuModel_?.appInfo.id);
}
this.closeMenu_();
}

private onUninstallItemClick_() {
if (this.selectedActionMenuModel_?.appInfo.id) {
BrowserProxy.getInstance().handler.uninstallApp(
Expand Down
4 changes: 2 additions & 2 deletions chrome/browser/ui/webui/app_home/app_home.mojom
Expand Up @@ -37,8 +37,8 @@ struct AppInfo {
// The app's `RunOnOsLoginMode`, including `RunOnOsLoginModeNotRun` and
// `RunOnOsLoginModeWindowed`.
RunOnOsLoginMode run_on_os_login_mode;
// Whether to show `open_in_window` menu item.
bool may_show_open_in_window;
// Whether the app is installed locally.
bool is_locally_installed;
// Whether the app open in a app window or as a browser tab.
bool open_in_window;

Expand Down
13 changes: 11 additions & 2 deletions chrome/browser/ui/webui/app_home/app_home_page_handler.cc
Expand Up @@ -31,6 +31,7 @@
#include "chrome/browser/ui/web_applications/web_app_ui_manager_impl.h"
#include "chrome/browser/ui/webui/extensions/extension_icon_source.h"
#include "chrome/browser/web_applications/extension_status_utils.h"
#include "chrome/browser/web_applications/extensions/bookmark_app_util.h"
#include "chrome/browser/web_applications/locks/app_lock.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/web_app.h"
Expand Down Expand Up @@ -332,7 +333,7 @@ app_home::mojom::AppInfoPtr AppHomePageHandler::CreateAppInfoPtrFromWebApp(
app_info->may_toggle_run_on_os_login_mode = login_mode.user_controllable;
app_info->run_on_os_login_mode = login_mode.value;

app_info->may_show_open_in_window = is_locally_installed;
app_info->is_locally_installed = is_locally_installed;
// Treat all other types of display mode as "open as window".
app_info->open_in_window = registrar.GetAppEffectiveDisplayMode(app_id) !=
blink::mojom::DisplayMode::kBrowser;
Expand All @@ -357,7 +358,10 @@ app_home::mojom::AppInfoPtr AppHomePageHandler::CreateAppInfoPtrFromExtension(
app_info->may_show_run_on_os_login_mode = false;
app_info->may_toggle_run_on_os_login_mode = false;

app_info->may_show_open_in_window = false;
app_info->is_locally_installed =
!extension->is_hosted_app() ||
extensions::BookmarkAppIsLocallyInstalled(extension_service_->profile(),
extension);
return app_info;
}

Expand Down Expand Up @@ -564,6 +568,11 @@ void AppHomePageHandler::OnWebAppUserDisplayModeChanged(
page_->AddApp(CreateAppInfoPtrFromWebApp(app_id));
}

void AppHomePageHandler::OnWebAppInstalledWithOsHooks(
const web_app::AppId& app_id) {
page_->AddApp(CreateAppInfoPtrFromWebApp(app_id));
}

void AppHomePageHandler::OnAppRegistrarDestroyed() {
web_app_registrar_observation_.Reset();
}
Expand Down
5 changes: 5 additions & 0 deletions chrome/browser/ui/webui/app_home/app_home_page_handler.h
Expand Up @@ -60,7 +60,12 @@ class AppHomePageHandler
~AppHomePageHandler() override;

// web_app::WebAppInstallManagerObserver:
// Listens to both `OnWebAppInstalled` and `OnWebAppInstalledWithOsHooks` as
// some type of installs, e.g. sync install only trigger `OnWebAppInstalled`.
// `OnWebAppInstalledWithOsHooks` also gets fired when an installed app gets
// locally installed.
void OnWebAppInstalled(const web_app::AppId& app_id) override;
void OnWebAppInstalledWithOsHooks(const web_app::AppId& app_id) override;
void OnWebAppWillBeUninstalled(const web_app::AppId& app_id) override;
void OnWebAppInstallManagerDestroyed() override;

Expand Down
1 change: 1 addition & 0 deletions chrome/browser/ui/webui/app_home/app_home_ui.cc
Expand Up @@ -28,6 +28,7 @@ void AddAppHomeLocalizedStrings(content::WebUIDataSource* ui_source) {
{"appLaunchAtStartupCheckboxLabel",
IDS_ACCNAME_APP_HOME_LAUNCH_AT_STARTUP_CHECKBOX},
{"createShortcutForAppLabel", IDS_APP_HOME_CREATE_SHORTCUT},
{"installLocallyLabel", IDS_APP_HOME_INSTALL_LOCALLY},
{"uninstallAppLabel", IDS_APP_HOME_UNINSTALL_APP},
{"appSettingsLabel", IDS_APP_HOME_APP_SETTINGS},
};
Expand Down
112 changes: 100 additions & 12 deletions chrome/test/data/webui/app_home/app_list_test.ts
Expand Up @@ -40,7 +40,7 @@ suite('AppListTest', () => {
mayShowRunOnOsLoginMode: true,
mayToggleRunOnOsLoginMode: false,
runOnOsLoginMode: RunOnOsLoginMode.kNotRun,
mayShowOpenInWindow: true,
isLocallyInstalled: true,
openInWindow: false,
},
{
Expand All @@ -54,7 +54,7 @@ suite('AppListTest', () => {
mayShowRunOnOsLoginMode: false,
mayToggleRunOnOsLoginMode: false,
runOnOsLoginMode: RunOnOsLoginMode.kNotRun,
mayShowOpenInWindow: false,
isLocallyInstalled: false,
openInWindow: false,
},
],
Expand All @@ -70,7 +70,7 @@ suite('AppListTest', () => {
mayShowRunOnOsLoginMode: false,
mayToggleRunOnOsLoginMode: false,
runOnOsLoginMode: RunOnOsLoginMode.kNotRun,
mayShowOpenInWindow: false,
isLocallyInstalled: true,
openInWindow: false,
};
testBrowserProxy = new TestAppHomeBrowserProxy(apps);
Expand Down Expand Up @@ -104,7 +104,7 @@ suite('AppListTest', () => {
assertEquals(
appItems[1]!.shadowRoot!
.querySelector<HTMLImageElement>('.icon-container img')!.src,
apps.appList[1]!.iconUrl.url);
apps.appList[1]!.iconUrl.url + '?grayscale=true');
});

test('add/remove app', async () => {
Expand All @@ -131,7 +131,7 @@ suite('AppListTest', () => {
testAppInfo.name));
});

test('context menu', () => {
test('context menu locally installed', () => {
// Get the first app item.
const appItem = appListElement.shadowRoot!.querySelector('app-item');
assertTrue(!!appItem);
Expand All @@ -150,7 +150,7 @@ suite('AppListTest', () => {
const openInWindow =
contextMenu.querySelector<HTMLElement>('#open-in-window');
assertTrue(!!openInWindow);
assertEquals(openInWindow.hidden, !appInfo.mayShowOpenInWindow);
assertEquals(openInWindow.hidden, !appInfo.isLocallyInstalled);
assertEquals(
openInWindow.querySelector('cr-checkbox')!.checked,
appInfo.openInWindow);
Expand All @@ -170,6 +170,42 @@ suite('AppListTest', () => {
assertTrue(!!contextMenu.querySelector('#create-shortcut'));
assertTrue(!!contextMenu.querySelector('#uninstall'));
assertTrue(!!contextMenu.querySelector('#app-settings'));
assertTrue(!!contextMenu.querySelector('#install-locally'));

assertFalse(
contextMenu.querySelector<HTMLElement>('#create-shortcut')!.hidden);
assertFalse(contextMenu.querySelector<HTMLElement>('#uninstall')!.hidden);
assertFalse(
contextMenu.querySelector<HTMLElement>('#app-settings')!.hidden);
assertTrue(
contextMenu.querySelector<HTMLElement>('#install-locally')!.hidden);
});

test('context menu not locally installed', () => {
// Get the second app item that's not locally installed.
const appList = appListElement.shadowRoot!.querySelectorAll('app-item');
assertEquals(appList.length, 2);
const appItem = appList[1];
assertTrue(!!appItem);

const contextMenu =
appListElement.shadowRoot!.querySelector('cr-action-menu');
assertTrue(!!contextMenu);
assertTrue(contextMenu.hidden);

appItem.dispatchEvent(new CustomEvent('contextmenu'));
assertFalse(contextMenu.hidden);

assertTrue(
contextMenu.querySelector<HTMLElement>('#open-in-window')!.hidden);
assertTrue(
contextMenu.querySelector<HTMLElement>('#launch-on-startup')!.hidden);
assertTrue(
contextMenu.querySelector<HTMLElement>('#create-shortcut')!.hidden);
assertTrue(contextMenu.querySelector<HTMLElement>('#app-settings')!.hidden);
assertFalse(contextMenu.querySelector<HTMLElement>('#uninstall')!.hidden);
assertFalse(
contextMenu.querySelector<HTMLElement>('#install-locally')!.hidden);
});

test('toggle open in window', () => {
Expand Down Expand Up @@ -239,7 +275,7 @@ suite('AppListTest', () => {
assertEquals(appInfo.runOnOsLoginMode, RunOnOsLoginMode.kWindowed);
});

test('click uninstall', () => {
test('click uninstall', async () => {
const appItem = appListElement.shadowRoot!.querySelector('app-item');
assertTrue(!!appItem);

Expand All @@ -250,11 +286,11 @@ suite('AppListTest', () => {
assertTrue(!!uninstall);

uninstall.click();
testBrowserProxy.fakeHandler.whenCalled('uninstallApp')
await testBrowserProxy.fakeHandler.whenCalled('uninstallApp')
.then((appId: string) => assertEquals(appId, apps.appList[0]!.id));
});

test('click app settings', () => {
test('click app settings', async () => {
const appItem = appListElement.shadowRoot!.querySelector('app-item');
assertTrue(!!appItem);

Expand All @@ -265,11 +301,11 @@ suite('AppListTest', () => {
assertTrue(!!appSettings);

appSettings.click();
testBrowserProxy.fakeHandler.whenCalled('showAppSettings')
await testBrowserProxy.fakeHandler.whenCalled('showAppSettings')
.then((appId: string) => assertEquals(appId, apps.appList[0]!.id));
});

test('click create shortcut', () => {
test('click create shortcut', async () => {
const appItem = appListElement.shadowRoot!.querySelector('app-item');
assertTrue(!!appItem);

Expand All @@ -281,8 +317,60 @@ suite('AppListTest', () => {
assertTrue(!!createShortcut);

createShortcut.click();
testBrowserProxy.fakeHandler.whenCalled('createAppShortcut')
await testBrowserProxy.fakeHandler.whenCalled('createAppShortcut')
.then((appId: string) => assertEquals(appId, apps.appList[0]!.id));
});

test('click install locally', async () => {
const appItem = appListElement.shadowRoot!.querySelectorAll('app-item')[1];
assertTrue(!!appItem);

assertEquals(
appItem.shadowRoot!
.querySelector<HTMLImageElement>('.icon-container img')!.src,
apps.appList[1]!.iconUrl.url + '?grayscale=true');

appItem.dispatchEvent(new CustomEvent('contextmenu'));

const contextMenu =
appListElement.shadowRoot!.querySelector('cr-action-menu');
assertTrue(!!contextMenu);

assertTrue(
contextMenu.querySelector<HTMLElement>('#open-in-window')!.hidden);
assertTrue(
contextMenu.querySelector<HTMLElement>('#create-shortcut')!.hidden);
assertTrue(contextMenu.querySelector<HTMLElement>('#app-settings')!.hidden);
assertFalse(contextMenu.querySelector<HTMLElement>('#uninstall')!.hidden);

const installLocally =
appListElement.shadowRoot!.querySelector<HTMLElement>(
'#install-locally');
assertTrue(!!installLocally);
assertFalse(installLocally.hidden);

installLocally.click();
await testBrowserProxy.fakeHandler.whenCalled('installAppLocally')
.then((appId: string) => assertEquals(appId, apps.appList[1]!.id));

await callbackRouterRemote.$.flushForTesting();
flush();
assertEquals(
appItem.shadowRoot!
.querySelector<HTMLImageElement>('.icon-container img')!.src,
apps.appList[1]!.iconUrl.url);

appItem.dispatchEvent(new CustomEvent('contextmenu'));

assertFalse(
contextMenu.querySelector<HTMLElement>('#open-in-window')!.hidden);
assertFalse(
contextMenu.querySelector<HTMLElement>('#create-shortcut')!.hidden);
assertFalse(
contextMenu.querySelector<HTMLElement>('#app-settings')!.hidden);
assertFalse(contextMenu.querySelector<HTMLElement>('#uninstall')!.hidden);
assertTrue(
contextMenu.querySelector<HTMLElement>('#install-locally')!.hidden);
});

});

0 comments on commit ac40cb4

Please sign in to comment.