[Select to Speak] Update extension to MV3 manifest
List of changes:
* Create an off screen document to handle DOM interactions:
* playing the null selection tone in select_to_speak.ts
* google docs specific logic in input_handler.ts
* Removing the use of eval from a content script and hard coding the extension ID instead.
* Replaced deprecated API end point chrome.tabs.executeScript
* Add KeepAlive class
* Use MV3 InstanceChecker
Testing:
* Basic testing of select-to-speak
* Null selection did not work before (https://issuetracker.google.com/issues/398728543), but I confirmed that null tone can be heard by calling `chrome.runtime.sendMessage(undefined, {command: 'playNullSelectionTone'});` directly.
* After turning on select-to-speak and opening a google doc, the text of the google doc can be selected and spoken with text to speak
* If the google doc is already open and select to speak is turned on, the google doc text will not be spoken, bug filed here: https://issuetracker.google.com/issues/399093287
* I was not able to test google docs specific logic in input_hanlder.ts because of a pre-existing bug: https://issuetracker.google.com/issues/400335369
Bug: 388867837
Change-Id: I375fc3f7ce71445e65722d366ec910b4be6b1819
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6324769
Reviewed-by: Akihiro Ota <akihiroota@chromium.org>
Commit-Queue: Valerie Young <spectranaut@igalia.com>
Cr-Commit-Position: refs/heads/main@{#1433622}
diff --git a/chrome/browser/resources/chromeos/accessibility/definitions/offscreen.d.ts b/chrome/browser/resources/chromeos/accessibility/definitions/offscreen.d.ts
new file mode 100644
index 0000000..f22b53b
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/definitions/offscreen.d.ts
@@ -0,0 +1,48 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Definitions for chrome.offscreen API
+ * Generated from: extensions/common/api/offscreen.idl
+ * run `tools/json_schema_compiler/compiler.py
+ * extensions/common/api/offscreen.idl -g ts_definitions` to regenerate.
+ */
+
+
+
+declare namespace chrome {
+ export namespace offscreen {
+
+ export enum Reason {
+ TESTING = 'TESTING',
+ AUDIO_PLAYBACK = 'AUDIO_PLAYBACK',
+ IFRAME_SCRIPTING = 'IFRAME_SCRIPTING',
+ DOM_SCRAPING = 'DOM_SCRAPING',
+ BLOBS = 'BLOBS',
+ DOM_PARSER = 'DOM_PARSER',
+ USER_MEDIA = 'USER_MEDIA',
+ DISPLAY_MEDIA = 'DISPLAY_MEDIA',
+ WEB_RTC = 'WEB_RTC',
+ CLIPBOARD = 'CLIPBOARD',
+ LOCAL_STORAGE = 'LOCAL_STORAGE',
+ WORKERS = 'WORKERS',
+ BATTERY_STATUS = 'BATTERY_STATUS',
+ MATCH_MEDIA = 'MATCH_MEDIA',
+ GEOLOCATION = 'GEOLOCATION',
+ }
+
+ export interface CreateParameters {
+ reasons: Reason[];
+ url: string;
+ justification: string;
+ }
+
+ export function createDocument(parameters: CreateParameters): Promise<void>;
+
+ export function closeDocument(): Promise<void>;
+
+ export function hasDocument(): Promise<boolean>;
+
+ }
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/definitions/scripting.d.ts b/chrome/browser/resources/chromeos/accessibility/definitions/scripting.d.ts
new file mode 100644
index 0000000..f9199aa
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/definitions/scripting.d.ts
@@ -0,0 +1,96 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Definitions for chrome.scripting API
+ * Generated from: chrome/common/extensions/api/scripting.idl
+ * run `tools/json_schema_compiler/compiler.py
+ * chrome/common/extensions/api/scripting.idl -g ts_definitions` to regenerate.
+ */
+
+
+
+declare namespace chrome {
+ export namespace scripting {
+
+ export const globalParams: number;
+
+ export enum StyleOrigin {
+ AUTHOR = 'AUTHOR',
+ USER = 'USER',
+ }
+
+ export enum ExecutionWorld {
+ ISOLATED = 'ISOLATED',
+ MAIN = 'MAIN',
+ }
+
+ export interface InjectionTarget {
+ tabId: number;
+ frameIds?: number[];
+ documentIds?: string[];
+ allFrames?: boolean;
+ }
+
+ export interface ScriptInjection {
+ func?: () => void;
+ args?: any[];
+ function?: () => void;
+ files?: string[];
+ target: InjectionTarget;
+ world?: ExecutionWorld;
+ injectImmediately?: boolean;
+ }
+
+ export interface CSSInjection {
+ target: InjectionTarget;
+ css?: string;
+ files?: string[];
+ origin?: StyleOrigin;
+ }
+
+ export interface InjectionResult {
+ result?: any;
+ frameId: number;
+ documentId: string;
+ }
+
+ export interface RegisteredContentScript {
+ id: string;
+ matches?: string[];
+ excludeMatches?: string[];
+ css?: string[];
+ js?: string[];
+ allFrames?: boolean;
+ matchOriginAsFallback?: boolean;
+ runAt?: extensionTypes.RunAt;
+ persistAcrossSessions?: boolean;
+ world?: ExecutionWorld;
+ }
+
+ export interface ContentScriptFilter {
+ ids?: string[];
+ }
+
+ export function executeScript(injection: ScriptInjection):
+ Promise<InjectionResult[]>;
+
+ export function insertCSS(injection: CSSInjection): Promise<void>;
+
+ export function removeCSS(injection: CSSInjection): Promise<void>;
+
+ export function registerContentScripts(scripts: RegisteredContentScript[]):
+ Promise<void>;
+
+ export function getRegisteredContentScripts(filter?: ContentScriptFilter):
+ Promise<RegisteredContentScript[]>;
+
+ export function unregisterContentScripts(filter?: ContentScriptFilter):
+ Promise<void>;
+
+ export function updateContentScripts(scripts: RegisteredContentScript[]):
+ Promise<void>;
+
+ }
+}
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/BUILD.gn b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/BUILD.gn
index fec8bef..474f1e9 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/BUILD.gn
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/BUILD.gn
@@ -38,6 +38,7 @@
"ui_manager.ts",
"prefs_manager.ts",
"select_to_speak.ts",
+ "offscreen.ts",
]
# Root dir must be the parent directory so it can reach common.
@@ -61,7 +62,9 @@
"../../definitions/extensions.d.ts",
"../../definitions/extension_types.d.ts",
"../../definitions/i18n.d.ts",
+ "../../definitions/offscreen.d.ts",
"../../definitions/runtime.d.ts",
+ "../../definitions/scripting.d.ts",
"../../definitions/settings_private_mv2.d.ts",
"../../definitions/storage_mv2.d.ts",
"../../definitions/tabs.d.ts",
@@ -91,6 +94,8 @@
"background.html",
"checked.png",
"earcons/null_selection.ogg",
+ "gdocs_script.js",
+ "offscreen.html",
"select_to_speak-2x.svg",
"sts-icon-128.png",
"sts-icon-16.png",
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/gdocs_script.js b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/gdocs_script.js
new file mode 100644
index 0000000..49133366
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/gdocs_script.js
@@ -0,0 +1,6 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Set to Select to Speak extension ID.
+window['_docs_annotate_canvas_by_ext'] = 'klbcgckkldhdhonijdbnhhaiedfkllef';
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/input_handler.ts b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/input_handler.ts
index bb9bf19..2922b9e 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/input_handler.ts
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/input_handler.ts
@@ -6,6 +6,8 @@
import {SelectToSpeakConstants} from './select_to_speak_constants.js';
+type MessageSender = chrome.runtime.MessageSender;
+
/**
* Callbacks for InputHandler.
* |canStartSelecting| returns true if the user can start selecting a region
@@ -39,13 +41,9 @@
private isSelectionKeyDown_: boolean;
private keysCurrentlyDown_: Set<number>;
private keysPressedTogether_: Set<number>;
- private lastClearClipboardDataTime_: Date;
- private lastReadClipboardDataTime_: Date;
private mouseStart_: {x: number, y: number};
private mouseEnd_: {x: number, y: number};
private trackingMouse_: boolean;
- static kClipboardClearMaxDelayMs: number;
- static kClipboardReadMaxDelayMs: number;
/**
* Please keep fields in alphabetical order.
@@ -62,65 +60,14 @@
*/
this.keysPressedTogether_ = new Set();
- /**
- * The timestamp at which the last clipboard data clear was requested.
- * Used to make sure we don't clear the clipboard on a user's request,
- * but only after the clipboard was used to read selected text.
- */
- this.lastClearClipboardDataTime_ = new Date(0);
-
- /**
- * The timestamp at which clipboard data read was requested by the user
- * doing a "read selection" keystroke on a Google Docs app. If a
- * clipboard change event comes in within kClipboardReadMaxDelayMs,
- * Select-to-Speak will read that text out loud.
- */
- this.lastReadClipboardDataTime_ = new Date(0);
-
this.mouseStart_ = {x: 0, y: 0};
this.mouseEnd_ = {x: 0, y: 0};
this.trackingMouse_ = false;
}
- private clearClipboard_(): void {
- this.lastClearClipboardDataTime_ = new Date();
- document.execCommand('copy');
- }
-
- private onClipboardCopy_(evt: ClipboardEvent): void {
- if (new Date().getTime() - this.lastClearClipboardDataTime_.getTime() <
- InputHandler.kClipboardClearMaxDelayMs) {
- // onClipboardPaste has just completed reading the clipboard for speech.
- // This is used to clear the clipboard.
- // @ts-ignore: TODO(b/270623046): clipboardData can be null.
- evt.clipboardData.setData('text/plain', '');
- evt.preventDefault();
- this.lastClearClipboardDataTime_ = new Date(0);
- }
- }
-
- private onClipboardDataChanged_() : void {
- if (new Date().getTime() - this.lastReadClipboardDataTime_.getTime() <
- InputHandler.kClipboardReadMaxDelayMs) {
- // The data has changed, and we are ready to read it.
- // Get it using a paste.
- document.execCommand('paste');
- }
- }
-
- private onClipboardPaste_(evt: ClipboardEvent): void {
- if (new Date().getTime() - this.lastReadClipboardDataTime_.getTime() <
- InputHandler.kClipboardReadMaxDelayMs) {
- // Read the current clipboard data.
- evt.preventDefault();
- // @ts-ignore: TODO(b/270623046): clipboardData can be null.
- this.callbacks_.onTextReceived(evt.clipboardData.getData('text/plain'));
- this.lastReadClipboardDataTime_ = new Date(0);
- // Clear the clipboard data by copying nothing (the current document).
- // Do this in a timeout to avoid a recursive warning per
- // https://crbug.com/363288.
- setTimeout(() => this.clearClipboard_(), 0);
- }
+ private onClipboardPaste_(content: String): void {
+ // @ts-ignore: TODO(crbug.com/270623046): clipboardData can be null.
+ this.callbacks_.onTextReceived(content);
}
/**
@@ -130,10 +77,9 @@
* any particular window.
*/
setUpEventListeners(): void {
- chrome.clipboard.onClipboardDataChanged.addListener(
- () => this.onClipboardDataChanged_());
- document.addEventListener('paste', evt => this.onClipboardPaste_(evt));
- document.addEventListener('copy', evt => this.onClipboardCopy_(evt));
+ chrome.clipboard.onClipboardDataChanged.addListener(() => {
+ chrome.runtime.sendMessage(undefined, {command: 'clipboardDataChanged'});
+ });
chrome.accessibilityPrivate.onSelectToSpeakKeysPressedChanged.addListener(
(keysPressed) => {
this.onKeysPressedChanged(new Set(keysPressed));
@@ -142,6 +88,18 @@
(eventType, mouseX, mouseY) => {
this.onMouseEvent(eventType, mouseX, mouseY);
});
+
+ // Handle messages from the offscreen document.
+ chrome.runtime.onMessage.addListener(
+ (message: any|undefined, _sender: MessageSender,
+ _sendResponse: (value: any) => void) => {
+ switch (message['command']) {
+ case 'paste':
+ this.onClipboardPaste_(message);
+ break;
+ }
+ return false;
+ });
}
/**
@@ -166,7 +124,8 @@
* Sets the date at which we last wanted the clipboard data to be read.
*/
onRequestReadClipboardData(): void {
- this.lastReadClipboardDataTime_ = new Date();
+ chrome.runtime.sendMessage(
+ undefined, {command: 'updateLastReadClipboardDataTime'});
}
/**
@@ -346,11 +305,3 @@
}
}
-// Number of milliseconds to wait after requesting a clipboard read
-// before clipboard change and paste events are ignored.
-InputHandler.kClipboardReadMaxDelayMs = 1000;
-
-// Number of milliseconds to wait after requesting a clipboard copy
-// before clipboard copy events are ignored, used to clear the clipboard
-// after reading data in a paste event.
-InputHandler.kClipboardClearMaxDelayMs = 500;
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/offscreen.html b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/offscreen.html
new file mode 100644
index 0000000..cbae3e95
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/offscreen.html
@@ -0,0 +1,8 @@
+<!--
+Copyright 2025 The Chromium Authors
+Use of this source code is governed by a BSD-style license that can be
+found in the LICENSE file.
+-->
+<body>
+ <script src="offscreen.js"></script>
+</body>
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/offscreen.ts b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/offscreen.ts
new file mode 100644
index 0000000..546abb8
--- /dev/null
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/offscreen.ts
@@ -0,0 +1,118 @@
+// Copyright 2025 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+type MessageSender = chrome.runtime.MessageSender;
+
+let AudioAndCopyHandlerObject;
+
+// Number of milliseconds to wait after requesting a clipboard read
+// before clipboard change and paste events are ignored.
+const kClipboardReadMaxDelayMs = 1000;
+
+// Number of milliseconds to wait after requesting a clipboard copy
+// before clipboard copy events are ignored, used to clear the clipboard
+// after reading data in a paste event.
+const kClipboardClearMaxDelayMs = 500;
+
+class AudioAndCopyHandler {
+ private audioElement_: HTMLAudioElement;
+
+ private lastClearClipboardDataTime_: Date;
+ private lastReadClipboardDataTime_: Date;
+
+ constructor() {
+ this.audioElement_ = new Audio('earcons/null_selection.ogg');
+
+ /**
+ * The timestamp at which the last clipboard data clear was requested.
+ * Used to make sure we don't clear the clipboard on a user's request,
+ * but only after the clipboard was used to read selected text.
+ */
+ this.lastClearClipboardDataTime_ = new Date(0);
+
+ /**
+ * The timestamp at which clipboard data read was requested by the user
+ * doing a "read selection" keystroke on a Google Docs app. If a
+ * clipboard change event comes in within kClipboardReadMaxDelayMs,
+ * Select-to-Speak will read that text out loud.
+ */
+ this.lastReadClipboardDataTime_ = new Date(0);
+
+ document.addEventListener('paste', evt => {
+ this.onClipboardPaste_(evt);
+ });
+
+ document.addEventListener('copy', evt => {
+ this.onClipboardCopy_(evt);
+ });
+
+ // Handle messages from the service worker.
+ chrome.runtime.onMessage.addListener(
+ (message: any|undefined, _sender: MessageSender,
+ _sendResponse: (value: any) => void) => {
+ switch (message['command']) {
+ case 'playNullSelectionTone':
+ this.audioElement_.play();
+ break;
+ case 'updateLastReadClipboardDataTime':
+ this.lastReadClipboardDataTime_ = new Date();
+ break;
+ case 'clipboardDataChanged':
+ this.onClipboardDataChanged_();
+ break;
+ }
+ return false;
+ });
+ }
+
+ private onClipboardDataChanged_(): void {
+ if (new Date().getTime() - this.lastReadClipboardDataTime_.getTime() <
+ kClipboardReadMaxDelayMs) {
+ // The data has changed, and we are ready to read it.
+ // Get it using a paste.
+ document.execCommand('paste');
+ }
+ }
+
+ private onClipboardPaste_(evt: ClipboardEvent): void {
+ if (new Date().getTime() - this.lastReadClipboardDataTime_.getTime() <
+ kClipboardReadMaxDelayMs) {
+ // Read the current clipboard data.
+ evt.preventDefault();
+ this.lastReadClipboardDataTime_ = new Date(0);
+ // Clear the clipboard data by copying nothing (the current document).
+ // Do this in a timeout to avoid a recursive warning per
+ // https://crbug.com/363288.
+ setTimeout(() => this.clearClipboard_(), 0);
+
+ // @ts-ignore: TODO(crbug.com/270623046): clipboardData can be null.
+ let content = evt.clipboardData.getData('text/plain');
+ chrome.runtime.sendMessage(undefined, {
+ command: 'paste',
+ content,
+ });
+ }
+ }
+
+ private onClipboardCopy_(evt: ClipboardEvent): void {
+ if (new Date().getTime() - this.lastClearClipboardDataTime_.getTime() <
+ kClipboardClearMaxDelayMs) {
+ // onClipboardPaste has just completed reading the clipboard for speech.
+ // This is used to clear the clipboard.
+ // @ts-ignore: TODO(crbug.com/270623046): clipboardData can be null.
+ evt.clipboardData.setData('text/plain', '');
+ evt.preventDefault();
+ this.lastClearClipboardDataTime_ = new Date(0);
+ }
+ }
+
+ private clearClipboard_(): void {
+ this.lastClearClipboardDataTime_ = new Date();
+ document.execCommand('copy');
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ AudioAndCopyHandlerObject = new AudioAndCopyHandler();
+});
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/select_to_speak.ts b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/select_to_speak.ts
index ff696c9..b7b104a 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/select_to_speak.ts
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/select_to_speak.ts
@@ -5,7 +5,6 @@
import {AutomationPredicate} from '/common/automation_predicate.js';
import {AutomationUtil} from '/common/automation_util.js';
import {constants} from '/common/constants.js';
-import {FlagName, Flags} from '/common/flags.js';
import {NodeNavigationUtils} from '/common/node_navigation_utils.js';
import {NodeUtils} from '/common/node_utils.js';
import {ParagraphUtils} from '/common/paragraph_utils.js';
@@ -63,10 +62,10 @@
private currentNodeGroupItemIndex_: number;
private currentNodeGroups_: ParagraphUtils.NodeGroup[];
private currentNodeWord_: {start: number, end: number}|null;
+ private createdOffscreenDocument_: Promise<void>|null = null;
private desktop_: AutomationNode|undefined;
private inputHandler_: InputHandler|null;
private intervalId_: number|undefined;
- private nullSelectionTone_: HTMLAudioElement;
private onStateChangeRequestedCallbackForTest_: (() => void)|null;
private prefsManager_: PrefsManager;
private scrollToSpokenNode_: boolean;
@@ -136,8 +135,6 @@
*/
this.intervalId_;
- this.nullSelectionTone_ = new Audio('earcons/null_selection.ogg');
-
/**
* Function to be called when a state change request is received from the
* accessibilityPrivate API.
@@ -196,24 +193,17 @@
this.prefsManager_.initPreferences();
this.runContentScripts_();
- this.setUpEventListeners_();
-
- await Flags.init();
+ await this.setUpEventListeners_();
const createArgs: chrome.contextMenus.CreateProperties = {
title: chrome.i18n.getMessage(
'select_to_speak_listen_context_menu_option_text'),
contexts: [chrome.contextMenus.ContextType.SELECTION],
id: 'select_to_speak',
};
- if (Flags.isEnabled(FlagName.MANIFEST_V3)) {
- chrome.contextMenus.onClicked.addListener(() => {
- this.getFocusedNodeAndSpeakSelectedText_();
- });
- } else {
- createArgs['onclick'] = () => {
- this.getFocusedNodeAndSpeakSelectedText_();
- };
- }
+ chrome.contextMenus.onClicked.addListener(() => {
+ this.getFocusedNodeAndSpeakSelectedText_();
+ });
+
// Install the context menu in the Ash browser.
await chrome.contextMenus.create(createArgs);
@@ -350,8 +340,8 @@
// that a node is in ARC++.
if (!NodeUtils.findAllMatching(root, rect, nodes) && focusedNode &&
focusedNode.root!.role !== RoleType.DESKTOP) {
- // TODO(b/314203187): Determine if not null assertion is appropriate
- // here.
+ // TODO(crbug.com/314203187): Determine if not null assertion is
+ // appropriate here.
NodeUtils.findAllMatching(focusedNode.root!, rect, nodes);
}
if (nodes.length === 1 && UiManager.isTrayButton(nodes[0])) {
@@ -491,7 +481,7 @@
method: MetricsUtils.StartSpeechMethod|null,
focusedNode: AutomationNode|undefined): void {
const nodes = [];
- // TODO(b/314204374): AutomationUtil.findNextNode may return null.
+ // TODO(crbug.com/314204374): AutomationUtil.findNextNode may return null.
let selectedNode: AutomationNode|null = firstPosition.node;
// If the method is set, a user requested the speech.
const userRequested = method !== null;
@@ -584,11 +574,14 @@
return;
}
const tab = tabs[0];
- chrome.tabs.executeScript(tab.id, {
- allFrames: true,
- matchAboutBlank: true,
- code: 'document.execCommand("copy");',
+ chrome.scripting.executeScript({
+ target: {
+ tabId: tab.id!,
+ allFrames: true,
+ },
+ func: () => document.execCommand('copy'),
});
+
if (userRequested) {
MetricsUtils.recordStartEvent(methodNumber, this.prefsManager_);
}
@@ -636,8 +629,10 @@
* keystroke but nothing was selected.
*/
private onNullSelection_(): void {
+ this.maybeCreateOffscreenDocument_();
+
if (!this.shouldShowNavigationControls_()) {
- this.nullSelectionTone_.play();
+ chrome.runtime.sendMessage(undefined, {command: 'playNullSelectionTone'});
return;
}
@@ -807,7 +802,11 @@
},
tabs => {
tabs.forEach(tab => {
- chrome.tabs.executeScript(tab.id, {file: script});
+ chrome.scripting.executeScript({
+ target: {tabId: tab.id!},
+ files: [script],
+ world: chrome.scripting.ExecutionWorld.MAIN,
+ });
});
});
}
@@ -815,7 +814,10 @@
/**
* Set up event listeners user input.
*/
- private setUpEventListeners_(): void {
+ private async setUpEventListeners_(): Promise<void> {
+ // Offscreen documented necessary for InputHandler.
+ await this.maybeCreateOffscreenDocument_();
+
this.inputHandler_ = new InputHandler({
// canStartSelecting: Whether mouse selection can begin.
canStartSelecting: () => {
@@ -1212,7 +1214,7 @@
MetricsUtils.recordTtsEngineUsed(voiceName, this.prefsManager_);
this.ttsManager_.speak(
- // TODO(b/314203187): Options may be undefined.
+ // TODO(crbug.com/314203187): Options may be undefined.
nodeGroup.text, options!, this.prefsManager_.isNetworkVoice(voiceName),
fallbackVoiceName);
}
@@ -1820,6 +1822,43 @@
// Desktop already loaded.
callback();
}
+
+ /**
+ * Creates an offscreen document if it does not yet exist.
+ * The offscreen document is used to play sounds (the null tone)
+ * and listen to copy/paste document events in InputHandler.
+ */
+ async maybeCreateOffscreenDocument_() {
+ const offscreenUrl =
+ chrome.runtime.getURL('select_to_speak/offscreen.html');
+
+ const existingContexts = await chrome.runtime.getContexts({
+ contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT],
+ documentUrls: [offscreenUrl]
+ });
+
+ if (existingContexts.length > 0) {
+ return;
+ }
+
+ if (!this.createdOffscreenDocument_) {
+ this.createdOffscreenDocument_ = chrome.offscreen.createDocument({
+ url: offscreenUrl,
+ reasons: [
+ chrome.offscreen.Reason.AUDIO_PLAYBACK,
+ chrome.offscreen.Reason.DOM_PARSER
+ ],
+ justification: 'Use the audio element and monitor clipboard',
+ });
+ }
+ try {
+ await this.createdOffscreenDocument_;
+ } catch (error) {
+ console.error('Failed to create offscreen document: ', error);
+ throw error;
+ }
+ this.createdOffscreenDocument_ = null;
+ }
}
TestImportManager.exportForTesting(getGSuiteAppRoot);
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/select_to_speak_main.ts b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/select_to_speak_main.ts
index 5905c5e..3d41f04 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/select_to_speak_main.ts
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak/mv3/select_to_speak_main.ts
@@ -6,7 +6,8 @@
import '/common/async_util.js';
import '/common/event_generator.js';
-import {InstanceChecker} from '/common/instance_checker.js';
+import {KeepAlive} from '/common/keep_alive.js';
+import {InstanceChecker} from '/common/mv3/instance_checker.js';
import {TestImportManager} from '/common/testing/test_import_manager.js';
import {SelectToSpeak} from './select_to_speak.js';
@@ -14,6 +15,9 @@
export let selectToSpeak: SelectToSpeak;
if (InstanceChecker.isActiveInstance()) {
+ // Prevent this service worker from going to sleep.
+ KeepAlive.init();
+
selectToSpeak = new SelectToSpeak();
TestImportManager.exportForTesting(['selectToSpeak', selectToSpeak]);
}
diff --git a/chrome/browser/resources/chromeos/accessibility/select_to_speak_manifest.json.jinja2 b/chrome/browser/resources/chromeos/accessibility/select_to_speak_manifest.json.jinja2
index 6a0b784..377a6df 100644
--- a/chrome/browser/resources/chromeos/accessibility/select_to_speak_manifest.json.jinja2
+++ b/chrome/browser/resources/chromeos/accessibility/select_to_speak_manifest.json.jinja2
@@ -23,6 +23,10 @@
{% endif %}
"permissions": [
"accessibilityPrivate",
+{% if is_manifest_v3 == '1' %}
+ "offscreen",
+ "scripting",
+{% endif %}
"commandLinePrivate",
"metricsPrivate",
"settingsPrivate",
@@ -36,6 +40,12 @@
"clipboardWrite",
"contextMenus"
],
+{% if is_manifest_v3 == '1' %}
+ "host_permissions": [
+ "https://docs.google.com/*",
+ "https://docs.sandbox.google.com/*"
+ ],
+{% endif %}
"icons": {
"128": "select_to_speak/sts-icon-128.png",
"16": "select_to_speak/sts-icon-16.png",
@@ -48,11 +58,18 @@
"content_scripts": [
{
"matches": [ "https://docs.google.com/document*",
- "https://docs.sandbox.google.com/document*" ],
+ "https://docs.sandbox.google.com/document*"],
"all_frames": true,
+{% if is_manifest_v3 == '1' %}
+ "js": [
+ "select_to_speak/gdocs_script.js"
+ ],
+ "world": "MAIN",
+{% else %}
"js": [
"common/gdocs_script.js"
],
+{% endif %}
"run_at": "document_start"
}
]