Commit df8d8b5a authored by Regan Hsu's avatar Regan Hsu Committed by Commit Bot

[CrOS PhoneHub] Add notifications to chrome://multidevice-internals.

Allows user to input fake PhoneHub notification data, and for
notifications to eventually display via FakeNotificationManager. User
can send, update, delete, and view a list of notifications.

This CL also cleans up some of the other phonehub forms in
chrome://multidevice-internals.

Screenshots:
https://screenshot.googleplex.com/9XNVYwEeAQUZeWG Disabled Send button
https://screenshot.googleplex.com/4nz5PaMH8bfLbdR Enabled Send button
https://screenshot.googleplex.com/AUsqtxMgJwMwHBs Sent notification
https://screenshot.googleplex.com/5nCmMZW6xcviatK Updated! UI
https://screenshot.googleplex.com/3fnNdKZcFDCWgcL Multiple notifications
https://screenshot.googleplex.com/86y2Mqa2C8AmZSP Unique id & inline id

Bug: 1106937
Change-Id: Ie5aecd70a03b6e84aa8745a2ece421e0d8086970
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2411546
Commit-Queue: Regan Hsu <hsuregan@chromium.org>
Reviewed-by: default avatarKyle Horimoto <khorimoto@chromium.org>
Cr-Commit-Position: refs/heads/master@{#807272}
parent 592d484a
......@@ -15,6 +15,8 @@ js_type_check("closure_compile") {
":multidevice_logs_browser_proxy",
":multidevice_phonehub_browser_proxy",
":multidevice_phonehub_browser_proxy",
":notification_form",
":notification_manager",
":phone_name_form",
":phone_status_model_form",
":phonehub_tab",
......@@ -22,6 +24,20 @@ js_type_check("closure_compile") {
]
}
js_library("notification_manager") {
deps = [
":notification_form",
":types",
]
}
js_library("notification_form") {
deps = [
":multidevice_phonehub_browser_proxy",
":types",
]
}
js_library("browser_tabs_model_form") {
deps = [
":multidevice_phonehub_browser_proxy",
......@@ -96,6 +112,7 @@ js_library("phonehub_tab") {
":browser_tabs_model_form",
":i18n_setup",
":multidevice_phonehub_browser_proxy",
":notification_manager",
":phone_status_model_form",
":types",
"//ui/webui/resources/js:load_time_data.m",
......@@ -119,5 +136,7 @@ html_to_js("web_components") {
"shared_style.js",
"browser_tabs_metadata_form.js",
"browser_tabs_model_form.js",
"notification_form.js",
"notification_manager.js",
]
}
......@@ -10,31 +10,30 @@
</style>
<div class="column">
<div class="label">URL: </div>
<cr-input value="{{url_}}" id="urlInput" auto-validate required
<cr-input value="{{url_}}" id="urlInput" auto-validate required label="URL: "
error-message="This browser tab will be a nullopt if no url input">
</cr-input>
</div>
<div class="column">
<div class="label">Title: </div>
<cr-input value="{{title_}}" id="titleInput" auto-validate required
<cr-input value="{{title_}}" label="Title:" auto-validate required
error-message="This browser tab will be a nullopt if no title input">
</cr-input>
</div>
<div class="column">
<div class="label">Last Accessed (ms): </div>
<cr-input value="{{lastAccessedTimeStamp_}}"
<cr-input value="{{lastAccessedTimeStamp_}}" label="Last Accessed (ms):"
id="lastAccessedTimeStampInput" type="number" min="0"
on-change="onLastAccessTimeStampChanged_"
auto-validate error-message="Must be greater than 0" required>
</cr-input>
</div>
<div class="column" id="faviconSelector">
<div class="label">Favicon selector: </div>
<label>Favicon Image Type</label>
<select id="faviconList" class="md-select"
on-change="onFaviconSelected_">
on-change="onFaviconSelected_">
<template is="dom-repeat" items="[[faviconList_]]">
<option>[[getFaviconTypeName_(item)]]</option>
<option selected="[[isEqual_(item, favicon_)]]">
[[getImageTypeName_(item)]]
</option>
</template>
</select>
</div>
......@@ -8,19 +8,7 @@ import 'chrome://resources/cr_elements/cr_input/cr_input.m.js';
import './shared_style.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {BrowserTabsMetadataModel, FaviconType} from './types.js';
/**
* Maps a FaviconType to its title label in the dropdown.
* @type {!Map<FaviconType, String>}
*/
const faviconTypeToStringMap = new Map([
[FaviconType.PINK, 'Pink'],
[FaviconType.RED, 'Red'],
[FaviconType.GREEN, 'Green'],
[FaviconType.BLUE, 'Blue'],
[FaviconType.YELLOW, 'Yellow'],
]);
import {BrowserTabsMetadataModel, ImageType, imageTypeToStringMap} from './types.js';
Polymer({
is: 'browser-tabs-metadata-form',
......@@ -53,10 +41,10 @@ Polymer({
value: Date.now(),
},
/** @private{FaviconType} */
/** @private{ImageType} */
favicon_: {
type: Number,
value: FaviconType.PINK,
value: ImageType.PINK,
},
/** @private */
......@@ -64,11 +52,12 @@ Polymer({
type: Array,
value: () => {
return [
FaviconType.PINK,
FaviconType.RED,
FaviconType.GREEN,
FaviconType.BLUE,
FaviconType.YELLOW,
ImageType.NONE,
ImageType.PINK,
ImageType.RED,
ImageType.GREEN,
ImageType.BLUE,
ImageType.YELLOW,
];
},
readonly: true,
......@@ -76,12 +65,12 @@ Polymer({
},
/**
* @param {FaviconType} faviconType
* @param {ImageType} faviconType
* @return {String}
* @private
*/
getFaviconTypeName_(faviconType) {
return faviconTypeToStringMap.get(faviconType);
getImageTypeName_(faviconType) {
return imageTypeToStringMap.get(faviconType);
},
/** @private */
......@@ -114,4 +103,14 @@ Polymer({
this.lastAccessedTimeStamp_ = Number(inputValue);
},
/**
* @param {*} lhs
* @param {*} rhs
* @return {boolean}
* @private
*/
isEqual_(lhs, rhs) {
return lhs === rhs;
},
});
......@@ -20,6 +20,14 @@
file="${root_gen_dir}\chrome\browser\resources\chromeos\multidevice_internals\browser_tabs_model_form.js"
use_base_dir="false"
type="BINDATA"/>
<include name="IDR_MULTIDEVICE_INTERNALS_BROWSER_NOTIFICATION_MANAGER_JS"
file="${root_gen_dir}\chrome\browser\resources\chromeos\multidevice_internals\notification_manager.js"
use_base_dir="false"
type="BINDATA"/>
<include name="IDR_MULTIDEVICE_INTERNALS_BROWSER_NOTIFICATION_FORM_JS"
file="${root_gen_dir}\chrome\browser\resources\chromeos\multidevice_internals\notification_form.js"
use_base_dir="false"
type="BINDATA"/>
<include name="IDR_MULTIDEVICE_INTERNALS_INDEX_HTML"
file="index.html"
type="BINDATA"/>
......
......@@ -3,7 +3,7 @@
// found in the LICENSE file.
import {addSingletonGetter} from 'chrome://resources/js/cr.m.js';
import {BrowserTabsModel, FeatureStatus, PhoneStatusModel} from './types.js';
import {BrowserTabsModel, FeatureStatus, Notification, PhoneStatusModel} from './types.js';
/**
* JavaScript hooks into the native WebUI handler for Phonehub tab.
......@@ -50,6 +50,22 @@ export class MultidevicePhoneHubBrowserProxy {
setBrowserTabs(browserTabsModel) {
chrome.send('setBrowserTabs', [browserTabsModel]);
}
/**
* Sets a notification.
* @param {!Notification} notification
*/
setNotification(notification) {
chrome.send('setNotification', [notification]);
}
/**
* Removes a notification with the id |notificationId|
* @param {number} notificationId
*/
removeNotification(notificationId) {
chrome.send('removeNotification', [notificationId]);
}
}
addSingletonGetter(MultidevicePhoneHubBrowserProxy);
<style include="cr-shared-style shared-style">
:host {
display: flex;
flex: 1 0 100%;
padding: 10px;
width: 100%;
}
:host([is-sent_]) #notificationContainer {
background-color: LightGreen;
}
:host(:not([is-sent_])) #notificationContainer {
box-shadow: var(--cr-elevation-3);
}
#notificationContainer {
display: flex;
flex: 1 0 100%;
}
#fields {
flex: 4;
}
.dropdown {
display: flex;
flex-direction: column;
padding: 5px;
}
cr-input {
padding: 5px;
}
</style>
<div id="notificationContainer">
<div class="column">
<cr-button hidden="[[notification.sent]]" id="sendBtn"
disabled="[[!isNotificationDataValid_]]"
on-click="onSetNotification_" class="internals-button">
<span class="emphasize">Send this notification</span>
</cr-button>
<cr-button hidden="[[!notification.sent]]" id="editBtn"
disabled="[[!isNotificationDataValid_]]"
on-click="onUpdateNotification_" class="internals-button">
<span class="emphasize">[[updateNotificationText_]]</span>
</cr-button>
<cr-button hidden="[[!notification.sent]]" id="removeBtn"
on-click="onRemoveButtonClick_" class="internals-button">
<span class="emphasize">Remove this notification</span>
</cr-button>
</div>
<div class="column" id="fields">
<cr-input value="{{notification.id}}" label="notification ID"
type="number" invalid="[[!isValidId_]]"
on-change="onNotificationIdChanged_"
auto-validate error-message="ID already used" required
disabled="[[notification.sent]]">
</cr-input>
<cr-input label="Visible App Name"
value="{{notification.appMetadata.visibleAppName}}" id="urlInput">
</cr-input>
<cr-input label="Package Name"
value="{{notification.appMetadata.packageName}}" id="packageName">
</cr-input>
<div class="dropdown">
<label>Icon Image Type</label>
<select id="iconImageTypeSelector" class="md-select"
on-change="onIconImageTypeSelected_">
<template is="dom-repeat" items="[[imageList_]]">
<option selected="[[isEqual_(item, notification.appMetadata.icon)]]">
[[getImageTypeName_(item)]]
</option>
</template>
</select>
</div>
<div class="dropdown">
<label>Importance</label>
<select id="importanceSelector" class="md-select"
on-change="onImportanceSelected_">
<template is="dom-repeat" items="[[importanceList_]]">
<option selected="[[isEqual_(item, notification.importance)]]">
[[getImportanceName_(item)]]
</option>
</template>
</select>
</div>
<cr-input value="{{notification.inlineReplyId}}" label="Inline reply id"
type="number" error-message="Inline reply ID already used"
on-change="onInlineReplyIdChanged_" auto-validate required
disabled="[[notification.sent]]" invalid="[[!isValidInlineReplyId_]]">
</cr-input>
<cr-input value="{{notification.timestamp}}" label="Timestamp (ms)" min="0"
id="timestampInput" type="number" on-change="onTimeStampChanged_"
auto-validate error-message="Must be greater than 0" required>
</cr-input>
<cr-input label="Title (Optional)" value="{{notification.title}}">
</cr-input>
<cr-input label="Text Content (Optional)"
value="{{notification.textContent}}" id="textContent">
</cr-input>
<div class="dropdown">
<label>
Shared Image Type (Optional)
</label>
<select id="sharedImageTypeSelector" class="md-select"
on-change="onSharedImageTypeSelected_">
<template is="dom-repeat" items="[[imageList_]]">
<option selected="[[isEqual_(item, notification.sharedImage)]]">
[[getImageTypeName_(item)]]
</option>
</template>
</select>
</div>
<div class="dropdown">
<label>Contact Image Type (Optional)</label>
<select id="contactImageSelector" class="md-select"
on-change="onContactImageTypeSelected_">
<template is="dom-repeat" items="[[imageList_]]">
<option selected="[[isEqual_(item, notification.contactImage)]]">
[[getImageTypeName_(item)]]
</option>
</template>
</select>
</div>
</div>
</div>
// Copyright 2020 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 'chrome://resources/cr_elements/shared_style_css.m.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_input/cr_input.m.js';
import './shared_style.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {MultidevicePhoneHubBrowserProxy} from './multidevice_phonehub_browser_proxy.js';
import {ImageType, imageTypeToStringMap, Importance, importanceToString, Notification} from './types.js';
Polymer({
is: 'notification-form',
_template: html`{__html_template__}`,
properties: {
/** @type{!Notification} */
notification: Object,
/** @private */
isNotificationDataValid_: {
type: Boolean,
computed: 'computeIsNotificationDataValid_(notification.*)',
},
/** @private */
isSent_: {
type: Boolean,
computed: 'computeIsSent_(notification.*)',
reflectToAttribute: true,
},
/** @private */
isValidId_: {
type: Boolean,
computed: 'computeIsValidId_(forbiddenIds)',
},
/** @private */
isValidInlineReplyId_: {
type: Boolean,
computed: 'computeIsValidInlineReplyId_(forbiddenInlineReplyIds)',
},
/** @type{!Array<number>} */
forbiddenIds: {
type: Array,
value: [],
},
/** @type{!Array<number>} */
forbiddenInlineReplyIds: {
type: Array,
value: [],
},
/** @private */
imageList_: {
type: Array,
value: () => {
return [
ImageType.NONE,
ImageType.PINK,
ImageType.RED,
ImageType.GREEN,
ImageType.BLUE,
ImageType.YELLOW,
];
},
readonly: true,
},
/** @private */
importanceList_: {
type: Array,
value: () => {
return [
Importance.UNSPECIFIED,
Importance.NONE,
Importance.MIN,
Importance.LOW,
Importance.DEFAULT,
Importance.HIGH,
];
},
readonly: true,
},
/** @private */
updateNotificationText_: {
type: String,
value: 'Update this notification',
},
},
/** @private{?MultidevicePhoneHubBrowserProxy}*/
browserProxy_: null,
/** @override */
created() {
this.browserProxy_ = MultidevicePhoneHubBrowserProxy.getInstance();
},
/**
* @return {boolean}
* @private
*/
computeIsValidId_() {
return this.notification.sent ||
!this.forbiddenIds.includes(Number(this.notification.id));
},
/**
* @return {boolean}
* @private
*/
computeIsValidInlineReplyId_() {
return this.notification.sent ||
!this.forbiddenInlineReplyIds.includes(
Number(this.notification.inlineReplyId));
},
/**
* @return {boolean}
* @private
*/
computeIsNotificationDataValid_() {
// If either the notification ID or inline reply id is invalid,
// the notification is invalid.
if (!this.isValidId_ || !this.isValidInlineReplyId_) {
return false;
}
// Other required fields that need to be formatted correctly.
if (!this.notification.appMetadata.visibleAppName ||
this.notification.icon === ImageType.NONE ||
Number(this.notification.timestamp) < 0) {
return false;
}
// At least the title, text content, or shared image must be populated.
return !!this.notification.title || !!this.notification.textContent ||
this.notification.sharedImage !== ImageType.NONE;
},
/** @private */
computeIsSent_() {
return this.notification.sent;
},
/** @private */
onSetNotification_() {
this.browserProxy_.setNotification(this.notification);
this.notification.sent = true;
this.notifyPath('notification.sent');
},
/** @private */
onUpdateNotification_() {
this.onSetNotification_();
this.updateNotificationText_ = 'Update Sent!';
setTimeout(() => {
this.updateNotificationText_ = 'Update this notification';
}, 1000);
},
/** @private */
onRemoveButtonClick_() {
this.browserProxy_.removeNotification(this.notification.id);
this.fire('remove-notification');
},
/**
* @param {ImageType} imageType
* @return {String}
* @private
*/
getImageTypeName_(imageType) {
return imageTypeToStringMap.get(imageType);
},
/**
* @param {Importance} importance
* @return {String}
* @private
*/
getImportanceName_(importance) {
return importanceToString.get(importance);
},
/** @private */
onInlineReplyIdChanged_() {
//<cr-input> does not save value numerically.
this.notification.inlineReplyId = Number(this.notification.inlineReplyId);
this.notifyPath('notification.inlineReplyId');
},
/** @private */
onNotificationIdChanged_() {
//<cr-input> does not save value numerically.
this.notification.id = Number(this.notification.id);
this.notifyPath('notification.id');
},
/** @private */
onTimeStampChanged_() {
//<cr-input> does not save value numerically.
this.notification.timestamp = Number(this.notification.timestamp);
this.notifyPath('notification.timestamp');
},
/** @private */
onIconImageTypeSelected_() {
const select = /** @type {!HTMLSelectElement} */
(this.$$('#iconImageTypeSelector'));
this.notification.appMetadata.icon = this.imageList_[select.selectedIndex];
},
/** @private */
onSharedImageTypeSelected_() {
const select = /** @type {!HTMLSelectElement} */
(this.$$('#sharedImageTypeSelector'));
this.notification.sharedImage = this.imageList_[select.selectedIndex];
this.notifyPath('notification.sharedImage');
},
/** @private */
onContactImageTypeSelected_() {
const select = /** @type {!HTMLSelectElement} */
(this.$$('#contactImageSelector'));
this.notification.contactImage = this.imageList_[select.selectedIndex];
this.notifyPath('notification.contactImage');
},
/** @private */
onImportanceSelected_() {
const select = /** @type {!HTMLSelectElement} */
(this.$$('#importanceSelector'));
this.notification.importance = this.importanceList_[select.selectedIndex];
this.notifyPath('notification.importance');
},
/**
* @param {*} lhs
* @param {*} rhs
* @return {boolean}
* @private
*/
isEqual_(lhs, rhs) {
return lhs === rhs;
},
});
<style include="cr-shared-style shared-style">
:host {
display: flex;
flex: 1 0 100%;
}
#listContainer {
flex: 3;
height: 40vh;
}
</style>
<div class="column">
<cr-button on-click="onAddNotificationClick_" class="internals-button">
Add Notification
</cr-button>
<div class="label">
<span class="emphasize">Note:</span> A notification should include at least
one of Title, Text Content, and Shared Image so that it can be
rendered in the UI. When the notification is first sent, the notification
ID and Inline Reply ID will not be editable anymore. Sent notifications will
have a green background. To update a notification, change the fields and
click the Update button. To remove a notification, click the Remove button.
</div>
</div>
<div class="column" id="listContainer">
<template id="notificationList" is="dom-repeat"
items="{{notificationList_}}">
<notification-form class="notification" notification="{{item}}"
forbidden-ids="[[sentNotificationIds_]]"
forbidden-inline-reply-ids="[[sentInlineReplyIds_]]"
on-remove-notification="onRemoveNotification_">
</notification-form>
</template>
</div>
// Copyright 2020 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 'chrome://resources/cr_elements/shared_style_css.m.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_input/cr_input.m.js';
import './shared_style.js';
import './notification_form.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ImageType, Importance, Notification} from './types.js';
/**
* @param {number} notificationId
* @param {number} nextNotificationInlineReplyId
* @return {!Notification}
*/
function newNotification(notificationId, nextNotificationInlineReplyId) {
return {
sent: false,
id: notificationId,
appMetadata: {
visibleAppName: 'Fake visible app name',
packageName: 'Fake package name',
icon: ImageType.RED,
},
timestamp: Date.now(),
importance: Importance.DEFAULT,
inlineReplyId: nextNotificationInlineReplyId,
title: null,
textContent: null,
sharedImage: ImageType.NONE,
contactImage: ImageType.NONE,
};
}
Polymer({
is: 'notification-manager',
_template: html`{__html_template__}`,
properties: {
/** @private */
idLatest_: {
type: Number,
value: 0,
},
/** @private */
inlineReplyIdLatest_: {
type: Number,
value: 0,
},
/**
* @type {!Array<!Notification>}
*/
notificationList_: {
type: Array,
value: [],
},
/**
* @type {!Array<number>}
*/
sentNotificationIds_: {
type: Array,
computed: 'computeSentNotificationIds_(notificationList_.*)',
},
/**
* @type {!Array<number>}
*/
sentInlineReplyIds_: {
type: Array,
computed: 'computeSentInlineReplyIds_(notificationList_.*)',
},
},
/** @private */
onAddNotificationClick_() {
this.notificationList_.unshift(
newNotification(this.idLatest_, this.inlineReplyIdLatest_));
this.idLatest_++;
this.inlineReplyIdLatest_++;
this.$.notificationList.render();
},
/**
* @param {!Event} e
* @private
*/
onRemoveNotification_(e) {
const notificationEl = e.path[0];
const notificationIndex =
this.$.notificationList.indexForElement(notificationEl);
this.notificationList_.splice(notificationIndex, 1);
this.$.notificationList.render();
},
/** @return {!Array<number>} */
computeSentNotificationIds_() {
return this.notificationList_.filter(item => item.sent)
.map(item => item.id);
},
/** @return {!Array<number>}*/
computeSentInlineReplyIds_() {
return this.notificationList_.filter(item => item.sent)
.map(item => item.inlineReplyId);
},
});
......@@ -6,7 +6,7 @@
.cr-row {
display: flex;
flex: 0 0 100%;
flex: 0 1 100%;
}
#flagDisabledContainer {
......@@ -32,7 +32,9 @@
<select id="featureStatusList" class="md-select"
on-change="onFeatureStatusSelected_">
<template is="dom-repeat" items="[[featureStatusList_]]">
<option>[[getFeatureStatusName_(item)]]</option>
<option selected="[[isEqual_(item, featureStatus_)]]">
[[getFeatureStatusName_(item)]]
</option>
</template>
</select>
<div class="cr-padded-text" hidden="[[isFeatureEnabledAndConnected_]]">
......@@ -52,6 +54,9 @@
<div class="cr-row">
<browser-tabs-model-form></browser-tabs-model-form>
</div>
<div class="cr-row">
<notification-manager></notification-manager>
</div>
</template>
</template>
</template>
......
......@@ -10,6 +10,7 @@ import './browser_tabs_model_form.js';
import './i18n_setup.js';
import './phone_name_form.js';
import './phone_status_model_form.js';
import './notification_manager.js';
import './shared_style.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
......@@ -82,7 +83,7 @@ Polymer({
/** @private {!FeatureStatus} */
featureStatus_: {
type: Number,
value: FeatureStatus.NOT_ELIGIBLE_FOR_FEATURE,
value: FeatureStatus.ENABLED_AND_CONNECTED,
},
/** @private */
......@@ -161,4 +162,14 @@ Polymer({
onPhoneHubFlagButtonClick_() {
window.open('chrome://flags/#enable-phone-hub');
},
/**
* @param {*} lhs
* @param {*} rhs
* @return {boolean}
* @private
*/
isEqual_(lhs, rhs) {
return lhs === rhs;
},
});
......@@ -13,6 +13,11 @@
margin: 5px;
}
.internals-button[disabled] {
background-color: LightGray;
color: gray;
}
.label {
width: 100%;
}
......
......@@ -88,18 +88,33 @@ export const BatterySaverState = {
};
/**
* Numerical values should not be changed because they must stay in sync with
* FaviconType in chromeos/components/phonehub/phone_status_model.cc.
* With the exception of NONE, numerical values should not be changed because
* they must stay in sync with ImageType in
* chromeos/multidevice_internals/multidevice_internals_phone_hub_handler.cc.
* @enum{number}
*/
export const FaviconType = {
PINK: 0,
RED: 1,
GREEN: 2,
BLUE: 3,
YELLOW: 4,
export const ImageType = {
NONE: 0,
PINK: 1,
RED: 2,
GREEN: 3,
BLUE: 4,
YELLOW: 5,
};
/**
* Maps a ImageType to its title label in the dropdown.
* @type {!Map<ImageType, String>}
*/
export const imageTypeToStringMap = new Map([
[ImageType.NONE, 'None'],
[ImageType.PINK, 'Pink'],
[ImageType.RED, 'Red'],
[ImageType.GREEN, 'Green'],
[ImageType.BLUE, 'Blue'],
[ImageType.YELLOW, 'Yellow'],
]);
/**
* @typedef {{
* mobileStatus: !MobileStatus,
......@@ -117,7 +132,7 @@ export let PhoneStatusModel;
* url: string,
* title: string,
* lastAccessedTimeStamp: number,
* favicon: !FaviconType,
* favicon: !ImageType,
* }}
*/
export let BrowserTabsMetadataModel;
......@@ -130,3 +145,57 @@ export let BrowserTabsMetadataModel;
* }}
*/
export let BrowserTabsModel;
/**
* Numerical values should not be changed because they must stay in sync with
* Importance in chromeos/components/phonehub/notification.h.
* @enum{number}
*/
export const Importance = {
UNSPECIFIED: 0,
NONE: 1,
MIN: 2,
LOW: 3,
DEFAULT: 4,
HIGH: 5,
};
/**
* Maps an Importance to its title label in the dropdown.
* @type {!Map<Importance, String>}
*/
export const importanceToString = new Map([
[Importance.UNSPECIFIED, 'Unspecified'],
[Importance.NONE, 'None'],
[Importance.MIN, 'Min'],
[Importance.LOW, 'Low'],
[Importance.DEFAULT, 'Default'],
[Importance.HIGH, 'High'],
]);
/**
* @typedef {{
* visibleAppName: string,
* packageName: string,
* icon: !ImageType,
* }}
*/
export let AppMetadata;
/**
* With the exception of the sent property, values match with Notifications in
* chromeos/components/phonehub/notification.h
* @typedef {{
* sent: boolean,
* id: number,
* appMetadata: !AppMetadata,
* timestamp: number,
* importance: !Importance,
* inlineReplyId: number,
* title: ?string,
* textContent: ?string,
* sharedImage: !ImageType,
* contactImage: !ImageType,
* }}
*/
export let Notification;
......@@ -19,32 +19,32 @@ namespace multidevice {
namespace {
// Fake Favicon colors used for coloring the fake favicon bitmaps.
enum class FaviconType {
kPink = 0,
kRed = 1,
kGreen = 2,
kBlue = 3,
kYellow = 4,
// Fake image types used for fields that require gfx::Image().
enum class ImageType {
kPink = 1,
kRed = 2,
kGreen = 3,
kBlue = 4,
kYellow = 5,
};
const SkBitmap FaviconNumToBitmap(FaviconType favicon_num) {
const SkBitmap ImageTypeToBitmap(ImageType image_type_num) {
SkBitmap bitmap;
bitmap.allocN32Pixels(16, 16);
switch (favicon_num) {
case FaviconType::kPink:
switch (image_type_num) {
case ImageType::kPink:
bitmap.eraseARGB(0, 255, 192, 203);
break;
case FaviconType::kRed:
case ImageType::kRed:
bitmap.eraseARGB(0, 255, 0, 0);
break;
case FaviconType::kGreen:
case ImageType::kGreen:
bitmap.eraseARGB(0, 0, 255, 0);
break;
case FaviconType::kBlue:
case ImageType::kBlue:
bitmap.eraseARGB(0, 0, 0, 255);
break;
case FaviconType::kYellow:
case ImageType::kYellow:
bitmap.eraseARGB(0, 255, 255, 0);
break;
default:
......@@ -53,6 +53,24 @@ const SkBitmap FaviconNumToBitmap(FaviconType favicon_num) {
return bitmap;
}
phonehub::Notification::AppMetadata DictToAppMetadata(
const base::DictionaryValue* app_metadata_dict) {
base::string16 visible_app_name;
CHECK(app_metadata_dict->GetString("visibleAppName", &visible_app_name));
std::string package_name;
CHECK(app_metadata_dict->GetString("packageName", &package_name));
int icon_image_type_as_int;
CHECK(app_metadata_dict->GetInteger("icon", &icon_image_type_as_int));
auto icon_image_type = static_cast<ImageType>(icon_image_type_as_int);
gfx::Image icon =
gfx::Image::CreateFrom1xBitmap(ImageTypeToBitmap(icon_image_type));
return phonehub::Notification::AppMetadata(visible_app_name, package_name,
icon);
}
base::Optional<phonehub::BrowserTabsModel::BrowserTabMetadata>
DictToBrowserTabMetadataModel(
const base::DictionaryValue* browser_tab_metadata) {
......@@ -73,14 +91,15 @@ DictToBrowserTabMetadataModel(
return base::nullopt;
}
int favicon_type_as_int;
if (!browser_tab_metadata->GetInteger("favicon", &favicon_type_as_int)) {
int favicon_image_type_as_int;
if (!browser_tab_metadata->GetInteger("favicon",
&favicon_image_type_as_int)) {
return base::nullopt;
}
auto favicon_type = static_cast<FaviconType>(favicon_type_as_int);
auto favicon_image_type = static_cast<ImageType>(favicon_image_type_as_int);
gfx::Image favicon =
gfx::Image::CreateFrom1xBitmap(FaviconNumToBitmap(favicon_type));
gfx::Image::CreateFrom1xBitmap(ImageTypeToBitmap(favicon_image_type));
return phonehub::BrowserTabsModel::BrowserTabMetadata(
GURL(url), title, base::Time::FromJsTime(last_accessed_timestamp),
favicon);
......@@ -121,6 +140,16 @@ void MultidevicePhoneHubHandler::RegisterMessages() {
"setBrowserTabs",
base::BindRepeating(&MultidevicePhoneHubHandler::HandleSetBrowserTabs,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"setNotification",
base::BindRepeating(&MultidevicePhoneHubHandler::HandleSetNotification,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"removeNotification",
base::BindRepeating(&MultidevicePhoneHubHandler::HandleRemoveNotification,
base::Unretained(this)));
}
void MultidevicePhoneHubHandler::EnableRealPhoneHubManager() {
......@@ -278,5 +307,84 @@ void MultidevicePhoneHubHandler::HandleSetBrowserTabs(
PA_LOG(VERBOSE) << "Set second most recent browser tab to" << *metadatas[0];
}
void MultidevicePhoneHubHandler::HandleSetNotification(
const base::ListValue* args) {
const base::DictionaryValue* notification_data_dict = nullptr;
CHECK(args->GetDictionary(0, &notification_data_dict));
int id;
CHECK(notification_data_dict->GetInteger("id", &id));
const base::DictionaryValue* app_metadata_dict = nullptr;
CHECK(
notification_data_dict->GetDictionary("appMetadata", &app_metadata_dict));
phonehub::Notification::AppMetadata app_metadata =
DictToAppMetadata(app_metadata_dict);
// JavaScript time stamps don't fit in int.
double js_timestamp;
CHECK(notification_data_dict->GetDouble("timestamp", &js_timestamp));
auto timestamp = base::Time::FromJsTime(js_timestamp);
int importance_as_int;
CHECK(notification_data_dict->GetInteger("importance", &importance_as_int));
auto importance =
static_cast<phonehub::Notification::Importance>(importance_as_int);
int inline_reply_id;
CHECK(notification_data_dict->GetInteger("inlineReplyId", &inline_reply_id));
base::Optional<base::string16> opt_title;
base::string16 title;
if (notification_data_dict->GetString("title", &title) && !title.empty()) {
opt_title = title;
}
base::Optional<base::string16> opt_text_content;
base::string16 text_content;
if (notification_data_dict->GetString("textContent", &text_content) &&
!text_content.empty()) {
opt_text_content = text_content;
}
base::Optional<gfx::Image> opt_shared_image;
int shared_image_type_as_int;
if (notification_data_dict->GetInteger("sharedImage",
&shared_image_type_as_int) &&
shared_image_type_as_int) {
auto shared_image_type = static_cast<ImageType>(shared_image_type_as_int);
opt_shared_image =
gfx::Image::CreateFrom1xBitmap(ImageTypeToBitmap(shared_image_type));
}
base::Optional<gfx::Image> opt_contact_image;
int contact_image_type_as_int;
if (notification_data_dict->GetInteger("contactImage",
&contact_image_type_as_int) &&
contact_image_type_as_int) {
auto shared_contact_image_type =
static_cast<ImageType>(contact_image_type_as_int);
opt_contact_image = gfx::Image::CreateFrom1xBitmap(
ImageTypeToBitmap(shared_contact_image_type));
}
auto notification = phonehub::Notification(
id, app_metadata, timestamp, importance, inline_reply_id, opt_title,
opt_text_content, opt_shared_image, opt_contact_image);
PA_LOG(VERBOSE) << "Set notification" << notification;
fake_phone_hub_manager_->fake_notification_manager()->SetNotification(
std::move(notification));
}
void MultidevicePhoneHubHandler::HandleRemoveNotification(
const base::ListValue* args) {
int notification_id = 0;
CHECK(args->GetInteger(0, &notification_id));
fake_phone_hub_manager_->fake_notification_manager()->RemoveNotification(
notification_id);
PA_LOG(VERBOSE) << "Removed notification with id " << notification_id;
}
} // namespace multidevice
} // namespace chromeos
......@@ -37,6 +37,8 @@ class MultidevicePhoneHubHandler : public content::WebUIMessageHandler {
void HandleSetFakePhoneName(const base::ListValue* args);
void HandleSetFakePhoneStatus(const base::ListValue* args);
void HandleSetBrowserTabs(const base::ListValue* args);
void HandleSetNotification(const base::ListValue* args);
void HandleRemoveNotification(const base::ListValue* args);
std::unique_ptr<phonehub::FakePhoneHubManager> fake_phone_hub_manager_;
};
......
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