blob: fbe5af1914adab801a25af5443b91571ac1b8e4a [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Entry point for the Image Loader's service worker.
*/
import {assert} from 'chrome://resources/js/assert.js';
import type {LoadImageRequest, LoadImageResponse} from './load_image_request.js';
import type {PrivateApi} from './sw_od_messages.js';
const EXTENSION_ID = 'pmfjbimdmchhbnneeidfognadeopoehp';
const ALLOW_LISTED_FILE_MANAGER_SWA = 'chrome://file-manager';
const ALLOW_LISTED_NATIVE = 'com.google.ash_thumbnail_loader';
// setupOffscreenDocument is based on
// https://developer.chrome.com/docs/extensions/reference/api/offscreen#maintain_the_lifecycle_of_an_offscreen_document
let creatingOffscreenDocument: Promise<void>|undefined;
async function setupOffscreenDocument() {
const url = chrome.runtime.getURL('offscreen.html');
const existingContexts = await chrome.runtime.getContexts({
contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT],
documentUrls: [url],
});
if (existingContexts.length > 0) {
return;
}
if (creatingOffscreenDocument) {
await creatingOffscreenDocument;
return;
}
creatingOffscreenDocument = chrome.offscreen.createDocument({
url: url,
// 'USER_MEDIA' isn't quite right, since we never call getUserMedia, but
// it's the closest listed reason from
// https://developer.chrome.com/docs/extensions/reference/api/offscreen#type-Reason
reasons: [chrome.offscreen.Reason.USER_MEDIA],
justification: 'Lets us use <img>, <canvas> and XMLHttpRequest',
});
await creatingOffscreenDocument;
creatingOffscreenDocument = undefined;
}
// Forwards a message from the service worker to the offscreen document.
//
// sendResponse will not be called if msg.cancel is truthy. See the
// ImageLoader.handle method.
async function sendToOffscreenDocument(
msg: LoadImageRequest, senderOrigin: string,
sendResponse: (r: LoadImageResponse) => void) {
assert(msg);
try {
await setupOffscreenDocument();
} catch (e) {
console.error('setupOffscreenDocument:', e);
return;
}
msg.imageLoaderRequestId = senderOrigin + ':' + msg.taskId;
chrome.runtime.sendMessage(
null, msg, undefined, (response: LoadImageResponse) => {
sendResponse(response);
});
}
// Compare the hard-coded-at-compile-or-package-time script version with the
// loaded-at-runtime manifest version. Chrome has a service worker script
// cache, which is usually flushed whenever a Chrome extension is upgraded to a
// new version. But the ChromeOS Files App (and its image loader Chrome
// extension) is unusual (for a Chrome extension) in that it's built in to the
// OS image and does not go through an extension's typical "uninstall the old;
// install the new" mechanisms. Even when the OS image ships a newer version,
// the service worker script cache will serve the older, stale version. This
// conditional chrome.runtime.reload() call works around that.
//
// https://groups.google.com/a/google.com/g/chromeos-files-syd/c/dBhXhPGGkg0/m/pw3Gv3TLAAAJ
//
// When updating this '0.5' string, keep it synchronized with what's in
// manifest.json near the ยง.
if (chrome.runtime.getManifest().version !== '0.5') {
chrome.runtime.reload();
} else {
// Handle imageLoaderPrivate calls on behalf of the offscreen document.
chrome.runtime.onMessage.addListener(
(msg: PrivateApi, sender: chrome.runtime.MessageSender,
sendResponse: (thumbnailDataUrl: string) => void) => {
if (sender.id !== EXTENSION_ID) {
return false;
} else if (msg.apiMethod === 'getArcDocumentsProviderThumbnail') {
chrome.imageLoaderPrivate.getArcDocumentsProviderThumbnail(
msg.params.url,
msg.params.widthHint,
msg.params.heightHint,
sendResponse,
);
return true;
} else if (msg.apiMethod === 'getDriveThumbnail') {
chrome.imageLoaderPrivate.getDriveThumbnail(
msg.params.url,
msg.params.cropToSquare,
sendResponse,
);
return true;
} else if (msg.apiMethod === 'getPdfThumbnail') {
chrome.imageLoaderPrivate.getPdfThumbnail(
msg.params.url,
msg.params.width,
msg.params.height,
sendResponse,
);
return true;
}
return false;
});
// Listen to messages from the Files app SWA.
chrome.runtime.onMessageExternal.addListener(
(msg: LoadImageRequest, sender: chrome.runtime.MessageSender,
sendResponse: (r: LoadImageResponse) => void) => {
if (!msg || (sender.origin !== ALLOW_LISTED_FILE_MANAGER_SWA)) {
return false;
}
sendToOffscreenDocument(
msg, ALLOW_LISTED_FILE_MANAGER_SWA, sendResponse);
// Return true (or false) if the runtime should keep sendResponse
// valid-to-call (or invalid) after this closure returns.
//
// If msg.cancel is truthy then sendToOffscreenDocument will not call
// sendResponse, so we can return false.
//
// If msg.cancel is falsy then sendToOffscreenDocument will call it,
// but also asynchronously, so return true.
return !msg.cancel;
});
// Listen to messages from ash-chrome itself, e.g. thumbnails in the "tote"
// (also known as the "holding space"), after taking a screenshot.
//
// Each native connection is expected to send a single request only, so we
// disconnect after handling a message.
chrome.runtime.onConnectNative.addListener((port: chrome.runtime.Port) => {
assert(port.sender);
if (port.sender.nativeApplication !== ALLOW_LISTED_NATIVE) {
port.disconnect();
return;
}
port.onMessage.addListener((msg: LoadImageRequest) => {
if (!msg) {
port.disconnect();
return;
} else if (msg.cancel) {
port.disconnect();
}
sendToOffscreenDocument(
msg, ALLOW_LISTED_NATIVE, (response: LoadImageResponse) => {
assert(!msg.cancel);
port.postMessage(response);
port.disconnect();
});
});
});
}