Commit dec38a71 authored by Joel Hockey's avatar Joel Hockey Committed by Commit Bot

CrOS FilesApp: Share crostini entries before opening

When a user opens files within Downloads using a crostini app,
show a dialog for the user to give permission to share the
directory that the files are in with crostini.

* Move handling of 'crostin-files' flag checking into
  foreground/js/crostini.js
* Change FileTasks.executeInternal to detect files that
  can be shared with crostini and do sharing before execute.
* UI Test verifies share dialog is shown before execute.
* ConvertFileSystemURLToPathInsideCrostini maps
  path from within Downloads to '/ChromeOS/Downloads/...'

Bug: 878324
Change-Id: Ie88090ff958dd201aeacb23fdfcc84a4f3d20210
Reviewed-on: https://chromium-review.googlesource.com/c/1243871Reviewed-by: default avatarNicholas Verne <nverne@chromium.org>
Reviewed-by: default avatarLuciano Pacheco <lucmult@chromium.org>
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Cr-Commit-Position: refs/heads/master@{#596003}
parent 2068a3fc
......@@ -1036,12 +1036,21 @@
<message name="IDS_FILE_BROWSER_NO_TASK_FOR_CRX" desc="Message shown when a user tries to open a *.crx file, which we don't handle in the Files app.">
We're constantly looking for ways to make your browsing safer. Previously, any website could prompt you to add an extension into your browser. In the latest versions of Google Chrome, you must explicitly tell Chrome that you want to install these extensions by adding them through the Extensions page. <ph name="BEGIN_LINK">&lt;a target='_blank' href='https://support.google.com/chrome_webstore/answer/2664769?p=crx_warning&amp;rd=1'&gt;</ph>Learn more<ph name="END_LINK">&lt;/a&gt;</ph>
</message>
<message name="IDS_FILE_BROWSER_UNABLE_TO_OPEN_CROSTINI_TITLE" desc="Message title shown when a user tries to use a crostini app to open a file outside the crostini container (e.g. in Downloads). This message will be removed once we support this action.">
<message name="IDS_FILE_BROWSER_UNABLE_TO_OPEN_CROSTINI_TITLE" desc="Message title shown when a user tries to use a crostini app to open a file which cannot be shared with the crostini container (e.g. in Play or USB). This message will be removed once we support this action.">
Unable to open with $1
</message>
<message name="IDS_FILE_BROWSER_UNABLE_TO_OPEN_CROSTINI" desc="Message shown when a user tries to use a crostini app to open a file outside the crostini container (e.g. in Downloads). This message will be removed once we support this action.">
<message name="IDS_FILE_BROWSER_UNABLE_TO_OPEN_CROSTINI" desc="Message shown when a user tries to use a crostini app to open a file which cannot be shared with the crostini container (e.g. in Play or USB). This message will be removed once we support this action.">
To open files with $1, first copy to Linux files folder.
</message>
<message name="IDS_FILE_BROWSER_SHARE_BEFORE_OPEN_CROSTINI_TITLE" desc="Message title shown when a user tries to use a crostini app to open a file outside the crostini container which can be shared with the container (e.g. in Downloads).">
Share files with Linux
</message>
<message name="IDS_FILE_BROWSER_SHARE_BEFORE_OPEN_CROSTINI_SINGLE" desc="Message shown when a user tries to use a crostini app to open a single file outside the crostini container which can be shared with the container (e.g. in Downloads).">
Let Linux apps open $1.
</message>
<message name="IDS_FILE_BROWSER_SHARE_BEFORE_OPEN_CROSTINI_MULTIPLE" desc="Message shown when a user tries to use a crostini app to multiple files outside the crostini container which can be shared with the container (e.g. in Downloads).">
Let Linux apps open $1 files.
</message>
<message name="IDS_FILE_BROWSER_FOLDER" desc="Folder entry type">
Folder
......
848e13f48a12b5f1acf8e08366f66914133aca82
\ No newline at end of file
1103d69da142202431923da3129ea3c1f33b0908
\ No newline at end of file
1103d69da142202431923da3129ea3c1f33b0908
\ No newline at end of file
......@@ -376,10 +376,14 @@ std::string ContainerUserNameForProfile(Profile* profile) {
return container_username;
}
base::FilePath HomeDirectoryForProfile(Profile* profile) {
base::FilePath ContainerHomeDirectoryForProfile(Profile* profile) {
return base::FilePath("/home/" + ContainerUserNameForProfile(profile));
}
base::FilePath ContainerChromeOSBaseDirectory() {
return base::FilePath("/ChromeOS/");
}
std::string AppNameFromCrostiniAppId(const std::string& id) {
return kCrostiniAppNamePrefix + id;
}
......
......@@ -75,7 +75,11 @@ std::string CryptohomeIdForProfile(Profile* profile);
std::string ContainerUserNameForProfile(Profile* profile);
// Returns the home directory within the container for a given profile.
base::FilePath HomeDirectoryForProfile(Profile* profile);
base::FilePath ContainerHomeDirectoryForProfile(Profile* profile);
// Returns the mount directory within the container where paths from the Chrome
// OS host such as within Downloads are shared with the container.
base::FilePath ContainerChromeOSBaseDirectory();
// The Terminal opens Crosh but overrides the Browser's app_name so that we can
// identify it as the Crostini Terminal. In the future, we will also use these
......
......@@ -758,6 +758,12 @@ ExtensionFunction::ResponseAction FileManagerPrivateGetStringsFunction::Run() {
SET_STRING("MEDIA_TITLE_COLUMN_LABEL",
IDS_FILE_BROWSER_MEDIA_TITLE_COLUMN_LABEL);
SET_STRING("RECENT_ROOT_LABEL", IDS_FILE_BROWSER_RECENT_ROOT_LABEL);
SET_STRING("SHARE_BEFORE_OPEN_CROSTINI_MULTIPLE",
IDS_FILE_BROWSER_SHARE_BEFORE_OPEN_CROSTINI_MULTIPLE);
SET_STRING("SHARE_BEFORE_OPEN_CROSTINI_SINGLE",
IDS_FILE_BROWSER_SHARE_BEFORE_OPEN_CROSTINI_SINGLE);
SET_STRING("SHARE_BEFORE_OPEN_CROSTINI_TITLE",
IDS_FILE_BROWSER_SHARE_BEFORE_OPEN_CROSTINI_TITLE);
SET_STRING("SUGGEST_DIALOG_INSTALLATION_FAILED",
IDS_FILE_BROWSER_SUGGEST_DIALOG_INSTALLATION_FAILED);
SET_STRING("SUGGEST_DIALOG_LINK_TO_WEBSTORE",
......
......@@ -182,14 +182,29 @@ std::vector<std::string> GetCrostiniMountOptions(
std::string ConvertFileSystemURLToPathInsideCrostini(
Profile* profile,
const storage::FileSystemURL& file_system_url) {
std::string id(file_system_url.mount_filesystem_id());
std::string mount_point_name_crostini = GetCrostiniMountPointName(profile);
std::string mount_point_name_downloads = GetDownloadsMountPointName(profile);
DCHECK(file_system_url.mount_type() == storage::kFileSystemTypeExternal);
DCHECK(file_system_url.type() == storage::kFileSystemTypeNativeLocal);
// Reformat virtual_path()
// from <mount_label>/path/to/file
// to /<home-directory>/path/to/file
base::FilePath folder(util::GetCrostiniMountPointName(profile));
base::FilePath result = HomeDirectoryForProfile(profile);
DCHECK(id == mount_point_name_crostini || id == mount_point_name_downloads);
// Reformat virtual_path() from:
// <mount_label>/path/to/file
// To either:
// /<home-directory>/path/to/file (path is already in crostini volume)
// /ChromeOS/<volume_id>/path/to/file (path is shared with crostini)
base::FilePath result;
base::FilePath folder;
if (id == mount_point_name_crostini) {
folder = base::FilePath(mount_point_name_crostini);
result = ContainerHomeDirectoryForProfile(profile);
} else if (id == mount_point_name_downloads) {
folder = base::FilePath(mount_point_name_downloads);
result = ContainerChromeOSBaseDirectory().Append(kDownloadsFolderName);
} else {
NOTREACHED();
}
bool success =
folder.AppendRelativePath(file_system_url.virtual_path(), &result);
DCHECK(success);
......
......@@ -49,9 +49,9 @@ const char kLsbRelease[] =
TEST(FileManagerPathUtilTest, GetDownloadLocationText) {
content::TestBrowserThreadBundle thread_bundle;
content::TestServiceManagerContext service_manager_context;
TestingProfileManager profile_manager(TestingBrowserProcess::GetGlobal());
TestingProfile profile(base::FilePath("/home/chronos/u-0123456789abcdef"));
EXPECT_EQ("Downloads",
GetDownloadLocationText(&profile, "/home/chronos/user/Downloads"));
EXPECT_EQ("Downloads",
......@@ -148,6 +148,33 @@ TEST(FileManagerPathUtilTest, MultiProfileDownloadsFolderMigration) {
&path));
}
TEST(FileManagerPathUtilTest, ConvertFileSystemURLToPathInsideCrostini) {
content::TestBrowserThreadBundle thread_bundle;
TestingProfile profile;
storage::ExternalMountPoints* mount_points =
storage::ExternalMountPoints::GetSystemInstance();
// Register crostini and downloads.
mount_points->RegisterFileSystem(
GetCrostiniMountPointName(&profile), storage::kFileSystemTypeNativeLocal,
storage::FileSystemMountOption(), GetCrostiniMountDirectory(&profile));
mount_points->RegisterFileSystem(
GetDownloadsMountPointName(&profile), storage::kFileSystemTypeNativeLocal,
storage::FileSystemMountOption(), GetDownloadsFolderForProfile(&profile));
EXPECT_EQ("/home/testing_profile/path/in/crostini",
ConvertFileSystemURLToPathInsideCrostini(
&profile, mount_points->CreateExternalFileSystemURL(
GURL(), "crostini_test_termina_penguin",
base::FilePath("path/in/crostini"))));
EXPECT_EQ("/ChromeOS/Downloads/path/in/downloads",
ConvertFileSystemURLToPathInsideCrostini(
&profile,
mount_points->CreateExternalFileSystemURL(
GURL(), "Downloads", base::FilePath("path/in/downloads"))));
}
std::unique_ptr<KeyedService> CreateFileSystemOperationRunnerForTesting(
content::BrowserContext* context) {
return arc::ArcFileSystemOperationRunner::CreateForTesting(
......
......@@ -4,6 +4,12 @@
const Crostini = {};
/**
* Set from cmd line flag 'crostini-files'.
* @type {boolean}
*/
Crostini.IS_CROSTINI_FILES_ENABLED = false;
/**
* Maintains a list of paths shared with the crostini container.
* Keyed by VolumeManagerCommon.RootType, with boolean set values
......@@ -75,3 +81,16 @@ Crostini.isCrostiniEntry = function(entry, volumeManager) {
return volumeManager.getLocationInfo(entry).rootType ===
VolumeManagerCommon.RootType.CROSTINI;
};
/**
* Returns true if entry can be shared with Crostini.
* @param {!Entry} entry
* @param {!VolumeManager} volumeManager
*/
Crostini.canSharePath = function(entry, volumeManager) {
// Do not allow root, or non-directories in root.
return Crostini.IS_CROSTINI_FILES_ENABLED && entry.fullPath !== '/' &&
(entry.isDirectory || entry.fullPath.split('/').length > 2) &&
volumeManager.getLocationInfo(entry).rootType ===
VolumeManagerCommon.RootType.DOWNLOADS;
};
......@@ -5,6 +5,7 @@
-->
<script src="../../common/js/mock_entry.js"></script>
<script src="../../common/js/volume_manager_common.js"></script>
<script src="crostini.js"></script>
<script src="crostini_unittest.js"></script>
......@@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const volumeManager = {
const volumeManagerTest = {
getLocationInfo: (entry) => {
return {root: 'testroot'};
return {rootType: 'testroot'};
},
};
......@@ -16,17 +16,46 @@ function testIsPathShared() {
const foo2 = new MockDirectoryEntry(mockFileSystem, '/foo2');
const foobar2 = new MockDirectoryEntry(mockFileSystem, '/foo2/bar2');
assertFalse(Crostini.isPathShared(foo1, volumeManager));
assertFalse(Crostini.isPathShared(foo1, volumeManagerTest));
Crostini.registerSharedPath(foo1, volumeManager);
assertFalse(Crostini.isPathShared(root, volumeManager));
assertTrue(Crostini.isPathShared(foo1, volumeManager));
assertTrue(Crostini.isPathShared(foobar1, volumeManager));
Crostini.registerSharedPath(foo1, volumeManagerTest);
assertFalse(Crostini.isPathShared(root, volumeManagerTest));
assertTrue(Crostini.isPathShared(foo1, volumeManagerTest));
assertTrue(Crostini.isPathShared(foobar1, volumeManagerTest));
Crostini.registerSharedPath(foobar2, volumeManager);
assertFalse(Crostini.isPathShared(foo2, volumeManager));
assertTrue(Crostini.isPathShared(foobar2, volumeManager));
Crostini.registerSharedPath(foobar2, volumeManagerTest);
assertFalse(Crostini.isPathShared(foo2, volumeManagerTest));
assertTrue(Crostini.isPathShared(foobar2, volumeManagerTest));
Crostini.unregisterSharedPath(foobar2, volumeManager);
assertFalse(Crostini.isPathShared(foobar2, volumeManager));
Crostini.unregisterSharedPath(foobar2, volumeManagerTest);
assertFalse(Crostini.isPathShared(foobar2, volumeManagerTest));
}
const volumeManagerDownloads = {
getLocationInfo: (entry) => {
return {rootType: 'downloads'};
},
};
function testCanSharePath() {
Crostini.IS_CROSTINI_FILES_ENABLED = true;
const mockFileSystem = new MockFileSystem('volumeId');
const root = new MockDirectoryEntry(mockFileSystem, '/');
const rootFile = new MockEntry(mockFileSystem, '/file');
const rootFolder = new MockDirectoryEntry(mockFileSystem, '/folder');
const fooFile = new MockEntry(mockFileSystem, '/foo/folder');
const fooFolder = new MockDirectoryEntry(mockFileSystem, '/foo/folder');
assertFalse(Crostini.canSharePath(root, volumeManagerTest));
assertFalse(Crostini.canSharePath(rootFile, volumeManagerTest));
assertFalse(Crostini.canSharePath(rootFolder, volumeManagerTest));
assertFalse(Crostini.canSharePath(fooFile, volumeManagerTest));
assertFalse(Crostini.canSharePath(fooFolder, volumeManagerTest));
assertFalse(Crostini.canSharePath(root, volumeManagerDownloads));
assertFalse(Crostini.canSharePath(rootFile, volumeManagerDownloads));
assertTrue(Crostini.canSharePath(rootFolder, volumeManagerDownloads));
assertTrue(Crostini.canSharePath(fooFile, volumeManagerDownloads));
assertTrue(Crostini.canSharePath(fooFolder, volumeManagerDownloads));
}
......@@ -1222,6 +1222,12 @@ FileManager.prototype = /** @struct */ {
*/
FileManager.prototype.setupCrostini_ = function() {
chrome.fileManagerPrivate.isCrostiniEnabled((enabled) => {
// Check for 'crostini-files' cmd line flag.
chrome.commandLinePrivate.hasSwitch('crostini-files', (filesEnabled) => {
Crostini.IS_CROSTINI_FILES_ENABLED = filesEnabled;
});
// Setup Linux files fake root.
this.directoryTree.dataModel.linuxFilesItem = enabled ?
new NavigationModelFakeItem(
str('LINUX_FILES_ROOT_LABEL'), NavigationModelItemType.CROSTINI,
......@@ -1229,6 +1235,7 @@ FileManager.prototype = /** @struct */ {
str('LINUX_FILES_ROOT_LABEL'),
VolumeManagerCommon.RootType.CROSTINI, true)) :
null;
// Redraw the tree even if not enabled. This is required for testing.
this.directoryTree.redraw(false);
......@@ -1241,7 +1248,6 @@ FileManager.prototype = /** @struct */ {
Crostini.registerSharedPath(entries[i], assert(this.volumeManager_));
}
});
});
};
......
......@@ -356,13 +356,6 @@ var CommandHandler = function(fileManager, selectionHandler) {
'disable-zip-archiver-packer', function(disabled) {
CommandHandler.IS_ZIP_ARCHIVER_PACKER_ENABLED_ = !disabled;
});
chrome.fileManagerPrivate.isCrostiniEnabled((enabled) => {
if (enabled) {
chrome.commandLinePrivate.hasSwitch('crostini-files', (enabled) => {
CommandHandler.IS_CROSTINI_FILES_ENABLED_ = enabled;
});
}
});
};
/**
......@@ -372,13 +365,6 @@ var CommandHandler = function(fileManager, selectionHandler) {
*/
CommandHandler.IS_ZIP_ARCHIVER_PACKER_ENABLED_ = false;
/**
* A flag that determines whether crostini file sharing is enabled.
* @type {boolean}
* @private
*/
CommandHandler.IS_CROSTINI_FILES_ENABLED_ = false;
/**
* Supported disk file system types for renaming.
* @type {!Array<!VolumeManagerCommon.FileSystemType>}
......@@ -1676,12 +1662,9 @@ CommandHandler.COMMANDS_['share-with-linux'] = /** @type {Command} */ ({
canExecute: function(event, fileManager) {
// Must be single directory subfolder of Downloads not already shared.
const entries = CommandUtil.getCommandEntries(event.target);
event.canExecute = CommandHandler.IS_CROSTINI_FILES_ENABLED_ &&
entries.length === 1 && entries[0].isDirectory &&
!Crostini.isPathShared(entries[0], assert(fileManager.volumeManager)) &&
entries[0].fullPath !== '/' &&
fileManager.volumeManager.getLocationInfo(entries[0]).rootType ===
VolumeManagerCommon.RootType.DOWNLOADS;
event.canExecute = entries.length === 1 && entries[0].isDirectory &&
!Crostini.isPathShared(entries[0], fileManager.volumeManager) &&
Crostini.canSharePath(entries[0], fileManager.volumeManager);
event.command.setHidden(!event.canExecute);
}
});
......
......@@ -439,24 +439,6 @@ FileTasks.isOpenTask = function(task) {
return !task.verb || task.verb == chrome.fileManagerPrivate.Verb.OPEN_WITH;
};
/**
* @param {string} taskId Task identifier.
* @return {boolean} True if the task ID is for crostini.
* @private
*/
FileTasks.isCrostiniTask_ = function(taskId) {
return taskId.split('|', 2)[1] === 'crostini';
};
/**
* @return {boolean} True if all entries are crostini.
* @private
*/
FileTasks.prototype.allCrostiniEntries_ = function() {
return this.entries_.every(
entry => Crostini.isCrostiniEntry(entry, this.volumeManager_));
};
/**
* Annotates tasks returned from the API.
*
......@@ -564,6 +546,102 @@ FileTasks.annotateTasks_ = function(tasks, entries) {
return result;
};
/**
* Checks if task is a crostini task and all entries are accessible to crostini.
* If entries can be shared with crostini, share dialog is shown. Otherwise if
* entries cannot be shared, the Unable to Open dialog is shown.
* @param {!chrome.fileManagerPrivate.FileTask} task Task to run.
* @return {boolean} True if crostini task and dialog is shown.
* @private
*/
FileTasks.prototype.maybeShowCrostiniShareDialog_ = function(task) {
// Check if this is a crostini task.
if (task.taskId.split('|', 2)[1] !== 'crostini' || this.entries_.length < 1)
return false;
let showUnableToOpen = false;
let showShareBeforeOpen = false;
let notSharedCount = 0;
let firstEntryNotShared;
for (let i = 0; i < this.entries_.length; i++) {
const entry = this.entries_[i];
if (Crostini.isCrostiniEntry(entry, this.volumeManager_) ||
Crostini.isPathShared(entry, this.volumeManager_)) {
continue;
}
if (!Crostini.canSharePath(entry, this.volumeManager_)) {
showUnableToOpen = true;
break;
}
notSharedCount++;
// Share before open. Ensure all entries are in the same directory.
showShareBeforeOpen = true;
if (!firstEntryNotShared) {
firstEntryNotShared = entry;
} else if (!util.isSiblingEntry(entry, firstEntryNotShared)) {
showUnableToOpen = true;
break;
}
}
// Show unable to open alert dialog.
if (showUnableToOpen) {
this.ui_.alertDialog.showHtml(
strf('UNABLE_TO_OPEN_CROSTINI_TITLE', task.title),
strf('UNABLE_TO_OPEN_CROSTINI', task.title));
return true;
}
// Show share before open confirm dialog.
if (showShareBeforeOpen) {
const parts = firstEntryNotShared.fullPath.split('/');
const parentName = parts[parts.length - 2];
this.ui_.confirmDialog.showHtml(
strf('SHARE_BEFORE_OPEN_CROSTINI_TITLE', task.title),
notSharedCount > 1 ?
strf('SHARE_BEFORE_OPEN_CROSTINI_MULTIPLE', notSharedCount) :
strf(
'SHARE_BEFORE_OPEN_CROSTINI_SINGLE',
`<b>${firstEntryNotShared.name}</b>`),
this.sharePathWithCrostiniAndExecute_.bind(this, task), () => {});
return true;
}
// No dialogs.
return false;
};
/**
* Share paths from entries and execute task.
* @param {!chrome.fileManagerPrivate.FileTask} task Task to run.
*/
FileTasks.prototype.sharePathWithCrostiniAndExecute_ = function(task) {
const entry = this.entries_[0];
entry.getParent(
(/** @type {!DirectoryEntry} */ dir) => {
chrome.fileManagerPrivate.sharePathWithCrostini(dir, () => {
if (chrome.runtime.lastError) {
console.error(
'Error sharing with linux to execute: ' +
chrome.runtime.lastError.message);
} else {
// Register path as shared, and execute. This will be the 2nd
// time we have gone through executeInternal_(). The first time,
// we showed the share dialog, and did not execute, now we
// should detect that paths are already shared, and it is OK to
// execute.
Crostini.registerSharedPath(dir, this.volumeManager_);
this.executeInternal_(task);
}
});
},
(fileError) => {
console.error(
'Error getting parent for ' + entry.fullPath + '. ' +
util.getFileErrorString(fileError.name));
});
};
/**
* Executes default task.
*
......@@ -723,11 +801,8 @@ FileTasks.prototype.executeInternal_ = function(task) {
this.taskHistory_.recordTaskExecuted(task.taskId);
if (FileTasks.isInternalTask_(task.taskId)) {
this.executeInternalTask_(task.taskId);
} else if (
FileTasks.isCrostiniTask_(task.taskId) && !this.allCrostiniEntries_()) {
this.ui_.alertDialog.showHtml(
strf('UNABLE_TO_OPEN_CROSTINI_TITLE', task.title),
strf('UNABLE_TO_OPEN_CROSTINI', task.title));
} else if (this.maybeShowCrostiniShareDialog_(task)) {
// Nothing to do, dialog will be shown.
} else {
FileTasks.recordZipHandlerUMA_(task.taskId);
chrome.fileManagerPrivate.executeTask(
......
......@@ -14,6 +14,7 @@ var mockTaskHistory = {
};
loadTimeData.data = {
MORE_ACTIONS_BUTTON_LABEL: 'MORE_ACTIONS_BUTTON_LABEL',
NO_TASK_FOR_EXECUTABLE: 'NO_TASK_FOR_EXECUTABLE',
NO_TASK_FOR_FILE_URL: 'NO_TASK_FOR_FILE_URL',
NO_TASK_FOR_FILE: 'NO_TASK_FOR_FILE',
......@@ -21,9 +22,13 @@ loadTimeData.data = {
NO_TASK_FOR_CRX: 'NO_TASK_FOR_CRX',
NO_TASK_FOR_CRX_TITLE: 'NO_TASK_FOR_CRX_TITLE',
OPEN_WITH_BUTTON_LABEL: 'OPEN_WITH_BUTTON_LABEL',
SHARE_BEFORE_OPEN_CROSTINI_TITLE: 'SHARE_BEFORE_OPEN_CROSTINI_TITLE',
SHARE_BEFORE_OPEN_CROSTINI_SINGLE: 'SHARE_BEFORE_OPEN_CROSTINI_SINGLE',
SHARE_BEFORE_OPEN_CROSTINI_MULTIPLE: 'SHARE_BEFORE_OPEN_CROSTINI_MULTIPLE',
TASK_INSTALL_LINUX_PACKAGE: 'TASK_INSTALL_LINUX_PACKAGE',
TASK_OPEN: 'TASK_OPEN',
MORE_ACTIONS_BUTTON_LABEL: 'MORE_ACTIONS_BUTTON_LABEL'
UNABLE_TO_OPEN_CROSTINI_TITLE: 'UNABLE_TO_OPEN_CROSTINI_TITLE',
UNABLE_TO_OPEN_CROSTINI: 'UNABLE_TO_OPEN_CROSTINI',
};
function setUp() {
......@@ -474,3 +479,93 @@ function testOpenInstallLinuxPackageDialog(callback) {
});
reportPromise(promise, callback);
}
function testMaybeShowCrostiniShareDialog() {
const volumeManagerDownloads = {
getLocationInfo: (entry) => {
return {rootType: 'downloads'};
}
};
const mockFileSystem = new MockFileSystem('downloads');
const sharedDir = new MockDirectoryEntry(mockFileSystem, '/shared');
const shared = new MockFileEntry(mockFileSystem, '/shared/file');
Crostini.registerSharedPath(sharedDir, volumeManagerDownloads);
const notShared1 = new MockFileEntry(mockFileSystem, '/notShared/file1');
const notShared2 = new MockFileEntry(mockFileSystem, '/notShared/file2');
const otherNotShared =
new MockFileEntry(mockFileSystem, '/otherNotShared/file');
const expect =
(comment, entries, expectShareDialogShown, expectedTitle,
expectedMessage) => {
let showHtmlCalled = false;
const showHtml = (title, message) => {
showHtmlCalled = true;
assertEquals(
expectedTitle, title, 'crostini share dialog title: ' + comment);
assertEquals(
expectedMessage, message,
'crostini share dialog message: ' + comment);
};
const fakeFilesTask = {
entries_: entries,
ui_: {
alertDialog: {showHtml: showHtml},
confirmDialog: {showHtml: showHtml},
},
sharePathWithCrostiniAndExecute_: () => {},
volumeManager_: volumeManagerDownloads,
};
const crostiniTask = {taskId: '|crostini|'};
const shareDialogShown =
FileTasks.prototype.maybeShowCrostiniShareDialog_.call(
fakeFilesTask, crostiniTask);
assertEquals(
expectShareDialogShown, shareDialogShown,
'dialog shown: ' + comment);
assertEquals(
expectShareDialogShown, showHtmlCalled,
'showHtml called:' + comment);
};
expect('No entries', [], false, '', '');
Crostini.IS_CROSTINI_FILES_ENABLED = false;
expect(
'Single entry, crostini-files not enabled', [notShared1], true,
'UNABLE_TO_OPEN_CROSTINI_TITLE', 'UNABLE_TO_OPEN_CROSTINI');
Crostini.IS_CROSTINI_FILES_ENABLED = true;
expect(
'Single entry, not shared', [notShared1], true,
'SHARE_BEFORE_OPEN_CROSTINI_TITLE', 'SHARE_BEFORE_OPEN_CROSTINI_SINGLE');
expect('Single entry, shared', [shared], false, '', '');
expect(
'2 entries, not shared, same dir', [notShared1, notShared2], true,
'SHARE_BEFORE_OPEN_CROSTINI_TITLE',
'SHARE_BEFORE_OPEN_CROSTINI_MULTIPLE');
expect(
'2 entries, not shared, different dir', [notShared1, otherNotShared],
true, 'UNABLE_TO_OPEN_CROSTINI_TITLE', 'UNABLE_TO_OPEN_CROSTINI');
expect(
'2 entries, 1 not shared, different dir, not shared first',
[notShared1, shared], true, 'SHARE_BEFORE_OPEN_CROSTINI_TITLE',
'SHARE_BEFORE_OPEN_CROSTINI_SINGLE');
expect(
'2 entries, 1 not shared, different dir, shared first',
[shared, notShared1], true, 'SHARE_BEFORE_OPEN_CROSTINI_TITLE',
'SHARE_BEFORE_OPEN_CROSTINI_SINGLE');
expect(
'3 entries, 2 not shared, different dir',
[shared, notShared1, notShared2], true,
'SHARE_BEFORE_OPEN_CROSTINI_TITLE',
'SHARE_BEFORE_OPEN_CROSTINI_MULTIPLE');
}
......@@ -8,7 +8,6 @@ action("create_test_main") {
script = "//ui/file_manager/file_manager/test/scripts/create_test_main.py"
output = "$target_gen_dir/../test.html"
sources = [
"../../../webui/resources/css/text_defaults.css",
"../background/js/background_common_scripts.js",
"../background/js/background_scripts.js",
"../foreground/elements/elements_bundle.html",
......@@ -18,6 +17,9 @@ action("create_test_main") {
"../foreground/js/elements_importer.js",
"../foreground/js/main_scripts.js",
"../main.html",
"//chrome/app/file_manager_strings.grdp",
"//chrome/browser/chromeos/extensions/file_manager/private_api_strings.cc",
"//ui/webui/resources/css/text_defaults.css",
"check_select.js",
"crostini.js",
"js/strings.js",
......
......@@ -133,7 +133,7 @@ crostini.testCrostiniMountOnDrag = (done) => {
});
};
crostini.testErrorOpeningDownloadsWithCrostiniApp = (done) => {
crostini.testShareBeforeOpeningDownloadsWithCrostiniApp = (done) => {
// Save old fmp.getFileTasks and replace with version that returns
// crostini app and chrome Text app.
let oldGetFileTasks = chrome.fileManagerPrivate.getFileTasks;
......@@ -152,11 +152,40 @@ crostini.testErrorOpeningDownloadsWithCrostiniApp = (done) => {
]);
};
test.setupAndWaitUntilReady()
// Save old fmp.sharePathWitCrostini.
const oldSharePath = chrome.fileManagerPrivate.sharePathWithCrostini;
let sharePathCalled = false;
chrome.fileManagerPrivate.sharePathWithCrostini = (entry, callback) => {
sharePathCalled = true;
oldSharePath(entry, callback);
};
// Save old fmp.executeTask.
const oldExecuteTask = chrome.fileManagerPrivate.executeTask;
let executeTaskCalled = false;
chrome.fileManagerPrivate.executeTask = (taskId, entries, callback) => {
executeTaskCalled = true;
oldExecuteTask(taskId, entries, callback);
};
test.setupAndWaitUntilReady([], [], [])
.then(() => {
// Add '/A', and '/A/hello.txt', refresh, 'A' is shown.
test.addEntries(
[test.ENTRIES.directoryA, test.ENTRIES.helloInA], [], []);
assertTrue(test.fakeMouseClick('#refresh-button'), 'click refresh');
return test.waitForFiles(
test.TestEntryInfo.getExpectedRows([test.ENTRIES.directoryA]));
})
.then(() => {
// Change to 'A' directory, hello.txt is shown.
assertTrue(test.fakeMouseDoubleClick('[file-name="A"]'));
return test.waitForFiles(
test.TestEntryInfo.getExpectedRows([test.ENTRIES.hello]));
})
.then(() => {
// Right click on 'hello.txt' file, wait for dialog with 'Open with'.
assertTrue(
test.fakeMouseRightClick('#listitem-' + test.maxListItemId()));
assertTrue(test.fakeMouseRightClick('[file-name="hello.txt"]'));
return test.waitForElement(
'cr-menu-item[command="#open-with"]:not([hidden]');
})
......@@ -167,7 +196,7 @@ crostini.testErrorOpeningDownloadsWithCrostiniApp = (done) => {
})
.then(() => {
// Ensure picker shows both options. Click on 'Crostini App'. Ensure
// error is shown.
// share path dialog is shown.
const list = document.querySelectorAll('#default-tasks-list li div');
assertEquals(2, list.length);
assertEquals('Open with Crostini App', list[0].innerText);
......@@ -175,27 +204,36 @@ crostini.testErrorOpeningDownloadsWithCrostiniApp = (done) => {
assertTrue(test.fakeMouseClick('#default-tasks-list li'));
return test.repeatUntil(() => {
return document.querySelector('.cr-dialog-title').innerText ===
'Unable to open with Crostini App' ||
test.pending('Waiting for Unable to open dialog');
'Share files with Linux' ||
test.pending('Waiting for share before open dialog');
});
})
.then(() => {
// Validate error messages, click 'OK' to close. Ensure dialog closes.
// Validate dialog messages, click 'OK' to share and open. Ensure
// dialog closes.
assertEquals(
'To open files with Crostini App, ' +
'first copy to Linux files folder.',
document.querySelector('.cr-dialog-text').innerText);
'Let Linux apps open <b>hello.txt</b>.',
document.querySelector('.cr-dialog-text').innerHTML);
assertTrue(test.fakeMouseClick('button.cr-dialog-ok'));
return test.waitForElementLost('.cr-dialog-container.shown');
})
.then(() => {
// Restore fmp.getFileTasks.
// Ensure fmp.sharePathWithCrostini, fmp.executeTask called.
return test.repeatUntil(() => {
return sharePathCalled && executeTaskCalled ||
test.pending('Waiting to share and open');
});
})
.then(() => {
// Restore fmp.*.
chrome.fileManagerPrivate.getFileTasks = oldGetFileTasks;
chrome.fileManagerPrivate.sharePathWithCrostini = oldSharePath;
chrome.fileManagerPrivate.executeTask = oldExecuteTask;
done();
});
};
crostini.testErrorOpeningDownloadsWithDefaultCrostiniApp = (done) => {
crostini.testErrorOpeningDownloadsRootWithDefaultCrostiniApp = (done) => {
// Save old fmp.getFileTasks and replace with version that returns
// crostini app and chrome Text app.
let oldGetFileTasks = chrome.fileManagerPrivate.getFileTasks;
......@@ -247,9 +285,11 @@ crostini.testErrorOpeningDownloadsWithDefaultCrostiniApp = (done) => {
};
crostini.testSharePathCrostiniSuccess = (done) => {
const oldSharePath = chrome.fileManagerPrivate.sharePathWithCrostini;
let sharePathCalled = false;
chrome.fileManagerPrivate.sharePathWithCrostini = (callback) => {
chrome.fileManagerPrivate.sharePathWithCrostini = (entry, callback) => {
sharePathCalled = true;
oldSharePath(entry, callback);
};
test.setupAndWaitUntilReady()
.then(() => {
......@@ -273,6 +313,7 @@ crostini.testSharePathCrostiniSuccess = (done) => {
.then(() => {
// Check sharePathWithCrostini is called.
assertTrue(sharePathCalled);
chrome.fileManagerPrivate.sharePathWithCrostini = oldSharePath;
done();
});
};
......
......@@ -236,6 +236,11 @@ test.ENTRIES = {
test.EntryType.FILE, 'text.txt', 'hello.mhtml', 'text/html',
test.SharedOption.NONE, 'Sep 4, 1998, 12:34 PM', 'hello.mhtml',
'51 bytes', 'HTML document'),
helloInA: new test.TestEntryInfo(
test.EntryType.FILE, 'text.txt', 'hello.txt', 'text/plain',
test.SharedOption.NONE, 'Sep 4, 1998, 12:34 PM', 'A/hello.txt',
'51 bytes', 'Plain text'),
};
/**
......@@ -492,10 +497,17 @@ test.waitForFiles = function(expected, opt_options) {
* Opens a Files app's main window and waits until it is initialized. Fills
* the window with initial files. Should be called for the first window only.
*
* @param {Array<!test.TestEntryInfo>=} opt_downloads Entries for downloads.
* @param {Array<!test.TestEntryInfo>=} opt_drive Entries for drive.
* @param {Array<!test.TestEntryInfo>=} opt_crostini Entries for crostini.
* @return {Promise} Promise to be fulfilled with the result object, which
* contains the file list.
*/
test.setupAndWaitUntilReady = function() {
test.setupAndWaitUntilReady = function(opt_downloads, opt_drive, opt_crostini) {
const entriesDownloads = opt_downloads || test.BASIC_LOCAL_ENTRY_SET;
const entriesDrive = opt_drive || test.BASIC_DRIVE_ENTRY_SET;
const entriesCrostini = opt_crostini || test.BASIC_CROSTINI_ENTRY_SET;
// Copy some functions from test.util.sync and bind to main window.
test.fakeMouseClick = test.util.sync.fakeMouseClick.bind(null, window);
test.fakeMouseDoubleClick =
......@@ -510,9 +522,7 @@ test.setupAndWaitUntilReady = function() {
return test.loadData()
.then(() => {
test.addEntries(
test.BASIC_LOCAL_ENTRY_SET, test.BASIC_DRIVE_ENTRY_SET,
test.BASIC_CROSTINI_ENTRY_SET);
test.addEntries(entriesDownloads, entriesDrive, entriesCrostini);
return test.waitForElement(
'#directory-tree [volume-type-icon="downloads"]');
})
......@@ -523,7 +533,7 @@ test.setupAndWaitUntilReady = function() {
'#refresh-button' :
'#directory-tree [volume-type-icon="downloads"]'));
return test.waitForFiles(
test.TestEntryInfo.getExpectedRows(test.BASIC_LOCAL_ENTRY_SET));
test.TestEntryInfo.getExpectedRows(entriesDownloads));
});
};
......
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