Add "Home" directory support.

Update FilesActivityUiTests to verify Home is present
    and that clicking a root sets the title accordingly.
Guard addition of WRITABLE flag with a volume test.

Bug: 25147243
Change-Id: Ic20372737cae08a82af0aade0c0dbbd8c22d5f34
diff --git a/packages/DocumentsUI/res/drawable/ic_root_home.xml b/packages/DocumentsUI/res/drawable/ic_root_home.xml
new file mode 100644
index 0000000..0a258ac
--- /dev/null
+++ b/packages/DocumentsUI/res/drawable/ic_root_home.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+
+    <path
+        android:fillColor="#000000"
+        android:pathData="M20 6h-8l-2-2H4c-1.1 0-1.99 .9 -1.99 2L2 18c0 1.1 .9 2 2 2h16c1.1 0 2-.9
+2-2V8c0-1.1-.9-2-2-2zm-5 3c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm4
+8h-8v-1c0-1.33 2.67-2 4-2s4 .67 4 2v1z" />
+    <path
+        android:pathData="M0 0h24v24H0z" />
+</vector>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
index beff196..4c844c4 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java
@@ -297,7 +297,7 @@
 
             for (final RootInfo root : roots) {
                 final RootItem item = new RootItem(root);
-                if (root.isLibrary()) {
+                if (root.isLibrary() || root.isHome()) {
                     libraries.add(item);
                 } else {
                     others.add(item);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
index 723700d..ae5644d 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/RootInfo.java
@@ -52,7 +52,7 @@
     public static final int TYPE_DOWNLOADS = 5;
     public static final int TYPE_LOCAL = 6;
     public static final int TYPE_MTP = 7;
-    public static final int TYPE_CLOUD = 8;
+    public static final int TYPE_OTHER = 8;
 
     public String authority;
     public String rootId;
@@ -168,7 +168,10 @@
         derivedMimeTypes = (mimeTypes != null) ? mimeTypes.split("\n") : null;
 
         // TODO: remove these special case icons
-        if (isExternalStorage()) {
+        if (isHome()) {
+            derivedIcon = R.drawable.ic_root_home;
+            derivedType = TYPE_LOCAL;
+        } else if (isExternalStorage()) {
             derivedIcon = R.drawable.ic_root_sdcard;
             derivedType = TYPE_LOCAL;
         } else if (isDownloads()) {
@@ -188,7 +191,7 @@
         } else if (isMtp()) {
             derivedType = TYPE_MTP;
         } else {
-            derivedType = TYPE_CLOUD;
+            derivedType = TYPE_OTHER;
         }
     }
 
@@ -196,6 +199,13 @@
         return authority == null && rootId == null;
     }
 
+    public boolean isHome() {
+        // Note that "home" is the expected root id for the auto-created
+        // user home directory on external storage. The "home" value should
+        // match ExternalStorageProvider.ROOT_ID_HOME.
+        return isExternalStorage() && "home".equals(rootId);
+    }
+
     public boolean isExternalStorage() {
         return "com.android.externalstorage.documents".equals(authority);
     }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
index ba91c83..71d8b34 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
@@ -125,6 +125,7 @@
                 "Videos",
                 "Audio",
                 "Downloads",
+                "Home",
                 ROOT_0_ID,
                 ROOT_1_ID);
     }
@@ -136,6 +137,13 @@
         mBot.assertHasDocuments("file0.log", "file1.png", "file2.csv");
     }
 
+    public void testRootClickSetsWindowTitle() throws Exception {
+        initTestFiles();
+
+        mBot.openRoot("Home");
+        mBot.assertWindowTitle("Home");
+    }
+
     public void testFilesList_LiveUpdate() throws Exception {
         initTestFiles();
 
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java b/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java
index 5c09794..ecad061 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/UiBot.java
@@ -16,6 +16,8 @@
 
 package com.android.documentsui;
 
+import static junit.framework.Assert.assertEquals;
+
 import android.support.test.uiautomator.By;
 import android.support.test.uiautomator.BySelector;
 import android.support.test.uiautomator.UiDevice;
@@ -80,6 +82,20 @@
         mDevice.waitForIdle();
     }
 
+    void assertWindowTitle(String expected) {
+        // Turns out the title field on a window does not have
+        // an id associated with it at runtime (which confuses the hell out of me)
+        // In code we address this via "android.R.id.title".
+        UiObject2 o = find(By.text(expected));
+        // It's a bit of a conceit that we then *assert* that the title
+        // is the value that we used to identify the UiObject2.
+        // If the preceeding lookup fails, this'll choke with an NPE.
+        // But given the issue described in the comment above, we're
+        // going to do it anyway. Because we shouldn't be looking up
+        // the uiobject by it's expected content :|
+        assertEquals(expected, o.getText());
+    }
+
     void assertHasRoots(String... labels) throws UiObjectNotFoundException {
         List<String> missing = new ArrayList<>();
         for (String label : labels) {
diff --git a/packages/ExternalStorageProvider/res/values/strings.xml b/packages/ExternalStorageProvider/res/values/strings.xml
index f1c1ade..e48436e 100644
--- a/packages/ExternalStorageProvider/res/values/strings.xml
+++ b/packages/ExternalStorageProvider/res/values/strings.xml
@@ -20,6 +20,6 @@
 
     <!-- Title for documents backend that offers internal storage. [CHAR LIMIT=24] -->
     <string name="root_internal_storage">Internal storage</string>
-    <!-- Title for documents backend that offers documents. [CHAR LIMIT=24] -->
-    <string name="root_documents">Documents</string>
+    <!-- Title for user home dir. [CHAR LIMIT=24] -->
+    <string name="root_home">Home</string>
 </resources>
diff --git a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
index fcd45f2..2cedc72 100644
--- a/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
+++ b/packages/ExternalStorageProvider/src/com/android/externalstorage/ExternalStorageProvider.java
@@ -85,9 +85,11 @@
         public String docId;
         public File visiblePath;
         public File path;
+        public boolean reportAvailableBytes = true;
     }
 
     private static final String ROOT_ID_PRIMARY_EMULATED = "primary";
+    private static final String ROOT_ID_HOME = "home";
 
     private StorageManager mStorageManager;
     private Handler mHandler;
@@ -118,6 +120,7 @@
     private void updateVolumesLocked() {
         mRoots.clear();
 
+        VolumeInfo primaryVolume = null;
         final int userId = UserHandle.myUserId();
         final List<VolumeInfo> volumes = mStorageManager.getVolumes();
         for (VolumeInfo volume : volumes) {
@@ -126,6 +129,9 @@
             final String rootId;
             final String title;
             if (volume.getType() == VolumeInfo.TYPE_EMULATED) {
+                // save off the primary volume for subsequent "Home" dir initialization.
+                primaryVolume = volume;
+
                 // We currently only support a single emulated volume mounted at
                 // a time, and it's always considered the primary
                 rootId = ROOT_ID_PRIMARY_EMULATED;
@@ -152,25 +158,58 @@
                 continue;
             }
 
+            final RootInfo root = new RootInfo();
+            mRoots.put(rootId, root);
+
+            root.rootId = rootId;
+            root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
+                    | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
+
+            // Dunno when this would NOT be the case, but never hurts to be correct.
+            if (volume.isMountedWritable()) {
+                root.flags |= Root.FLAG_SUPPORTS_CREATE;
+            }
+            root.title = title;
+            if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
+                root.flags |= Root.FLAG_HAS_SETTINGS;
+            }
+            if (volume.isVisibleForRead(userId)) {
+                root.visiblePath = volume.getPathForUser(userId);
+            } else {
+                root.visiblePath = null;
+            }
+            root.path = volume.getInternalPathForUser(userId);
             try {
-                final RootInfo root = new RootInfo();
-                mRoots.put(rootId, root);
-
-                root.rootId = rootId;
-                root.flags = Root.FLAG_SUPPORTS_CREATE | Root.FLAG_LOCAL_ONLY | Root.FLAG_ADVANCED
-                        | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD;
-                root.title = title;
-                if (volume.getType() == VolumeInfo.TYPE_PUBLIC) {
-                    root.flags |= Root.FLAG_HAS_SETTINGS;
-                }
-                if (volume.isVisibleForRead(userId)) {
-                    root.visiblePath = volume.getPathForUser(userId);
-                } else {
-                    root.visiblePath = null;
-                }
-                root.path = volume.getInternalPathForUser(userId);
                 root.docId = getDocIdForFile(root.path);
+            } catch (FileNotFoundException e) {
+                throw new IllegalStateException(e);
+            }
+        }
 
+        // Finally, if primary storage is available we add the "Home" directory,
+        // creating it as needed.
+        if (primaryVolume != null && primaryVolume.isVisible()) {
+            final RootInfo root = new RootInfo();
+            root.rootId = ROOT_ID_HOME;
+            mRoots.put(root.rootId, root);
+            root.title = getContext().getString(R.string.root_home);
+
+            // Only report bytes on *volumes*...as a matter of policy.
+            root.reportAvailableBytes = false;
+            root.flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_SEARCH
+                    | Root.FLAG_SUPPORTS_IS_CHILD;
+
+            // Dunno when this would NOT be the case, but never hurts to be correct.
+            if (primaryVolume.isMountedWritable()) {
+                root.flags |= Root.FLAG_SUPPORTS_CREATE;
+            }
+
+            root.visiblePath = new File(
+                    primaryVolume.getPathForUser(userId), root.rootId);
+            root.path = new File(
+                    primaryVolume.getInternalPathForUser(userId), root.rootId);
+            try {
+                root.docId = getDocIdForFile(root.path);
             } catch (FileNotFoundException e) {
                 throw new IllegalStateException(e);
             }
@@ -312,7 +351,8 @@
                 row.add(Root.COLUMN_FLAGS, root.flags);
                 row.add(Root.COLUMN_TITLE, root.title);
                 row.add(Root.COLUMN_DOCUMENT_ID, root.docId);
-                row.add(Root.COLUMN_AVAILABLE_BYTES, root.path.getFreeSpace());
+                row.add(Root.COLUMN_AVAILABLE_BYTES,
+                        root.reportAvailableBytes ? root.path.getFreeSpace() : -1);
             }
         }
         return result;