CrOS FilesApp: Crostini shared path management

New JS class Crostini to manage crostini shared paths.
Record paths that are shared, and do not show 'Share with Linux'
option for paths that are already shared.

Update Crostini UI test:
 * Verify that 'Share with Linux' is not shown for files (only dirs).
 * Fixed drive_welcome.css path.

Bug: 878324
Change-Id: I6b8445d0a127043f9d24126384267284af5ed7ab
Reviewed-on: https://chromium-review.googlesource.com/1215154
Reviewed-by: Luciano Pacheco <lucmult@chromium.org>
Commit-Queue: Joel Hockey <joelhockey@chromium.org>
Cr-Commit-Position: refs/heads/master@{#590211}
diff --git a/chrome/browser/chromeos/file_manager/file_manager_jstest.cc b/chrome/browser/chromeos/file_manager/file_manager_jstest.cc
index 815bfe6..246f79e 100644
--- a/chrome/browser/chromeos/file_manager/file_manager_jstest.cc
+++ b/chrome/browser/chromeos/file_manager/file_manager_jstest.cc
@@ -217,3 +217,8 @@
 IN_PROC_BROWSER_TEST_F(FileManagerJsTest, UtilTest) {
   RunTest(base::FilePath(FILE_PATH_LITERAL("common/js/util_unittest.html")));
 }
+
+IN_PROC_BROWSER_TEST_F(FileManagerJsTest, Crostini) {
+  RunTest(base::FilePath(
+      FILE_PATH_LITERAL("foreground/js/crostini_unittest.html")));
+}
diff --git a/ui/file_manager/file_manager/foreground/js/BUILD.gn b/ui/file_manager/file_manager/foreground/js/BUILD.gn
index 6a40012..4387a27 100644
--- a/ui/file_manager/file_manager/foreground/js/BUILD.gn
+++ b/ui/file_manager/file_manager/foreground/js/BUILD.gn
@@ -18,6 +18,7 @@
     ":closure_compile_externs",
     ":column_visibility_controller",
     ":constants",
+    ":crostini",
     ":dialog_action_controller",
     ":dialog_type",
     ":directory_contents",
@@ -160,6 +161,12 @@
   ]
 }
 
+js_library("crostini") {
+  deps = [
+    ":volume_manager_wrapper",
+  ]
+}
+
 js_library("dialog_type") {
 }
 
@@ -304,6 +311,7 @@
 
 js_library("file_tasks") {
   deps = [
+    ":crostini",
     ":directory_model",
     ":task_history",
     ":volume_manager_wrapper",
diff --git a/ui/file_manager/file_manager/foreground/js/constants.js b/ui/file_manager/file_manager/foreground/js/constants.js
index 24bea4c..29381a5 100644
--- a/ui/file_manager/file_manager/foreground/js/constants.js
+++ b/ui/file_manager/file_manager/foreground/js/constants.js
@@ -57,3 +57,9 @@
  * @type {string}
  */
 constants.FILES_QUICK_VIEW_HTML = 'foreground/elements/files_quick_view.html';
+
+/**
+ * Path for drive_welcome.css file.  Allow override for testing.
+ * @type {string}
+ */
+constants.DRIVE_WELCOME_CSS = 'foreground/css/drive_welcome.css';
\ No newline at end of file
diff --git a/ui/file_manager/file_manager/foreground/js/crostini.js b/ui/file_manager/file_manager/foreground/js/crostini.js
new file mode 100644
index 0000000..66d2ebd
--- /dev/null
+++ b/ui/file_manager/file_manager/foreground/js/crostini.js
@@ -0,0 +1,60 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const Crostini = {};
+
+/**
+ * Maintains a list of paths shared with the crostini container.
+ * Keyed by VolumeManagerCommon.RootType, with boolean set values
+ * of string paths.  e.g. {'Downloads': {'/foo': true, '/bar': true}}.
+ * @private @dict {!Object<!Object<boolean>>}
+ */
+Crostini.SHARED_PATHS_ = {};
+
+/**
+ * Add entry as a shared path.
+ * @param {!Entry} entry
+ * @param {!VolumeManagerWrapper} volumeManager
+ */
+Crostini.addSharedPath = function(entry, volumeManager) {
+  const root = volumeManager.getLocationInfo(entry).rootType;
+  let paths = Crostini.SHARED_PATHS_[root];
+  if (!paths) {
+    paths = {};
+    Crostini.SHARED_PATHS_[root] = paths;
+  }
+  paths[entry.fullPath] = true;
+};
+
+/**
+ * Returns true if entry is shared.
+ * @param {!Entry} entry
+ * @param {!VolumeManagerWrapper} volumeManager
+ * @return {boolean} True if path is shared either by a direct
+ * share or from one of its ancestor directories.
+ */
+Crostini.isPathShared = function(entry, volumeManager) {
+  const root = volumeManager.getLocationInfo(entry).rootType;
+  const paths = Crostini.SHARED_PATHS_[root];
+  if (!paths)
+    return false;
+  // Check path and all ancestor directories.
+  let path = entry.fullPath;
+  while (path != '') {
+    if (paths[path])
+      return true;
+    path = path.substring(0, path.lastIndexOf('/'));
+  }
+  return false;
+};
+
+/**
+ * @param {!Entry} entry
+ * @param {!VolumeManagerWrapper} volumeManager
+ * @return {boolean} True if the entry is from crostini.
+ */
+Crostini.isCrostiniEntry = function(entry, volumeManager) {
+  return volumeManager.getLocationInfo(entry).rootType ===
+      VolumeManagerCommon.RootType.CROSTINI;
+};
diff --git a/ui/file_manager/file_manager/foreground/js/crostini_unittest.html b/ui/file_manager/file_manager/foreground/js/crostini_unittest.html
new file mode 100644
index 0000000..7fe283a
--- /dev/null
+++ b/ui/file_manager/file_manager/foreground/js/crostini_unittest.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<!-- Copyright 2018 The Chromium Authors. All rights reserved.
+  -- Use of this source code is governed by a BSD-style license that can be
+  -- found in the LICENSE file.
+  -->
+
+<script src="../../common/js/mock_entry.js"></script>
+
+<script src="crostini.js"></script>
+<script src="crostini_unittest.js"></script>
diff --git a/ui/file_manager/file_manager/foreground/js/crostini_unittest.js b/ui/file_manager/file_manager/foreground/js/crostini_unittest.js
new file mode 100644
index 0000000..798f6f5
--- /dev/null
+++ b/ui/file_manager/file_manager/foreground/js/crostini_unittest.js
@@ -0,0 +1,29 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+const volumeManager = {
+  getLocationInfo: (entry) => {
+    return {root: 'testroot'};
+  },
+};
+
+function testIsPathShared() {
+  const mockFileSystem = new MockFileSystem('volumeId');
+  const root = new MockDirectoryEntry(mockFileSystem, '/');
+  const foo1 = new MockDirectoryEntry(mockFileSystem, '/foo1');
+  const foobar1 = new MockDirectoryEntry(mockFileSystem, '/foo1/bar1');
+  const foo2 = new MockDirectoryEntry(mockFileSystem, '/foo2');
+  const foobar2 = new MockDirectoryEntry(mockFileSystem, '/foo2/bar2');
+
+  assertFalse(Crostini.isPathShared(foo1, volumeManager));
+
+  Crostini.addSharedPath(foo1, volumeManager);
+  assertFalse(Crostini.isPathShared(root, volumeManager));
+  assertTrue(Crostini.isPathShared(foo1, volumeManager));
+  assertTrue(Crostini.isPathShared(foobar1, volumeManager));
+
+  Crostini.addSharedPath(foobar2, volumeManager);
+  assertFalse(Crostini.isPathShared(foo2, volumeManager));
+  assertTrue(Crostini.isPathShared(foobar2, volumeManager));
+}
diff --git a/ui/file_manager/file_manager/foreground/js/file_manager_commands.js b/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
index 78bb289..a2adba6f 100644
--- a/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
+++ b/ui/file_manager/file_manager/foreground/js/file_manager_commands.js
@@ -1656,12 +1656,16 @@
   execute: function(event, fileManager) {
     const entry = CommandUtil.getCommandEntry(event.target);
     if (entry && entry.isDirectory) {
+      const dir = /** @type {!DirectoryEntry} */ (entry);
       chrome.fileManagerPrivate.sharePathWithCrostiniContainer(
-          /** @type {!DirectoryEntry} */ (entry), () => {
-            if (chrome.runtime.lastError)
+          dir, () => {
+            if (chrome.runtime.lastError) {
               console.error(
                   'Error sharing with linux: ' +
                   chrome.runtime.lastError.message);
+            } else {
+              Crostini.addSharedPath(dir, assert(fileManager.volumeManager));
+            }
           });
     }
   },
@@ -1670,10 +1674,11 @@
    * @param {!CommandHandlerDeps} fileManager CommandHandlerDeps to use.
    */
   canExecute: function(event, fileManager) {
-    // Must be single directory subfolder of Downloads.
+    // 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;
diff --git a/ui/file_manager/file_manager/foreground/js/file_tasks.js b/ui/file_manager/file_manager/foreground/js/file_tasks.js
index 8a79bb7..7611dd5 100644
--- a/ui/file_manager/file_manager/foreground/js/file_tasks.js
+++ b/ui/file_manager/file_manager/foreground/js/file_tasks.js
@@ -188,7 +188,7 @@
       // a dialog with an error message, similar to when attempting to run
       // Crostini tasks with non-Crostini entries.
       if (entries.length !== 1 ||
-          !FileTasks.isCrostiniEntry_(entries[0], volumeManager)) {
+          !Crostini.isCrostiniEntry(entries[0], volumeManager)) {
         taskItems = taskItems.filter(function(item) {
           var taskParts = item.taskId.split('|');
           var appId = taskParts[0];
@@ -449,23 +449,12 @@
 };
 
 /**
- * @param {!Entry} entry
- * @param {!VolumeManagerWrapper} volumeManager
- * @return {boolean} True if the entry is from crostini.
- * @private
- */
-FileTasks.isCrostiniEntry_ = function(entry, volumeManager) {
-  return volumeManager.getLocationInfo(entry).rootType ===
-      VolumeManagerCommon.RootType.CROSTINI;
-};
-
-/**
  * @return {boolean} True if all entries are crostini.
  * @private
  */
 FileTasks.prototype.allCrostiniEntries_ = function() {
   return this.entries_.every(
-      entry => FileTasks.isCrostiniEntry_(entry, this.volumeManager_));
+      entry => Crostini.isCrostiniEntry(entry, this.volumeManager_));
 };
 
 /**
diff --git a/ui/file_manager/file_manager/foreground/js/file_tasks_unittest.html b/ui/file_manager/file_manager/foreground/js/file_tasks_unittest.html
index 9d867f05..2adea43 100644
--- a/ui/file_manager/file_manager/foreground/js/file_tasks_unittest.html
+++ b/ui/file_manager/file_manager/foreground/js/file_tasks_unittest.html
@@ -15,5 +15,6 @@
 
 <script src="constants.js"></script>
 <script src="web_store_utils.js"></script>
+<script src="crostini.js"></script>
 <script src="file_tasks.js"></script>
 <script src="file_tasks_unittest.js"></script>
diff --git a/ui/file_manager/file_manager/foreground/js/main_scripts.js b/ui/file_manager/file_manager/foreground/js/main_scripts.js
index 087ddd9..3fb4869 100644
--- a/ui/file_manager/file_manager/foreground/js/main_scripts.js
+++ b/ui/file_manager/file_manager/foreground/js/main_scripts.js
@@ -108,6 +108,7 @@
 // <include src="actions_model.js">
 // <include src="app_state_controller.js">
 // <include src="column_visibility_controller.js">
+// <include src="crostini.js">
 // <include src="dialog_action_controller.js">
 // <include src="dialog_type.js">
 // <include src="directory_contents.js">
diff --git a/ui/file_manager/file_manager/foreground/js/task_controller_unittest.html b/ui/file_manager/file_manager/foreground/js/task_controller_unittest.html
index 9257ce7..f23b8e0 100644
--- a/ui/file_manager/file_manager/foreground/js/task_controller_unittest.html
+++ b/ui/file_manager/file_manager/foreground/js/task_controller_unittest.html
@@ -20,6 +20,7 @@
 <script src="../../common/js/unittest_util.js"></script>
 <script src="../../common/js/util.js"></script>
 <script src="../../common/js/volume_manager_common.js"></script>
+<script src="crostini.js"></script>
 <script src="dialog_type.js"></script>
 <script src="file_selection.js"></script>
 <script src="file_tasks.js"></script>
diff --git a/ui/file_manager/file_manager/foreground/js/ui/banners.js b/ui/file_manager/file_manager/foreground/js/ui/banners.js
index 408c7f5..95d3862 100644
--- a/ui/file_manager/file_manager/foreground/js/ui/banners.js
+++ b/ui/file_manager/file_manager/foreground/js/ui/banners.js
@@ -212,7 +212,7 @@
   if (!this.document_.querySelector('link[drive-welcome-style]')) {
     var style = this.document_.createElement('link');
     style.rel = 'stylesheet';
-    style.href = 'foreground/css/drive_welcome.css';
+    style.href = constants.DRIVE_WELCOME_CSS;
     style.setAttribute('drive-welcome-style', '');
     this.document_.head.appendChild(style);
   }
diff --git a/ui/file_manager/file_manager/test/crostini.js b/ui/file_manager/file_manager/test/crostini.js
index 603689a..3ef5dc0 100644
--- a/ui/file_manager/file_manager/test/crostini.js
+++ b/ui/file_manager/file_manager/test/crostini.js
@@ -228,6 +228,7 @@
 };
 
 // Verify right-click menu with 'Share with Linux' is not shown for:
+// * Files (not directory)
 // * Root Downloads folder
 // * Any folder outside of downloads (e.g. crostini or drive)
 crostini.testSharePathNotShown = (done) => {
@@ -240,6 +241,15 @@
 
   test.setupAndWaitUntilReady()
       .then(() => {
+        // Right-click 'hello.txt' file.
+        // Check 'Share with Linux' is not shown in menu.
+        test.selectFile('hello.txt');
+        assertTrue(
+            test.fakeMouseRightClick('#file-list li[selected]'),
+            'right-click hello.txt');
+        return test.waitForElement(menuNoShareWithLinux);
+      })
+      .then(() => {
         // Select 'My files' in directory tree to show Downloads in file list.
         assertTrue(test.fakeMouseClick(myFiles), 'click My files');
         return test.waitForElement(downloads);
diff --git a/ui/file_manager/file_manager/test/js/test_util.js b/ui/file_manager/file_manager/test/js/test_util.js
index 572f80e..0004e04 100644
--- a/ui/file_manager/file_manager/test/js/test_util.js
+++ b/ui/file_manager/file_manager/test/js/test_util.js
@@ -9,6 +9,7 @@
 
 // Update paths for testing.
 constants.FILES_QUICK_VIEW_HTML = 'test/gen/elements/files_quick_view.html';
+constants.DRIVE_WELCOME_CSS = FILE_MANAGER_ROOT + constants.DRIVE_WELCOME_CSS;
 
 // Stores Blobs loaded from src/chrome/test/data/chromeos/file_manager.
 test.DATA = {