Commit df0d282f authored by Zain Afzal's avatar Zain Afzal Committed by Commit Bot

Added requestSaveFile ipc to chrome://media-app.

Google3 side of this change: ccl/320140322.

When we do a save copy operation we have to prompt the user to choose a
destination but for security reasons our call to saveFilePicker has to
be tied to a user gesture and needs to be triggered within a certain
timeframe of the user gesture being registered. Our current flow for
save copy is to do an encode of the current image then trigger the file
picker,sometimes this takes so long that our time window expires and our
call to saveFilePicker gets blocked by security policies.

This cl adds a new delegate function called "requestSaveFile" which does
just the step of calling showFilePicker to create a file handler
allowing us to do the actual file write later on once the encode is
done. As such SaveCopy now takes in a file token which the client gets
from requestSaveFile.

Additionally since this CL will land before the google3 side has been
rolled in saveCopy will still respect the old interface, calling
requestSaveFile implicitly if a token is not provided.

Change-Id: I0d06803cae819163c54f38bbcd170d064c78c316
bug: b/160658703
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2291810
Commit-Queue: Zain Afzal <zafzal@google.com>
Reviewed-by: default avatarTrent Apted <tapted@chromium.org>
Cr-Commit-Position: refs/heads/master@{#787952}
parent 090e8140
...@@ -175,10 +175,18 @@ guestMessagePipe.registerHandler(Message.NAVIGATE, async (message) => { ...@@ -175,10 +175,18 @@ guestMessagePipe.registerHandler(Message.NAVIGATE, async (message) => {
await advance(navigate.direction); await advance(navigate.direction);
}); });
guestMessagePipe.registerHandler(Message.REQUEST_SAVE_FILE, async (message) => {
const {suggestedName, mimeType} =
/** @type {!RequestSaveFileMessage} */ (message);
const handle = await pickWritableFile(suggestedName, mimeType);
/** @type {!RequestSaveFileResponse} */
const response = {token: generateToken(handle)};
return response;
});
guestMessagePipe.registerHandler(Message.SAVE_COPY, async (message) => { guestMessagePipe.registerHandler(Message.SAVE_COPY, async (message) => {
const {blob, suggestedName} = /** @type {!SaveCopyMessage} */ (message); const {blob, token} = /** @type {!SaveCopyMessage} */ (message);
const fileSystemHandle = await pickWritableFile(suggestedName, blob.type); const handle = tokenMap.get(token);
const {handle} = await getFileFromHandle(fileSystemHandle);
// Note `handle` could be the same as a `FileSystemFileHandle` that exists in // Note `handle` could be the same as a `FileSystemFileHandle` that exists in
// `tokenMap`. Possibly even the `File` currently open. But that's OK. E.g. // `tokenMap`. Possibly even the `File` currently open. But that's OK. E.g.
// the next overwrite-file request will just invoke `saveBlobToFile` in the // the next overwrite-file request will just invoke `saveBlobToFile` in the
...@@ -195,8 +203,8 @@ guestMessagePipe.registerHandler(Message.SAVE_COPY, async (message) => { ...@@ -195,8 +203,8 @@ guestMessagePipe.registerHandler(Message.SAVE_COPY, async (message) => {
*/ */
function pickWritableFile(suggestedName, mimeType) { function pickWritableFile(suggestedName, mimeType) {
const extension = suggestedName.split('.').reverse()[0]; const extension = suggestedName.split('.').reverse()[0];
// TODO(b/141587270): Add a default filename when it's supported by the native // TODO(b/161087799): Add a default filename when it's supported by the
// file api. // native file api.
/** @type {!FilePickerOptions} */ /** @type {!FilePickerOptions} */
const options = { const options = {
types: [ types: [
......
...@@ -135,12 +135,28 @@ mediaApp.ClientApiDelegate = function() {}; ...@@ -135,12 +135,28 @@ mediaApp.ClientApiDelegate = function() {};
*/ */
mediaApp.ClientApiDelegate.prototype.openFeedbackDialog = function() {}; mediaApp.ClientApiDelegate.prototype.openFeedbackDialog = function() {};
/** /**
* Saves a copy of `file` in a custom location with a custom * Request for the user to be prompted with a save file dialog. Once the user
* name which the user is prompted for via a native save file dialog. * selects a location a new file handle is created and a unique token to that
* file will be returned. This token can be then used with saveCopy(). The file
* extension on `suggestedName` and the provided `mimeType` are used to inform
* the save as dialog what file should be created. Once the Native Filesystem
* API allows, this save as dialog will additionally have the filename input be
* pre-filled with `suggestedName`.
* TODO(b/161087799): Update function description once Native Filesystem API
* supports suggestedName.
* @param {string} suggestedName
* @param {string} mimeType
* @return {!Promise<number>}
*/
mediaApp.ClientApiDelegate.prototype.requestSaveFile = function(
suggestedName, mimeType) {};
/**
* Saves a copy of `file` in the file specified by `token`.
* @param {!mediaApp.AbstractFile} file * @param {!mediaApp.AbstractFile} file
* @param {number} token
* @return {!Promise<undefined>} * @return {!Promise<undefined>}
*/ */
mediaApp.ClientApiDelegate.prototype.saveCopy = function(file) {}; mediaApp.ClientApiDelegate.prototype.saveCopy = function(file, token) {};
/** /**
* The client Api for interacting with the media app instance. * The client Api for interacting with the media app instance.
......
...@@ -20,6 +20,7 @@ const Message = { ...@@ -20,6 +20,7 @@ const Message = {
OPEN_FEEDBACK_DIALOG: 'open-feedback-dialog', OPEN_FEEDBACK_DIALOG: 'open-feedback-dialog',
OVERWRITE_FILE: 'overwrite-file', OVERWRITE_FILE: 'overwrite-file',
RENAME_FILE: 'rename-file', RENAME_FILE: 'rename-file',
REQUEST_SAVE_FILE: 'request-save-file',
SAVE_COPY: 'save-copy' SAVE_COPY: 'save-copy'
}; };
...@@ -101,21 +102,30 @@ let RenameFileResponse; ...@@ -101,21 +102,30 @@ let RenameFileResponse;
/** /**
* Message sent by the unprivileged context to the privileged context requesting * Message sent by the unprivileged context to the privileged context requesting
* for the provided file to be copied and saved in a new location which the user * for the user to be prompted with a save file dialog. Once the user selects a
* is prompted for, i.e a 'save as' operation. Once the native filesystem api * location a new file handle is created and a unique token to that file
* allows, this save as dialog will have the filename input be pre-filled with * will be returned. This token can be then used with saveCopy(). The file
* `suggestedName`. `suggestedName` is additionally used to determine the file * extension on `suggestedName` and the provided `mimeType` are used to inform
* extension which helps inform the save as dialog as to which files should be * the save as dialog what file should be created. Once the native filesystem
* overwritable. This method can be called with any file, not just the currently * api allows, this save as dialog will additionally have the filename input be
* writable file. * pre-filled with `suggestedName`.
* @typedef {{blob: !Blob, suggestedName: string}} * @typedef {{suggestedName: string, mimeType: string}}
*/ */
let SaveCopyMessage; let RequestSaveFileMessage;
/** /**
* Response message sent by the privileged context indicating the error message * Response message sent by the privileged context with a unique identifier for
* if the associated save as operation could not be performed. Returns nothing * the new writable file created on disk by the corresponding request save
* if the operation was successful. * file message.
* @typedef {{ errorMessage: ?string }} * @typedef {{token: number}}
*/ */
let SaveCopyResponse; let RequestSaveFileResponse;
/**
* Message sent by the unprivileged context to the privileged context requesting
* for the provided file to be copied and saved in the location specified by
* `token`. This method can be called with any file, not just the currently
* writable file.
* @typedef {{blob: !Blob, token: number}}
*/
let SaveCopyMessage;
...@@ -190,15 +190,40 @@ const DELEGATE = { ...@@ -190,15 +190,40 @@ const DELEGATE = {
await parentMessagePipe.sendMessage(Message.OPEN_FEEDBACK_DIALOG); await parentMessagePipe.sendMessage(Message.OPEN_FEEDBACK_DIALOG);
return /** @type {?string} */ (response['errorMessage']); return /** @type {?string} */ (response['errorMessage']);
}, },
/**
* @param {string} suggestedName
* @param {string} mimeType
* @return {!Promise<number>}
*/
async requestSaveFile(suggestedName, mimeType) {
/** @type {!RequestSaveFileMessage} */
const msg = {suggestedName, mimeType};
const response =
/** @type {!RequestSaveFileResponse} */ (
await parentMessagePipe.sendMessage(
Message.REQUEST_SAVE_FILE, msg));
return response.token;
},
/** /**
* @param {!mediaApp.AbstractFile} abstractFile * @param {!mediaApp.AbstractFile} abstractFile
* @param {number} token
* @this {mediaApp.ClientApiDelegate}
* @return {!Promise<undefined>} * @return {!Promise<undefined>}
*/ */
async saveCopy(abstractFile) { async saveCopy(abstractFile, token) {
if (token === undefined) {
// The guest frame must be running an older version of backlight which is
// assuming the `saveCopy(abstractFile)` interface. Make the
// requestSaveFile call on its behalf for backwards compatibility.
// TODO(b/160938402): remove this.
token =
await this.requestSaveFile(abstractFile.name, abstractFile.mimeType);
}
/** @type {!SaveCopyMessage} */ /** @type {!SaveCopyMessage} */
const msg = {blob: abstractFile.blob, suggestedName: abstractFile.name}; const msg = {blob: abstractFile.blob, token};
await parentMessagePipe.sendMessage(Message.SAVE_COPY, msg); await parentMessagePipe.sendMessage(Message.SAVE_COPY, msg);
} },
}; };
/** /**
......
...@@ -22,6 +22,7 @@ let TestMessageResponseData; ...@@ -22,6 +22,7 @@ let TestMessageResponseData;
* property: (string|undefined), * property: (string|undefined),
* renameLastFile: (string|undefined), * renameLastFile: (string|undefined),
* requestFullscreen: (boolean|undefined), * requestFullscreen: (boolean|undefined),
* requestSaveFile: (boolean|undefined),
* saveCopy: (boolean|undefined), * saveCopy: (boolean|undefined),
* testQuery: string, * testQuery: string,
* }} * }}
......
...@@ -84,13 +84,24 @@ async function runTestQuery(data) { ...@@ -84,13 +84,24 @@ async function runTestQuery(data) {
} catch (/** @type{!Error} */ error) { } catch (/** @type{!Error} */ error) {
result = `renameOriginalFile failed Error: ${error}`; result = `renameOriginalFile failed Error: ${error}`;
} }
} else if (data.requestSaveFile) {
const existingFile = assertCast(lastReceivedFileList).item(0);
if (!existingFile) {
result = 'requestSaveFile failed, no file loaded';
} else {
const token = await DELEGATE.requestSaveFile(
existingFile.name, existingFile.mimeType);
result = token.toString();
}
} else if (data.saveCopy) { } else if (data.saveCopy) {
const existingFile = assertCast(lastReceivedFileList).item(0); const existingFile = assertCast(lastReceivedFileList).item(0);
if (!existingFile) { if (!existingFile) {
result = 'saveCopy failed, no file loaded'; result = 'saveCopy failed, no file loaded';
} else { } else {
DELEGATE.saveCopy(existingFile); const token = await DELEGATE.requestSaveFile(
result = 'boo yah!'; existingFile.name, existingFile.mimeType);
await DELEGATE.saveCopy(existingFile, token);
result = 'file successfully saved';
} }
} else if (data.getFileErrors) { } else if (data.getFileErrors) {
result = result =
......
...@@ -824,8 +824,8 @@ TEST_F('MediaAppUIBrowserTest', 'RenameOriginalIPC', async () => { ...@@ -824,8 +824,8 @@ TEST_F('MediaAppUIBrowserTest', 'RenameOriginalIPC', async () => {
testDone(); testDone();
}); });
// Tests the IPC behind the saveCopy delegate function. // Tests the IPC behind the requestSaveFile delegate function.
TEST_F('MediaAppUIBrowserTest', 'SaveCopyIPC', async () => { TEST_F('MediaAppUIBrowserTest', 'RequestSaveFileIPC', async () => {
// Mock out choose file system entries since it can only be interacted with // Mock out choose file system entries since it can only be interacted with
// via trusted user gestures. // via trusted user gestures.
const newFileHandle = new FakeFileSystemFileHandle(); const newFileHandle = new FakeFileSystemFileHandle();
...@@ -838,13 +838,27 @@ TEST_F('MediaAppUIBrowserTest', 'SaveCopyIPC', async () => { ...@@ -838,13 +838,27 @@ TEST_F('MediaAppUIBrowserTest', 'SaveCopyIPC', async () => {
const testImage = await createTestImageFile(10, 10); const testImage = await createTestImageFile(10, 10);
await loadFile(testImage, new FakeFileSystemFileHandle()); await loadFile(testImage, new FakeFileSystemFileHandle());
const result = await sendTestMessage({saveCopy: true}); const result = await sendTestMessage({requestSaveFile: true});
assertEquals(result.testQueryResult, 'boo yah!');
const options = await chooseEntries; const options = await chooseEntries;
const lastToken = [...tokenMap.keys()].slice(-1)[0];
assertMatch(result.testQueryResult, lastToken);
assertEquals(options.types.length, 1); assertEquals(options.types.length, 1);
assertEquals(options.types[0].description, 'png'); assertEquals(options.types[0].description, 'png');
assertDeepEquals(options.types[0].accept['image/png'], ['png']); assertDeepEquals(options.types[0].accept['image/png'], ['png']);
testDone();
});
// Tests the IPC behind the saveCopy delegate function.
TEST_F('MediaAppUIBrowserTest', 'SaveCopyIPC', async () => {
// Mock out choose file system entries since it can only be interacted with
// via trusted user gestures.
const newFileHandle = new FakeFileSystemFileHandle();
window.showSaveFilePicker = () => Promise.resolve(newFileHandle);
const testImage = await createTestImageFile(10, 10);
await loadFile(testImage, new FakeFileSystemFileHandle());
const result = await sendTestMessage({saveCopy: true});
assertEquals(result.testQueryResult, 'file successfully saved');
const writeResult = await newFileHandle.lastWritable.closePromise; const writeResult = await newFileHandle.lastWritable.closePromise;
assertEquals(await writeResult.text(), await testImage.text()); assertEquals(await writeResult.text(), await testImage.text());
......
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