diff --git a/api-samples/power/keepAwake Advanced/README.md b/api-samples/power/keepAwake Advanced/README.md new file mode 100644 index 0000000000..76ae325dcc --- /dev/null +++ b/api-samples/power/keepAwake Advanced/README.md @@ -0,0 +1,27 @@ +# chrome.power + +This extension demonstrates the `chrome.power` API by allowing users to override their system's power management features. + +## Overview + +The extension adds an icon that allows the user to choose different power management states when clicked: + +- System Default +- Screen stays awake +- System stays awake, but screen can sleep + +There is also a popup where the user can also optionally specify an automatic +timeout for the chosen state. This popup can be triggered by clicking the icon +or by selecting it from the icon's context menu. + +## Running this extension + +Either install it from the Chrome Web Store: + +- [Keep Awake Extension](https://chrome.google.com/webstore/detail/keep-awake/bijihlabcfdnabacffofojgmehjdielb) + +Or load it as an upacked extension: + +1. Clone this repository. +2. Load this directory in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/development-basics/#load-unpacked). +3. Pin the extension and click the action button. diff --git a/api-samples/power/keepAwake Advanced/_locales/en/messages.json b/api-samples/power/keepAwake Advanced/_locales/en/messages.json new file mode 100644 index 0000000000..a37e542a31 --- /dev/null +++ b/api-samples/power/keepAwake Advanced/_locales/en/messages.json @@ -0,0 +1,58 @@ +{ + "extensionName": { + "message": "Keep Awake", + "description": "Extension name." + }, + "extensionDescription": { + "message": "Override system power-saving settings.", + "description": "Extension description." + }, + "disabledTitle": { + "message": "Default power-saving settings", + "description": "Browser action title when disabled." + }, + "displayTitle": { + "message": "Screen will be kept on", + "description": "Browser action title when preventing screen-off." + }, + "systemTitle": { + "message": "System will stay awake", + "description": "Browser action title when preventing system sleep." + }, + "untilText": { + "message": " until: ", + "description": "Suffix to append to above Titles to append an end time" + }, + "autoDisableAfterText": { + "message": "Automatically disable after:", + "description": "Text labelling a slider allowing setting a timeout for disabling the power saving state." + }, + "autoDisableAtText": { + "message": "Automatically disable at: ", + "description": "Prefix Text indicating the time when the state will automatically switch to disabled." + }, + "autoDisableHoursSuffix": { + "message": "h", + "description": "Text to append after a number indicating a quantity of hours" + }, + "disabledLabel": { + "message": "Disabled", + "description": "Button label to indicated keep awake is disabled." + }, + "displayLabel": { + "message": "Screen on", + "description": "Button label to indicated keep awake is preventing screen-off." + }, + "systemLabel": { + "message": "System on", + "description": "Button label to indicated keep awake is preventing system sleep." + }, + "usePopupMenuTitle": { + "message": "Always show State Popup", + "description": "Checkbox item indicating that the popup menu should always be shown." + }, + "openStateWindowMenuTitle": { + "message": "Change State...", + "description": "Menu item opening a popup window to change the state." + } +} diff --git a/api-samples/power/keepAwake Advanced/background.js b/api-samples/power/keepAwake Advanced/background.js new file mode 100644 index 0000000000..79f66ace1d --- /dev/null +++ b/api-samples/power/keepAwake Advanced/background.js @@ -0,0 +1,287 @@ +// @ts-check + +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { StateEnum, getSavedMode, verifyMode } from './common.js'; +/** @typedef {import('./common.js').KeepAwakeMode} KeepAwakeMode */ + +const ALARM_NAME = 'keepAwakeTimeout'; +const HOUR_TO_MILLIS = 60 * 60 * 1000; +const USE_POPUP_DEFAULT = { usePopup: true }; + +/** + * Simple timestamped log function + * @param {string} msg + * @param {...*} args + */ +function log(msg, ...args) { + console.log(new Date().toLocaleTimeString('short') + ' ' + msg, ...args); +} + +/** + * Set keep awake mode, and update icon. + * + * @param {KeepAwakeMode} mode + */ +function updateState(mode) { + let imagePrefix; + let title; + + switch (mode.state) { + case StateEnum.DISABLED: + chrome.power.releaseKeepAwake(); + imagePrefix = 'night'; + title = chrome.i18n.getMessage('disabledTitle'); + break; + case StateEnum.DISPLAY: + chrome.power.requestKeepAwake('display'); + imagePrefix = 'day'; + title = chrome.i18n.getMessage('displayTitle'); + break; + case StateEnum.SYSTEM: + chrome.power.requestKeepAwake('system'); + imagePrefix = 'sunset'; + title = chrome.i18n.getMessage('systemTitle'); + break; + default: + throw 'Invalid state "' + mode.state + '"'; + } + + chrome.action.setIcon({ + path: { + 19: 'images/' + imagePrefix + '-19.png', + 38: 'images/' + imagePrefix + '-38.png' + } + }); + + if (mode.endMillis && mode.state != StateEnum.DISABLED) { + // a timeout is specified, update the badge and the title text + let hoursLeft = Math.ceil((mode.endMillis - Date.now()) / HOUR_TO_MILLIS); + chrome.action.setBadgeText({ text: `${hoursLeft}h` }); + const endDate = new Date(mode.endMillis); + chrome.action.setTitle({ + title: `${title}${chrome.i18n.getMessage('untilText')} ${endDate.toLocaleTimeString(undefined, { timeStyle: 'short' })}` + }); + log( + `mode = ${mode.state} for the next ${hoursLeft}hrs until ${endDate.toLocaleTimeString()}` + ); + } else { + // No timeout. + chrome.action.setBadgeText({ text: '' }); + chrome.action.setTitle({ title: title }); + log(`mode = ${mode.state}`); + } +} + +/** + * + * Apply a new KeepAwake mode. + * + * @param {KeepAwakeMode} newMode + * @return {Promise} + */ +async function setNewMode(newMode) { + // Clear any old alarms + await chrome.alarms.clearAll(); + + // is a timeout required? + if (newMode.defaultDurationHrs && newMode.state !== StateEnum.DISABLED) { + // Set an alarm every 60 mins. + chrome.alarms.create(ALARM_NAME, { + delayInMinutes: 60, + periodInMinutes: 60 + }); + newMode.endMillis = + Date.now() + newMode.defaultDurationHrs * HOUR_TO_MILLIS; + } else { + newMode.endMillis = null; + } + + // Store the new mode. + chrome.storage.local.set(newMode); + updateState(newMode); + return newMode; +} + +/** + * Check to see if any set timeout has expired, and if so, reset the mode. + */ +async function checkTimeoutAndUpdateDisplay() { + const mode = await getSavedMode(); + if (mode.endMillis && mode.endMillis < Date.now()) { + log(`timer expired`); + // reset state to disabled + mode.state = StateEnum.DISABLED; + mode.endMillis = null; + setNewMode(mode); + } else { + updateState(mode); + } +} + +async function recreateAlarms() { + const mode = await getSavedMode(); + await chrome.alarms.clearAll(); + if ( + mode.state !== StateEnum.DISABLED && + mode.endMillis && + mode.endMillis > Date.now() + ) { + // previous timeout has not yet expired... + // restart alarm to be triggered at the next 1hr of the timeout + const remainingMillis = mode.endMillis - Date.now(); + const millisToNextHour = remainingMillis % HOUR_TO_MILLIS; + + log( + `recreating alarm, next = ${new Date(Date.now() + millisToNextHour).toLocaleTimeString()}` + ); + chrome.alarms.create(ALARM_NAME, { + delayInMinutes: millisToNextHour / 60_000, + periodInMinutes: 60 + }); + } +} + +/** + * Creates the context menu buttons on the action icon. + */ +async function reCreateContextMenus() { + chrome.contextMenus.removeAll(); + + chrome.contextMenus.create({ + type: 'normal', + id: 'openStateMenu', + title: chrome.i18n.getMessage('openStateWindowMenuTitle'), + contexts: ['action'] + }); + chrome.contextMenus.create({ + type: 'checkbox', + checked: USE_POPUP_DEFAULT.usePopup, + id: 'usePopupMenu', + title: chrome.i18n.getMessage('usePopupMenuTitle'), + contexts: ['action'] + }); + + updateUsePopupMenu( + (await chrome.storage.sync.get(USE_POPUP_DEFAULT)).usePopup + ); +} + +/** + * Sets whether or not to use the popup menu when clicking on the action icon. + * + * @param {boolean} usePopup + */ +function updateUsePopupMenu(usePopup) { + chrome.contextMenus.update('usePopupMenu', { checked: usePopup }); + if (usePopup) { + chrome.action.setPopup({ popup: 'popup.html' }); + } else { + chrome.action.setPopup({ popup: '' }); + } +} + +// Handle messages received from the popup. +chrome.runtime.onMessage.addListener(function (request, _, sendResponse) { + log( + `Got message from popup: state: %s, duration: %d`, + request.state, + request.duration + ); + + setNewMode( + verifyMode({ + state: request.state, + defaultDurationHrs: request.duration, + endMillis: null + }) + ) + .then((newMode) => sendResponse(newMode)) + .catch((e) => { + log(`failed to set new mode: ${e}`, e); + sendResponse(null); + }); + return true; // sendResponse() called asynchronously +}); + +// Handle action clicks - rotates the mode to the next mode. +chrome.action.onClicked.addListener(async () => { + log(`Action clicked`); + + const mode = await getSavedMode(); + switch (mode.state) { + case StateEnum.DISABLED: + mode.state = StateEnum.DISPLAY; + break; + case StateEnum.DISPLAY: + mode.state = StateEnum.SYSTEM; + break; + case StateEnum.SYSTEM: + mode.state = StateEnum.DISABLED; + break; + default: + throw 'Invalid state "' + mode.state + '"'; + } + setNewMode(mode); +}); + +// Handle context menu clicks +chrome.contextMenus.onClicked.addListener(async (e) => { + switch (e.menuItemId) { + case 'openStateMenu': + chrome.windows.create({ + focused: true, + height: 220, + width: 240, + type: 'popup', + url: './popup.html' + }); + break; + + case 'usePopupMenu': + // e.checked is new state, after being clicked. + chrome.storage.sync.set({ usePopup: !!e.checked }); + updateUsePopupMenu(!!e.checked); + break; + } +}); + +// Whenever the alarm is triggered check the timeout and update the icon. +chrome.alarms.onAlarm.addListener(() => { + log('alarm!'); + checkTimeoutAndUpdateDisplay(); +}); + +chrome.runtime.onStartup.addListener(async () => { + log('onStartup'); + recreateAlarms(); + reCreateContextMenus(); +}); + +chrome.runtime.onInstalled.addListener(async () => { + log('onInstalled'); + recreateAlarms(); + reCreateContextMenus(); +}); + +chrome.storage.sync.onChanged.addListener((changes) => { + if (changes.usePopup != null) { + log('usePopup changed to %s', changes.usePopup.newValue); + updateUsePopupMenu(!!changes.usePopup.newValue); + } +}); + +// Whenever the service worker starts up, check the timeout and update the state +checkTimeoutAndUpdateDisplay(); diff --git a/api-samples/power/keepAwake Advanced/common.js b/api-samples/power/keepAwake Advanced/common.js new file mode 100644 index 0000000000..4a82904c78 --- /dev/null +++ b/api-samples/power/keepAwake Advanced/common.js @@ -0,0 +1,75 @@ +// @ts-check + +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export const DURATION_FOR_EVER = 13; + +/** + * States that the extension can be in. + * @readonly + * @enum {string} + */ +export const StateEnum = { + DISABLED: 'disabled', + DISPLAY: 'display', + SYSTEM: 'system' +}; + +/** + * @typedef {{ + * state: StateEnum; + * defaultDurationHrs: number?; + * endMillis: number | null | undefined; + * }} KeepAwakeMode + */ + +/** + * Key used for storing the current state in {localStorage}. + * @type {KeepAwakeMode} + */ +const DEFAULT_MODE = { + state: StateEnum.DISABLED, + defaultDurationHrs: null, // no timeout. + endMillis: null +}; + +/** + * Gets the saved Keep Awake mode from local storage + * @return {Promise} + */ +export async function getSavedMode() { + let mode = await chrome.storage.local.get(DEFAULT_MODE); + return verifyMode(mode); +} + +/** + * Validates the values of the keepawake mode + * + * @param {*} mode + * @return {KeepAwakeMode} + */ +export function verifyMode(mode) { + if (!Object.values(StateEnum).includes(mode.state)) { + mode.state = DEFAULT_MODE.state; + } + mode.defaultDurationHrs = Number(mode.defaultDurationHrs); + if ( + mode.defaultDurationHrs < 1 || + mode.defaultDurationHrs >= DURATION_FOR_EVER + ) { + mode.defaultDurationHrs = null; // no timeout. + } + return mode; +} diff --git a/api-samples/power/images/day-19.png b/api-samples/power/keepAwake Advanced/images/day-19.png similarity index 100% rename from api-samples/power/images/day-19.png rename to api-samples/power/keepAwake Advanced/images/day-19.png diff --git a/api-samples/power/images/day-38.png b/api-samples/power/keepAwake Advanced/images/day-38.png similarity index 100% rename from api-samples/power/images/day-38.png rename to api-samples/power/keepAwake Advanced/images/day-38.png diff --git a/api-samples/power/images/icon-128.png b/api-samples/power/keepAwake Advanced/images/icon-128.png similarity index 100% rename from api-samples/power/images/icon-128.png rename to api-samples/power/keepAwake Advanced/images/icon-128.png diff --git a/api-samples/power/images/icon-16.png b/api-samples/power/keepAwake Advanced/images/icon-16.png similarity index 100% rename from api-samples/power/images/icon-16.png rename to api-samples/power/keepAwake Advanced/images/icon-16.png diff --git a/api-samples/power/images/icon-48.png b/api-samples/power/keepAwake Advanced/images/icon-48.png similarity index 100% rename from api-samples/power/images/icon-48.png rename to api-samples/power/keepAwake Advanced/images/icon-48.png diff --git a/api-samples/power/images/night-19.png b/api-samples/power/keepAwake Advanced/images/night-19.png similarity index 100% rename from api-samples/power/images/night-19.png rename to api-samples/power/keepAwake Advanced/images/night-19.png diff --git a/api-samples/power/images/night-38.png b/api-samples/power/keepAwake Advanced/images/night-38.png similarity index 100% rename from api-samples/power/images/night-38.png rename to api-samples/power/keepAwake Advanced/images/night-38.png diff --git a/api-samples/power/images/sunset-19.png b/api-samples/power/keepAwake Advanced/images/sunset-19.png similarity index 100% rename from api-samples/power/images/sunset-19.png rename to api-samples/power/keepAwake Advanced/images/sunset-19.png diff --git a/api-samples/power/images/sunset-38.png b/api-samples/power/keepAwake Advanced/images/sunset-38.png similarity index 100% rename from api-samples/power/images/sunset-38.png rename to api-samples/power/keepAwake Advanced/images/sunset-38.png diff --git a/api-samples/power/keepAwake Advanced/manifest.json b/api-samples/power/keepAwake Advanced/manifest.json new file mode 100644 index 0000000000..7cd7010958 --- /dev/null +++ b/api-samples/power/keepAwake Advanced/manifest.json @@ -0,0 +1,29 @@ +{ + "update_url": "https://clients2.google.com/service/update2/crx", + + "manifest_version": 3, + + "name": "__MSG_extensionName__", + "description": "__MSG_extensionDescription__", + "version": "2.0", + "icons": { + "16": "images/icon-16.png", + "48": "images/icon-48.png", + "128": "images/icon-128.png" + }, + + "permissions": ["power", "storage", "alarms", "contextMenus"], + "action": { + "default_title": "__MSG_disabledTitle__", + "default_icon": { + "19": "images/night-19.png", + "38": "images/night-38.png" + }, + "default_popup": "popup.html" + }, + "background": { + "service_worker": "background.js", + "type": "module" + }, + "default_locale": "en" +} diff --git a/api-samples/power/keepAwake Advanced/popup.css b/api-samples/power/keepAwake Advanced/popup.css new file mode 100644 index 0000000000..cbccdd566d --- /dev/null +++ b/api-samples/power/keepAwake Advanced/popup.css @@ -0,0 +1,83 @@ +/* + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:root { + --background-color: #e8eef7; + --border-color: #00b4fe; + --main-color: #000000; + --button-background-color: #d5deec; + --button-active-background-color: #c0ccde; +} + +@media (prefers-color-scheme: dark) { + body { + --background-color: #333333; + --main-color: #dddddd; + --button-background-color: #444444; + --button-active-background-color: #555555; + --border-color: #005b80; + } +} + +body { + font: 13px arial, sans-serif; + margin: 0px; + background-color: var(--background-color); + color: var(--main-color); +} + +#popup { + padding: 12px; + border: 3px solid var(--border-color); + margin: auto; + width: fit-content; + text-align: left; + text-wrap: nowrap; +} + +h3 { + margin: 0px; +} + +div { + margin-top:5px; +} + +div#buttons { + display:flex; + gap: 6px; +} + +div#buttons button { + flex: 1 0 0; + min-width: 0; + background-color: var(--button-background-color); + color: var(--main-color); + border: 0; + border-radius: 0.5em; + box-shadow: 3px 3px 2px rgba(0,0,0,0.5); + padding: 5px; +} +div#buttons button:active, div#buttons button.active { + top:2px; + left:1px; + box-shadow: inset 3px 3px 2px rgba(0,0,0,0.5); + background-color: var(--button-active-background-color); +} + +.buttonLabel { + font-size: 11px; +} diff --git a/api-samples/power/keepAwake Advanced/popup.html b/api-samples/power/keepAwake Advanced/popup.html new file mode 100644 index 0000000000..f62c7a2f79 --- /dev/null +++ b/api-samples/power/keepAwake Advanced/popup.html @@ -0,0 +1,44 @@ + + + + #extensionName + + + + + + + + diff --git a/api-samples/power/keepAwake Advanced/popup.js b/api-samples/power/keepAwake Advanced/popup.js new file mode 100644 index 0000000000..554dc428a3 --- /dev/null +++ b/api-samples/power/keepAwake Advanced/popup.js @@ -0,0 +1,168 @@ +// @ts-check + +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { DURATION_FOR_EVER, getSavedMode, StateEnum } from './common.js'; + +/** @typedef {import('./common.js').KeepAwakeMode} KeepAwakeMode */ + +/** @type {HTMLLabelElement} */ +let durationLabel; +/** @type {HTMLInputElement} */ +let durationSlider; + +/** + * Get an HTML element by selector and asserts not null. + * @param {string} query + * @return {HTMLElement} + */ +function querySelectorAndAssert(query) { + const e = document.querySelector(query); + if (!e) { + throw new Error('unable to get element with query: ' + query); + } + return /** @type {HTMLElement} */ (e); +} + +/** + * Updates the label for the duration slider. + */ +function updateDurationLabel() { + durationLabel.textContent = + (durationSlider.value === DURATION_FOR_EVER.toString() + ? '∞' + : durationSlider.value) + + ' ' + + chrome.i18n.getMessage('autoDisableHoursSuffix'); +} + +/** + * Called when the duration slider is changed + * + * Sends a message to the background service worker to set the new state + * and duration. + */ +async function durationSliderChanged() { + updateDurationLabel(); + + const newState = await sendMessageToBackground( + (await getSavedMode()).state, + Number(durationSlider.value) + ); + updateState(newState); +} + +/** + * Sends a message to the background service worker to set the new state and + * duration. + * + * @param {string} state + * @param {number} duration + * @return {Promise} Updated mode + */ +async function sendMessageToBackground(state, duration) { + if (!Object.values(StateEnum).includes(state)) { + throw new Error('invalid State: ' + state); + } + + const message = { state: state }; + if (duration < 1 || duration >= DURATION_FOR_EVER) { + // no timeout + message.duration = null; + } else { + message.duration = duration; + } + return await chrome.runtime.sendMessage(message); +} + +/** + * Called when one of the keepalive buttons is clicked. + * + * Sends a message to the background service worker to set the new state + * and duration. + * + * @param {MouseEvent} e + */ +async function buttonClicked(e) { + const button = /** @type {HTMLElement} */ (e.currentTarget); + const newState = await sendMessageToBackground( + // Button id is named after state. + button.id, + Number(durationSlider.value) + ); + updateState(newState); +} + +/** + * @param {KeepAwakeMode} newMode + */ + +function updateState(newMode) { + console.log('new state: %o', newMode); + if (!newMode) { + throw new Error('invalid null state'); + } + // Re-set active button state. + document.querySelector(`#buttons .active`)?.classList?.remove('active'); + querySelectorAndAssert(`#buttons #${newMode.state}`).classList?.add('active'); + if (newMode.state === StateEnum.DISABLED || !newMode.endMillis) { + querySelectorAndAssert('#autodisable-text').textContent = + chrome.i18n.getMessage('autoDisableAfterText'); + } else { + querySelectorAndAssert('#autodisable-text').textContent = + chrome.i18n.getMessage('autoDisableAtText') + + new Date(newMode.endMillis).toLocaleTimeString(undefined, { + timeStyle: 'short' + }); + } + updateDurationLabel(); +} + +/** + * Run when document is loaded. + * + * Initializes the popup, and sets I18n labels. + */ +async function onload() { + durationSlider = /** @type {HTMLInputElement} */ ( + querySelectorAndAssert('#durationSlider') + ); + durationLabel = /** @type {HTMLLabelElement} */ ( + querySelectorAndAssert('#durationLabel') + ); + + querySelectorAndAssert('#title').textContent = + chrome.i18n.getMessage('extensionName'); + + const mode = await getSavedMode(); + + // set button titles and listeners + for (const id of Object.values(StateEnum)) { + const button = querySelectorAndAssert(`#buttons #${id}`); + button.addEventListener('click', buttonClicked); + button.title = chrome.i18n.getMessage(button.id + 'Title'); + querySelectorAndAssert(`#buttons #${id} .buttonLabel`).textContent = + chrome.i18n.getMessage(button.id + 'Label'); + } + + durationSlider.max = DURATION_FOR_EVER.toString(); + durationSlider.value = ( + mode.defaultDurationHrs ? mode.defaultDurationHrs : DURATION_FOR_EVER + ).toString(); + durationSlider.addEventListener('input', durationSliderChanged); + updateState(mode); +} + +document.addEventListener('DOMContentLoaded', onload); diff --git a/api-samples/power/README.md b/api-samples/power/keepAwake Basic/README.md similarity index 100% rename from api-samples/power/README.md rename to api-samples/power/keepAwake Basic/README.md diff --git a/api-samples/power/_locales/en/messages.json b/api-samples/power/keepAwake Basic/_locales/en/messages.json similarity index 100% rename from api-samples/power/_locales/en/messages.json rename to api-samples/power/keepAwake Basic/_locales/en/messages.json diff --git a/api-samples/power/background.js b/api-samples/power/keepAwake Basic/background.js similarity index 100% rename from api-samples/power/background.js rename to api-samples/power/keepAwake Basic/background.js diff --git a/api-samples/power/keepAwake Basic/images/day-19.png b/api-samples/power/keepAwake Basic/images/day-19.png new file mode 100644 index 0000000000..04965f76ea Binary files /dev/null and b/api-samples/power/keepAwake Basic/images/day-19.png differ diff --git a/api-samples/power/keepAwake Basic/images/day-38.png b/api-samples/power/keepAwake Basic/images/day-38.png new file mode 100644 index 0000000000..1c7484218c Binary files /dev/null and b/api-samples/power/keepAwake Basic/images/day-38.png differ diff --git a/api-samples/power/keepAwake Basic/images/icon-128.png b/api-samples/power/keepAwake Basic/images/icon-128.png new file mode 100644 index 0000000000..5159ca4146 Binary files /dev/null and b/api-samples/power/keepAwake Basic/images/icon-128.png differ diff --git a/api-samples/power/keepAwake Basic/images/icon-16.png b/api-samples/power/keepAwake Basic/images/icon-16.png new file mode 100644 index 0000000000..3478152160 Binary files /dev/null and b/api-samples/power/keepAwake Basic/images/icon-16.png differ diff --git a/api-samples/power/keepAwake Basic/images/icon-48.png b/api-samples/power/keepAwake Basic/images/icon-48.png new file mode 100644 index 0000000000..73d8935835 Binary files /dev/null and b/api-samples/power/keepAwake Basic/images/icon-48.png differ diff --git a/api-samples/power/keepAwake Basic/images/night-19.png b/api-samples/power/keepAwake Basic/images/night-19.png new file mode 100644 index 0000000000..1fee02d8dc Binary files /dev/null and b/api-samples/power/keepAwake Basic/images/night-19.png differ diff --git a/api-samples/power/keepAwake Basic/images/night-38.png b/api-samples/power/keepAwake Basic/images/night-38.png new file mode 100644 index 0000000000..581b588631 Binary files /dev/null and b/api-samples/power/keepAwake Basic/images/night-38.png differ diff --git a/api-samples/power/keepAwake Basic/images/sunset-19.png b/api-samples/power/keepAwake Basic/images/sunset-19.png new file mode 100644 index 0000000000..aedc0d8f4d Binary files /dev/null and b/api-samples/power/keepAwake Basic/images/sunset-19.png differ diff --git a/api-samples/power/keepAwake Basic/images/sunset-38.png b/api-samples/power/keepAwake Basic/images/sunset-38.png new file mode 100644 index 0000000000..bdb716a210 Binary files /dev/null and b/api-samples/power/keepAwake Basic/images/sunset-38.png differ diff --git a/api-samples/power/manifest.json b/api-samples/power/keepAwake Basic/manifest.json similarity index 100% rename from api-samples/power/manifest.json rename to api-samples/power/keepAwake Basic/manifest.json