Merge "Style/color fixes to match UX spec." into nyc-andromeda-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 65c7cdc..286871b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -44,7 +44,7 @@
 
         <activity
             android:name=".files.LauncherActivity"
-            android:label="@string/downloads_label"
+            android:label="@string/app_label"
             android:icon="@drawable/files_icon"
             android:theme="@android:style/Theme.NoDisplay">
         </activity>
@@ -53,7 +53,7 @@
         <activity-alias
             android:name=".Launcher"
             android:targetActivity=".files.LauncherActivity"
-            android:label="@string/downloads_label"
+            android:label="@string/app_label"
             android:icon="@drawable/files_icon" >
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -63,7 +63,7 @@
 
         <activity
             android:name=".files.FilesActivity"
-            android:label="@string/downloads_label"
+            android:label="@string/app_label"
             android:icon="@drawable/files_icon"
             android:documentLaunchMode="intoExisting"
             android:theme="@style/DocumentsTheme">
diff --git a/res/layout/document_debug_info.xml b/res/layout/document_debug_info.xml
new file mode 100644
index 0000000..83664af
--- /dev/null
+++ b/res/layout/document_debug_info.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2013 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<com.android.documentsui.ui.DocumentDebugInfo
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_margin="@dimen/grid_item_margin"
+    android:textAlignment="viewStart"
+    android:typeface="monospace"
+    android:textSize="11sp"
+    android:textColor="#FF000000" />
diff --git a/res/layout/item_dir_grid.xml b/res/layout/item_dir_grid.xml
index 36af9b9..917f3f0 100644
--- a/res/layout/item_dir_grid.xml
+++ b/res/layout/item_dir_grid.xml
@@ -15,70 +15,83 @@
      limitations under the License.
 -->
 
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_margin="@dimen/grid_item_margin"
+    android:orientation="vertical"
     android:background="@color/item_doc_background"
     android:elevation="@dimen/grid_item_elevation"
-    android:focusable="true" >
+    android:focusable="true">
 
-    <!-- The height is 48px.
-         paddingTop (9dp) + @dimen/check_icon_size (30dp) + paddingBottom (9dp) -->
-    <LinearLayout
+    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="horizontal"
-        android:paddingBottom="9dp"
-        android:paddingLeft="9dp"
-        android:paddingRight="12dp"
-        android:paddingTop="9dp"
-        android:gravity="center_vertical">
+        android:layout_height="wrap_content">
 
-        <FrameLayout
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginEnd="8dp" >
+        <!-- The height is 48px.
+             paddingTop (9dp) + @dimen/check_icon_size (30dp) + paddingBottom (9dp) -->
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="horizontal"
+            android:paddingBottom="9dp"
+            android:paddingLeft="9dp"
+            android:paddingRight="12dp"
+            android:paddingTop="9dp"
+            android:gravity="center_vertical">
 
-            <ImageView
-                android:id="@+id/icon_mime_sm"
+            <FrameLayout
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_gravity="center"
-                android:contentDescription="@null"
-                android:scaleType="centerInside"
-                android:src="@drawable/ic_doc_folder" />
+                android:layout_marginEnd="8dp" >
 
-            <ImageView
-                android:id="@+id/icon_check"
-                android:layout_width="@dimen/check_icon_size"
-                android:layout_height="@dimen/check_icon_size"
-                android:alpha="0"
-                android:contentDescription="@null"
-                android:scaleType="fitCenter"
-                android:src="@drawable/ic_check_circle" />
+                <ImageView
+                    android:id="@+id/icon_mime_sm"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center"
+                    android:contentDescription="@null"
+                    android:scaleType="centerInside"
+                    android:src="@drawable/ic_doc_folder" />
 
-        </FrameLayout>
+                <ImageView
+                    android:id="@+id/icon_check"
+                    android:layout_width="@dimen/check_icon_size"
+                    android:layout_height="@dimen/check_icon_size"
+                    android:alpha="0"
+                    android:contentDescription="@null"
+                    android:scaleType="fitCenter"
+                    android:src="@drawable/ic_check_circle" />
 
-        <TextView
-            android:id="@android:id/title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:ellipsize="end"
-            android:singleLine="true"
-            android:textAlignment="viewStart"
-            android:textAppearance="@android:style/TextAppearance.Material.Subhead"
-            android:textColor="@color/item_title" />
+            </FrameLayout>
 
-    </LinearLayout>
+            <TextView
+                android:id="@android:id/title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:ellipsize="end"
+                android:singleLine="true"
+                android:textAlignment="viewStart"
+                android:textAppearance="@android:style/TextAppearance.Material.Subhead"
+                android:textColor="@color/item_title" />
 
-    <!-- An overlay that draws the item border when it is focused. -->
+        </LinearLayout>
 
-    <View
+        <!-- An overlay that draws the item border when it is focused. -->
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@drawable/item_doc_grid_border"
+            android:contentDescription="@null"
+            android:duplicateParentState="true" />
+
+    </FrameLayout>
+
+    <FrameLayout
+        android:id="@+id/debug_info"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:background="@drawable/item_doc_grid_border"
-        android:contentDescription="@null"
-        android:duplicateParentState="true" />
+        android:layout_height="wrap_content"
+        android:background="#FFEFEFEF" />
 
-</FrameLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/item_doc_grid.xml b/res/layout/item_doc_grid.xml
index 0fa9685..5b8ba30 100644
--- a/res/layout/item_doc_grid.xml
+++ b/res/layout/item_doc_grid.xml
@@ -14,126 +14,138 @@
      limitations under the License.
 -->
 
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:layout_margin="@dimen/grid_item_margin"
+    android:orientation="vertical"
     android:background="@color/item_doc_background"
     android:elevation="@dimen/grid_item_elevation"
     android:focusable="true">
 
-    <!-- Main item thumbnail.  Comprised of two overlapping images, the
-         visibility of which is controlled by code in
-         DirectoryFragment.java. -->
-
-    <FrameLayout
-        android:id="@+id/thumbnail"
+    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
         android:layout_height="wrap_content">
 
-        <com.android.documentsui.GridItemThumbnail
-            android:id="@+id/icon_thumb"
+        <!-- Main item thumbnail.  Comprised of two overlapping images, the
+             visibility of which is controlled by code in
+             DirectoryFragment.java. -->
+
+        <FrameLayout
+            android:id="@+id/thumbnail"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <com.android.documentsui.GridItemThumbnail
+                android:id="@+id/icon_thumb"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:scaleType="centerCrop"
+                android:contentDescription="@null"
+                android:tint="@color/item_doc_grid_tint"
+                android:tintMode="src_over" />
+
+            <com.android.documentsui.GridItemThumbnail
+                android:id="@+id/icon_mime_lg"
+                android:layout_width="@dimen/icon_size"
+                android:layout_height="@dimen/icon_size"
+                android:layout_gravity="center"
+                android:scaleType="fitCenter"
+                android:contentDescription="@null" />
+
+        </FrameLayout>
+
+        <!-- Item nameplate.  Has a mime-type icon and some text fields (title,
+             size, mod-time, etc). -->
+
+        <RelativeLayout
+            android:id="@+id/nameplate"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:scaleType="centerCrop"
+            android:layout_below="@id/thumbnail"
+            android:paddingTop="8dp"
+            android:paddingBottom="8dp"
+            android:paddingLeft="12dp"
+            android:paddingRight="12dp">
+
+            <ImageView
+                android:id="@+id/icon_mime_sm"
+                android:layout_width="@dimen/grid_item_icon_size"
+                android:layout_height="@dimen/grid_item_icon_size"
+                android:layout_marginEnd="8dp"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:scaleType="center"
+                android:contentDescription="@null"/>
+
+            <ImageView
+                android:id="@+id/icon_check"
+                android:src="@drawable/ic_check_circle"
+                android:alpha="0"
+                android:layout_width="@dimen/check_icon_size"
+                android:layout_height="@dimen/check_icon_size"
+                android:layout_marginEnd="8dp"
+                android:layout_alignParentStart="true"
+                android:layout_centerVertical="true"
+                android:scaleType="fitCenter"
+                android:contentDescription="@null"/>
+
+            <TextView
+                android:id="@android:id/title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentTop="true"
+                android:layout_toEndOf="@id/icon_mime_sm"
+                android:singleLine="true"
+                android:ellipsize="end"
+                android:textAlignment="viewStart"
+                android:textAppearance="@android:style/TextAppearance.Material.Subhead"
+                android:textColor="@color/item_title" />
+
+            <TextView
+                android:id="@+id/details"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_toEndOf="@id/icon_mime_sm"
+                android:layout_below="@android:id/title"
+                android:layout_marginEnd="4dp"
+                android:singleLine="true"
+                android:ellipsize="end"
+                android:textAlignment="viewStart"
+                android:textAppearance="@android:style/TextAppearance.Material.Caption"
+                android:textColor="@color/item_details" />
+
+            <TextView
+                android:id="@+id/date"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@android:id/title"
+                android:layout_toEndOf="@id/details"
+                android:singleLine="true"
+                android:ellipsize="end"
+                android:textAlignment="viewStart"
+                android:textAppearance="@android:style/TextAppearance.Material.Caption"
+                android:textColor="@color/item_details" />
+
+        </RelativeLayout>
+
+        <!-- An overlay that draws the item border when it is focused. -->
+        <View
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignBottom="@id/nameplate"
+            android:layout_alignTop="@id/thumbnail"
+            android:layout_alignLeft="@id/thumbnail"
+            android:layout_alignRight="@id/thumbnail"
             android:contentDescription="@null"
-            android:tint="@color/item_doc_grid_tint"
-            android:tintMode="src_over" />
-
-        <com.android.documentsui.GridItemThumbnail
-            android:id="@+id/icon_mime_lg"
-            android:layout_width="@dimen/icon_size"
-            android:layout_height="@dimen/icon_size"
-            android:layout_gravity="center"
-            android:scaleType="fitCenter"
-            android:contentDescription="@null" />
-
-    </FrameLayout>
-
-    <!-- Item nameplate.  Has a mime-type icon and some text fields (title,
-         size, mod-time, etc). -->
-
-    <RelativeLayout
-        android:id="@+id/nameplate"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:layout_below="@id/thumbnail"
-        android:paddingTop="8dp"
-        android:paddingBottom="8dp"
-        android:paddingLeft="12dp"
-        android:paddingRight="12dp">
-
-        <ImageView
-            android:id="@+id/icon_mime_sm"
-            android:layout_width="@dimen/grid_item_icon_size"
-            android:layout_height="@dimen/grid_item_icon_size"
-            android:layout_marginEnd="8dp"
-            android:layout_alignParentStart="true"
-            android:layout_centerVertical="true"
-            android:scaleType="center"
-            android:contentDescription="@null"/>
-
-        <ImageView
-            android:id="@+id/icon_check"
-            android:src="@drawable/ic_check_circle"
-            android:alpha="0"
-            android:layout_width="@dimen/check_icon_size"
-            android:layout_height="@dimen/check_icon_size"
-            android:layout_marginEnd="8dp"
-            android:layout_alignParentStart="true"
-            android:layout_centerVertical="true"
-            android:scaleType="fitCenter"
-            android:contentDescription="@null"/>
-
-        <TextView
-            android:id="@android:id/title"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_alignParentTop="true"
-            android:layout_toEndOf="@id/icon_mime_sm"
-            android:singleLine="true"
-            android:ellipsize="end"
-            android:textAlignment="viewStart"
-            android:textAppearance="@android:style/TextAppearance.Material.Subhead"
-            android:textColor="@color/item_title" />
-
-        <TextView
-            android:id="@+id/details"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_toEndOf="@id/icon_mime_sm"
-            android:layout_below="@android:id/title"
-            android:layout_marginEnd="4dp"
-            android:singleLine="true"
-            android:ellipsize="end"
-            android:textAlignment="viewStart"
-            android:textAppearance="@android:style/TextAppearance.Material.Caption"
-            android:textColor="@color/item_details" />
-
-        <TextView
-            android:id="@+id/date"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_below="@android:id/title"
-            android:layout_toEndOf="@id/details"
-            android:singleLine="true"
-            android:ellipsize="end"
-            android:textAlignment="viewStart"
-            android:textAppearance="@android:style/TextAppearance.Material.Caption"
-            android:textColor="@color/item_details" />
+            android:background="@drawable/item_doc_grid_border"
+            android:duplicateParentState="true" />
 
     </RelativeLayout>
 
-    <!-- An overlay that draws the item border when it is focused. -->
-    <View
-        android:layout_width="wrap_content"
+    <FrameLayout
+        android:id="@+id/debug_info"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_alignBottom="@id/nameplate"
-        android:layout_alignTop="@id/thumbnail"
-        android:layout_alignLeft="@id/thumbnail"
-        android:layout_alignRight="@id/thumbnail"
-        android:contentDescription="@null"
-        android:background="@drawable/item_doc_grid_border"
-        android:duplicateParentState="true" />
-
-</RelativeLayout>
+        android:background="#FFEFEFEF" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index ec8c544..17b9574 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -15,16 +15,13 @@
 -->
 
 <resources>
-    <!-- Allow Advanced Devices default value to be customised -->
-    <bool name="config_defaultAdvancedDevices">false</bool>
-
     <!-- Intentionally unset. Vendors should set this in an overlay. -->
     <string name="trusted_quick_viewer_package" translatable="false"></string>
 
-    <!-- Flags setup as productivity oriented in which case Downloads app will be presented
-             as Files app. Including showing of the Documents and "advanced" roots. -->
-    <bool name="productivity_device">false</bool>
+    <!-- Enable productivity oriented features like "Documents" root, and new window view. -->
+    <bool name="productivity_device">true</bool>
 
-    <!-- Indicates if search view is taking the whole toolbar space -->
+    <!-- Indicates if search view is taking the whole toolbar space. On larger
+         layouts we reduce this to an input-box adjacent to menu actions. -->
     <bool name="full_bar_search_view">true</bool>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index ce6f247..ecd598e 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -24,10 +24,9 @@
     <dimen name="list_item_thumbnail_size">40dp</dimen>
     <dimen name="grid_item_icon_size">30dp</dimen>
     <dimen name="progress_bar_height">4dp</dimen>
+    <fraction name="grid_scale_min">85%</fraction>
+    <fraction name="grid_scale_max">200%</fraction>
     <dimen name="grid_width">152dp</dimen>
-    <dimen name="grid_height">176dp</dimen>
-    <dimen name="grid_item_width">152dp</dimen>
-    <dimen name="grid_item_height">176dp</dimen>
     <dimen name="grid_section_separator_height">0dp</dimen>
     <dimen name="grid_item_margin">6dp</dimen>
     <dimen name="grid_padding_horiz">4dp</dimen>
diff --git a/res/values/drawables.xml b/res/values/drawables.xml
index 3cb6c76..2d2acbb 100644
--- a/res/values/drawables.xml
+++ b/res/values/drawables.xml
@@ -16,6 +16,6 @@
 
 <resources>
     <item name="app_icon" type="drawable">@mipmap/ic_app_icon</item>
-    <item name="files_icon" type="drawable">@mipmap/ic_launcher_downloads</item>
+    <item name="files_icon" type="drawable">@mipmap/ic_app_icon</item>
     <item name="picker_icon" type="drawable">@drawable/ic_doc_text</item>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 1fbf564..ebb92f3 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -18,9 +18,6 @@
     <!-- Title of the documents application [CHAR LIMIT=32] -->
     <string name="app_label">Files</string>
 
-    <!-- Title of the standalone downloads activity. [CHAR LIMIT=32] -->
-    <string name="downloads_label">Downloads</string>
-
     <!-- Action bar title prompting user to choose a location to open a document from [CHAR LIMIT=32] -->
     <string name="title_open">Open from</string>
     <!-- Action bar title prompting user to choose a location to save a document to [CHAR LIMIT=32] -->
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index f24085d..e9360ab 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -303,7 +303,7 @@
                 Shared.mustShowDeviceRoot(intent) || getScopedPreferences().getShowDeviceRoot();
 
         // Only show the toggle if advanced isn't forced enabled.
-        state.showAdvancedOption = !Shared.mustShowDeviceRoot(intent);
+        state.showDeviceStorageOption = !Shared.mustShowDeviceRoot(intent);
 
         if (DEBUG) Log.d(mTag, "Created new state object: " + state);
 
@@ -431,6 +431,12 @@
         }
 
         mNavigator.update();
+        // Causes talkback to announce the activity's new title
+        if (mState.stack.isRecents()) {
+            setTitle(mRoots.getRecentsRoot().title);
+        } else {
+            setTitle(mState.stack.getTitle());
+        }
         invalidateOptionsMenu();
     }
 
@@ -732,7 +738,7 @@
         });
     }
 
-    public final class RetainedState {
+    public static final class RetainedState {
         public @Nullable Selection selection;
 
         public boolean hasSelection() {
diff --git a/src/com/android/documentsui/DirectoryReloadLock.java b/src/com/android/documentsui/DirectoryReloadLock.java
index 8033bb7..b44a963 100644
--- a/src/com/android/documentsui/DirectoryReloadLock.java
+++ b/src/com/android/documentsui/DirectoryReloadLock.java
@@ -16,8 +16,11 @@
 
 package com.android.documentsui;
 
+import static com.android.documentsui.base.Shared.DEBUG;
+
 import android.annotation.MainThread;
 import android.annotation.Nullable;
+import android.util.Log;
 
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.selection.BandController;
@@ -27,6 +30,8 @@
  * while Band Selection is active.
  */
 public final class DirectoryReloadLock {
+    private static final String TAG = "DirectoryReloadLock";
+
     private int mPauseCount = 0;
     private @Nullable Runnable mCallback;
 
@@ -37,6 +42,7 @@
     public void block() {
         Shared.checkMainLoop();
         mPauseCount++;
+        if (DEBUG) Log.v(TAG, "Block count increments to " + mPauseCount + ".");
     }
 
     /**
@@ -46,7 +52,9 @@
     @MainThread
     public void unblock() {
         Shared.checkMainLoop();
+        assert(mPauseCount > 0);
         mPauseCount--;
+        if (DEBUG) Log.v(TAG, "Block count decrements to " + mPauseCount + ".");
         if (mPauseCount == 0 && mCallback != null) {
             mCallback.run();
             mCallback = null;
diff --git a/src/com/android/documentsui/MenuManager.java b/src/com/android/documentsui/MenuManager.java
index e261c9f..0d9b188 100644
--- a/src/com/android/documentsui/MenuManager.java
+++ b/src/com/android/documentsui/MenuManager.java
@@ -203,8 +203,8 @@
     }
 
     protected void updateAdvanced(MenuItem advanced) {
-        advanced.setVisible(mState.showAdvancedOption);
-        advanced.setTitle(mState.showAdvancedOption && mState.showAdvanced
+        advanced.setVisible(mState.showDeviceStorageOption);
+        advanced.setTitle(mState.showDeviceStorageOption && mState.showAdvanced
                 ? R.string.menu_advanced_hide : R.string.menu_advanced_show);
     }
 
@@ -303,10 +303,6 @@
             mActivity = activity;
         }
 
-        public boolean shouldShowFancyFeatures() {
-            return Shared.shouldShowFancyFeatures(mActivity);
-        }
-
         public boolean hasRootSettings() {
             return mActivity.getCurrentRoot().hasSettings();
         }
diff --git a/src/com/android/documentsui/archives/ArchiveId.java b/src/com/android/documentsui/archives/ArchiveId.java
index d136de1..ae23bce 100644
--- a/src/com/android/documentsui/archives/ArchiveId.java
+++ b/src/com/android/documentsui/archives/ArchiveId.java
@@ -18,7 +18,7 @@
 
 import android.net.Uri;
 
-class ArchiveId {
+public class ArchiveId {
     private final static char DELIMITER = '#';
 
     public final Uri mArchiveUri;
diff --git a/src/com/android/documentsui/archives/ArchivesProvider.java b/src/com/android/documentsui/archives/ArchivesProvider.java
index 1ebe427..fb96337 100644
--- a/src/com/android/documentsui/archives/ArchivesProvider.java
+++ b/src/com/android/documentsui/archives/ArchivesProvider.java
@@ -21,10 +21,11 @@
 import android.content.res.Configuration;
 import android.database.ContentObserver;
 import android.database.Cursor;
-import android.database.MatrixCursor;
 import android.database.MatrixCursor.RowBuilder;
+import android.database.MatrixCursor;
 import android.graphics.Point;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.CancellationSignal;
 import android.os.ParcelFileDescriptor;
 import android.provider.DocumentsContract.Document;
@@ -40,13 +41,9 @@
 import java.io.Closeable;
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.concurrent.Callable;
 import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReadWriteLock;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 /**
  * Provides basic implementation for creating, extracting and accessing
@@ -95,9 +92,23 @@
     public Cursor queryChildDocuments(String documentId, @Nullable String[] projection,
             @Nullable String sortOrder)
             throws FileNotFoundException {
+        final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
         Loader loader = null;
         try {
             loader = obtainInstance(documentId);
+            if (loader.mArchive == null) {
+                final MatrixCursor cursor = new MatrixCursor(
+                        projection != null ? projection : Archive.DEFAULT_PROJECTION);
+                // Return an empty cursor with EXTRA_LOADING, which shows spinner
+                // in DocumentsUI. Once the archive is loaded, the notification will
+                // be sent, and the directory reloaded.
+                final Bundle bundle = new Bundle();
+                bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true);
+                cursor.setExtras(bundle);
+                cursor.setNotificationUri(getContext().getContentResolver(),
+                        buildUriForArchive(archiveId.mArchiveUri));
+                return cursor;
+            }
             return loader.get().queryChildDocuments(documentId, projection, sortOrder);
         } finally {
             releaseInstance(loader);
@@ -138,19 +149,26 @@
             throws FileNotFoundException {
         final ArchiveId archiveId = ArchiveId.fromDocumentId(documentId);
         if (archiveId.mPath.equals("/")) {
-            // For the archive's root directory return hard-coded cursor, so clients know that
-            // it's actually a directory and queryChildDocuments() can be called on it.
-            //
-            // TODO: Move this code to the Archive class, once opening archives is moved to
-            // background.
-            final MatrixCursor cursor = new MatrixCursor(
-                    projection != null ? projection : Archive.DEFAULT_PROJECTION);
-            final RowBuilder row = cursor.newRow();
-            row.add(Document.COLUMN_DOCUMENT_ID, documentId);
-            row.add(Document.COLUMN_DISPLAY_NAME, "Archive");  // TODO: Fix.
-            row.add(Document.COLUMN_SIZE, 0);
-            row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
-            return cursor;
+            try (final Cursor archiveCursor = getContext().getContentResolver().query(
+                    archiveId.mArchiveUri,
+                    new String[] { Document.COLUMN_DISPLAY_NAME },
+                    null, null, null, null)) {
+                if (archiveCursor == null || !archiveCursor.moveToFirst()) {
+                    throw new FileNotFoundException(
+                            "Cannot resolve display name of the archive.");
+                }
+                final String displayName = archiveCursor.getString(
+                        archiveCursor.getColumnIndex(Document.COLUMN_DISPLAY_NAME));
+
+                final MatrixCursor cursor = new MatrixCursor(
+                        projection != null ? projection : Archive.DEFAULT_PROJECTION);
+                final RowBuilder row = cursor.newRow();
+                row.add(Document.COLUMN_DOCUMENT_ID, documentId);
+                row.add(Document.COLUMN_DISPLAY_NAME, displayName);
+                row.add(Document.COLUMN_SIZE, 0);
+                row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
+                return cursor;
+            }
         }
 
         Loader loader = null;
@@ -269,47 +287,4 @@
         mArchives.put(id.mArchiveUri, loader);
         return loader;
     }
-
-    /**
-     * Loads an instance of Archive lazily.
-     */
-    private static final class Loader {
-        private final Context mContext;
-        private final Uri mArchiveUri;
-        private final Uri mNotificationUri;
-        private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
-        private Archive mArchive = null;
-
-        Loader(Context context, Uri archiveUri, Uri notificationUri) {
-            this.mContext = context;
-            this.mArchiveUri = archiveUri;
-            this.mNotificationUri = notificationUri;
-        }
-
-        synchronized Archive get() throws FileNotFoundException {
-            if (mArchive != null) {
-                return mArchive;
-            }
-
-            try {
-                mArchive = Archive.createForParcelFileDescriptor(
-                        mContext,
-                        mContext.getContentResolver().openFileDescriptor(
-                                mArchiveUri, "r", null /* signal */),
-                        mArchiveUri, mNotificationUri);
-            } catch (IOException e) {
-                throw new IllegalStateException(e);
-            }
-
-            return mArchive;
-        }
-
-        Lock getReadLock() {
-            return mLock.readLock();
-        }
-
-        Lock getWriteLock() {
-            return mLock.writeLock();
-        }
-    }
 }
diff --git a/src/com/android/documentsui/archives/Loader.java b/src/com/android/documentsui/archives/Loader.java
new file mode 100644
index 0000000..1acdc15
--- /dev/null
+++ b/src/com/android/documentsui/archives/Loader.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.archives;
+
+import android.content.Context;
+import android.net.Uri;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Loads an instance of Archive lazily.
+ */
+public class Loader {
+    private final Context mContext;
+    private final Uri mArchiveUri;
+    private final Uri mNotificationUri;
+    private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
+    private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
+    private Exception mFailureException = null;
+    public Archive mArchive = null;
+
+    Loader(Context context, Uri archiveUri, Uri notificationUri) {
+        this.mContext = context;
+        this.mArchiveUri = archiveUri;
+        this.mNotificationUri = notificationUri;
+
+        // Start loading the archive immediately in the background.
+        mExecutor.submit(this::get);
+    }
+
+    synchronized Archive get() throws FileNotFoundException {
+        if (mArchive != null) {
+            return mArchive;
+        }
+
+        // Once loading the archive failed, do not to retry opening it until the
+        // archive file has changed (the loader is deleted once we receive
+        // a notification about the archive file being changed).
+        if (mFailureException != null) {
+            throw new IllegalStateException(
+                    "Trying to perform an operation on an archive which failed to load.",
+                    mFailureException);
+        }
+
+        try {
+            mArchive = Archive.createForParcelFileDescriptor(
+                    mContext,
+                    mContext.getContentResolver().openFileDescriptor(
+                            mArchiveUri, "r", null /* signal */),
+                    mArchiveUri, mNotificationUri);
+        } catch (IOException e) {
+            mFailureException = e;
+            throw new IllegalStateException(e);
+        } catch (RuntimeException e) {
+            mFailureException = e;
+            throw e;
+        } finally {
+            // Notify observers that the root directory is loaded (or failed)
+            // so clients reload it.
+            mContext.getContentResolver().notifyChange(
+                    ArchivesProvider.buildUriForArchive(mArchiveUri),
+                    null /* observer */, false /* syncToNetwork */);
+        }
+        return mArchive;
+    }
+
+    Lock getReadLock() {
+        return mLock.readLock();
+    }
+
+    Lock getWriteLock() {
+        return mLock.writeLock();
+    }
+}
diff --git a/src/com/android/documentsui/base/DebugFlags.java b/src/com/android/documentsui/base/DebugFlags.java
new file mode 100644
index 0000000..09638bb
--- /dev/null
+++ b/src/com/android/documentsui/base/DebugFlags.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.documentsui.base;
+
+import javax.annotation.Nullable;
+
+/**
+ * Shared values that may be set by {@link DebugCommandProcessor}.
+ */
+public final class DebugFlags {
+
+    private DebugFlags() {}
+
+    private static String mQvPackage;
+    private static boolean sGestureScaleEnabled;
+    private static boolean sDocumentDetailsEnabled;
+
+    public static void setQuickViewer(@Nullable String qvPackage) {
+        mQvPackage = qvPackage;
+    }
+
+    public static @Nullable String getQuickViewer() {
+        return mQvPackage;
+    }
+
+    public static void setDocumentDetailsEnabled(boolean enabled) {
+        sDocumentDetailsEnabled = enabled;
+    }
+
+    public static boolean getDocumentDetailsEnabled() {
+        return sDocumentDetailsEnabled;
+    }
+
+    public static void setGestureScaleEnabled(boolean enabled) {
+        sGestureScaleEnabled = enabled;
+    }
+
+    public static boolean getGestureScaleEnabled() {
+        return sGestureScaleEnabled;
+    }
+}
diff --git a/src/com/android/documentsui/base/DocumentInfo.java b/src/com/android/documentsui/base/DocumentInfo.java
index f9e9b5c..afdaccc 100644
--- a/src/com/android/documentsui/base/DocumentInfo.java
+++ b/src/com/android/documentsui/base/DocumentInfo.java
@@ -31,6 +31,8 @@
 import com.android.documentsui.archives.ArchivesProvider;
 import com.android.documentsui.roots.RootCursorWrapper;
 
+import libcore.io.IoUtils;
+
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.FileNotFoundException;
@@ -42,8 +44,6 @@
 
 import javax.annotation.Nullable;
 
-import libcore.io.IoUtils;
-
 /**
  * Representation of a {@link Document}.
  */
@@ -210,14 +210,15 @@
 
     @Override
     public String toString() {
-        return "Document{"
+        return "DocumentInfo{"
                 + "docId=" + documentId
                 + ", name=" + displayName
+                + ", mimeType=" + mimeType
                 + ", isContainer=" + isContainer()
                 + ", isDirectory=" + isDirectory()
                 + ", isArchive=" + isArchive()
                 + ", isPartial=" + isPartial()
-                + ", isVirtualDocument=" + isVirtualDocument()
+                + ", isVirtual=" + isVirtual()
                 + ", isDeleteSupported=" + isDeleteSupported()
                 + ", isCreateSupported=" + isCreateSupported()
                 + ", isRenameSupported=" + isRenameSupported()
@@ -228,18 +229,10 @@
         return (flags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0;
     }
 
-    public boolean isThumbnailSupported() {
-        return (flags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
-    }
-
     public boolean isDirectory() {
         return Document.MIME_TYPE_DIR.equals(mimeType);
     }
 
-    public boolean isGridPreferred() {
-        return (flags & Document.FLAG_DIR_PREFERS_GRID) != 0;
-    }
-
     public boolean isWriteSupported() {
         return (flags & Document.FLAG_SUPPORTS_WRITE) != 0;
     }
@@ -268,7 +261,7 @@
         return isDirectory() || isArchive();
     }
 
-    public boolean isVirtualDocument() {
+    public boolean isVirtual() {
         return (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
     }
 
diff --git a/src/com/android/documentsui/base/DocumentStack.java b/src/com/android/documentsui/base/DocumentStack.java
index 9e0c580..855f55f 100644
--- a/src/com/android/documentsui/base/DocumentStack.java
+++ b/src/com/android/documentsui/base/DocumentStack.java
@@ -33,6 +33,8 @@
 import java.util.LinkedList;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 /**
  * Representation of a stack of {@link DocumentInfo}, usually the result of a
  * user-driven traversal.
@@ -45,7 +47,7 @@
     private static final int VERSION_ADD_ROOT = 2;
 
     private LinkedList<DocumentInfo> mList;
-    private RootInfo mRoot;
+    private @Nullable RootInfo mRoot;
 
     private boolean mInitialRootChanged;
     private boolean mInitialDocChanged;
@@ -78,11 +80,11 @@
     }
 
     /**
-     * Makes a new shallow copy, and pushes all docs to the new copy in the same order as they're
+     * Makes a new copy, and pushes all docs to the new copy in the same order as they're
      * passed as parameters, i.e. the last document will be at the top of the stack.
      */
     public DocumentStack(DocumentStack src, DocumentInfo... docs) {
-        mList = src.mList;
+        mList = new LinkedList<>(src.mList);
         for (DocumentInfo doc : docs) {
             mList.addLast(doc);
         }
@@ -90,7 +92,7 @@
         mRoot = src.mRoot;
     }
 
-    public RootInfo getRoot() {
+    public @Nullable RootInfo getRoot() {
         return mRoot;
     }
 
diff --git a/src/com/android/documentsui/base/ScopedPreferences.java b/src/com/android/documentsui/base/ScopedPreferences.java
index e933ad0..92cca49 100644
--- a/src/com/android/documentsui/base/ScopedPreferences.java
+++ b/src/com/android/documentsui/base/ScopedPreferences.java
@@ -18,12 +18,23 @@
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
+import android.text.TextUtils;
 
+/**
+ * Provides an interface (and runtime implementation) for preferences that are
+ * scoped (presumably to an activity). This eliminates the need to pass
+ * scoping values into {@link LocalPreferences}, as well as eliminates
+ * the static-coupling to {@link LocalPreferences} increasing testability.
+ */
 public interface ScopedPreferences {
 
     boolean getShowDeviceRoot();
     void setShowDeviceRoot(boolean display);
 
+    /**
+     * @param scope An arbitrary string representitive of the scope
+     *        for prefs that are set using this object.
+     */
     public static ScopedPreferences create(Context context, String scope) {
         return new RuntimeScopedPreferences(
                 PreferenceManager.getDefaultSharedPreferences(context), scope);
@@ -37,6 +48,8 @@
         private String mScope;
 
         private RuntimeScopedPreferences(SharedPreferences sharedPrefs, String scope)  {
+            assert(!TextUtils.isEmpty(scope));
+
             mSharedPrefs = sharedPrefs;
             mScope = scope;
         }
diff --git a/src/com/android/documentsui/base/Shared.java b/src/com/android/documentsui/base/Shared.java
index 14bb081..8e93587 100644
--- a/src/com/android/documentsui/base/Shared.java
+++ b/src/com/android/documentsui/base/Shared.java
@@ -22,6 +22,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.res.Configuration;
 import android.net.Uri;
 import android.os.Looper;
@@ -54,13 +55,6 @@
             "com.android.documentsui.PICK_COPY_DESTINATION";
 
     /**
-     * Extra flag allowing app to be opened in productivity mode (less downloadsy).
-     * Useful developers and the likes. When set to true overrides the default
-     * config value of productivity_device.
-     */
-    public static final String EXTRA_PRODUCTIVITY_MODE = "com.android.documentsui.PRODUCTIVITY";
-
-    /**
      * Extra boolean flag for {@link #ACTION_PICK_COPY_DESTINATION}, which
      * specifies if the destination directory needs to create new directory or not.
      */
@@ -202,13 +196,17 @@
             if (info.isSystemApp() || info.isUpdatedSystemApp()) {
                 final String extra = activity.getIntent().getStringExtra(
                         DocumentsContract.EXTRA_PACKAGE_NAME);
-                if (extra != null) {
+                if (extra != null && !TextUtils.isEmpty(extra)) {
                     callingPackage = extra;
                 }
             }
-        } finally {
-            return callingPackage;
+        } catch (NameNotFoundException e) {
+            // Couldn't lookup calling package info. This isn't really
+            // gonna happen, given that we're getting the name of the
+            // calling package from trusty old Activity.getCallingPackage.
+            // For that reason, we ignore this exception.
         }
+        return callingPackage;
     }
 
     /**
@@ -216,7 +214,7 @@
      * Method can be overridden if the change of the behavior of the the child activity is needed.
      */
     public static Uri getDefaultRootUri(Activity activity) {
-        return shouldShowDocumentsRoot(activity, activity.getIntent())
+        return shouldShowDocumentsRoot(activity)
                 ? DocumentsContract.buildHomeUri()
                 : DocumentsContract.buildRootUri(
                         "com.android.providers.downloads.documents", "downloads");
@@ -233,37 +231,20 @@
     }
 
     /*
-     * Returns true if app is running in "productivity mode".
-     */
-    private static boolean isProductivityMode(Context context, Intent intent) {
-        return intent.getBooleanExtra(
-                Shared.EXTRA_PRODUCTIVITY_MODE,
-                context.getResources().getBoolean(R.bool.productivity_device));
-    }
-
-    /*
      * Returns true if "Documents" root should be shown.
      */
-    public static boolean shouldShowDocumentsRoot(Context context, Intent intent) {
-        return isProductivityMode(context, intent);
+    public static boolean shouldShowDocumentsRoot(Context context) {
+        return context.getResources().getBoolean(R.bool.productivity_device);
     }
 
     /*
-     * Returns true if device root should be shown.
+     * Returns true if the local/device storage root must be visible (this also hides
+     * the option to toggle visibility in the menu.)
      */
     public static boolean mustShowDeviceRoot(Intent intent) {
         return intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false);
     }
 
-    /**
-     * Returns true if device root should be shown.
-     */
-    public static boolean shouldShowFancyFeatures(Activity activity) {
-        Intent intent = activity.getIntent();
-        return isProductivityMode(activity, intent)
-                || intent.getBooleanExtra(DocumentsContract.EXTRA_FANCY_FEATURES, false);
-    }
-
     public static void checkMainLoop() {
         if (Looper.getMainLooper() != Looper.myLooper()) {
             Log.e(TAG, "Calling from non-UI thread!");
diff --git a/src/com/android/documentsui/base/State.java b/src/com/android/documentsui/base/State.java
index fff50c9..32d3cc2 100644
--- a/src/com/android/documentsui/base/State.java
+++ b/src/com/android/documentsui/base/State.java
@@ -77,7 +77,7 @@
 
     public boolean allowMultiple;
     public boolean localOnly;
-    public boolean showAdvancedOption;
+    public boolean showDeviceStorageOption;
     public boolean showAdvanced;
     public boolean restored;
     /*
@@ -127,7 +127,7 @@
         out.writeStringArray(acceptMimes);
         out.writeInt(allowMultiple ? 1 : 0);
         out.writeInt(localOnly ? 1 : 0);
-        out.writeInt(showAdvancedOption ? 1 : 0);
+        out.writeInt(showDeviceStorageOption ? 1 : 0);
         out.writeInt(showAdvanced ? 1 : 0);
         out.writeInt(restored ? 1 : 0);
         out.writeInt(external ? 1 : 0);
@@ -151,7 +151,7 @@
             state.acceptMimes = in.readStringArray();
             state.allowMultiple = in.readInt() != 0;
             state.localOnly = in.readInt() != 0;
-            state.showAdvancedOption = in.readInt() != 0;
+            state.showDeviceStorageOption = in.readInt() != 0;
             state.showAdvanced = in.readInt() != 0;
             state.restored = in.readInt() != 0;
             state.external = in.readInt() != 0;
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index a202049..59b58f1 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -22,6 +22,8 @@
 import static com.android.documentsui.base.State.MODE_GRID;
 import static com.android.documentsui.base.State.MODE_LIST;
 
+import android.annotation.DimenRes;
+import android.annotation.FractionRes;
 import android.annotation.IntDef;
 import android.annotation.StringRes;
 import android.app.Activity;
@@ -36,6 +38,7 @@
 import android.content.Loader;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Parcelable;
@@ -66,8 +69,8 @@
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.BaseActivity.RetainedState;
 import com.android.documentsui.DirectoryLoader;
-import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.DirectoryReloadLock;
+import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.ItemDragListener;
@@ -183,6 +186,9 @@
     private GridLayoutManager mLayout;
     private int mColumnCount = 1;  // This will get updated when layout changes.
 
+    private float mLiveScale = 1.0f;
+    private @ViewMode int mMode;
+
     private MessageBar mMessageBar;
     private View mProgressBar;
 
@@ -204,7 +210,7 @@
     public View onCreateView(
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 
-        BaseActivity activity = (BaseActivity<?>) getActivity();
+        BaseActivity<?> activity = (BaseActivity<?>) getActivity();
         final View view = inflater.inflate(R.layout.fragment_directory, container, false);
 
         mMessageBar = MessageBar.create(getChildFragmentManager());
@@ -342,6 +348,7 @@
         EventHandler<InputEvent> gestureHandler = mState.allowMultiple
                 ? gestureSel::start
                 : EventHandler.createStub(false);
+
         mInputHandler = new UserInputHandler<>(
                 mActions,
                 mFocusManager,
@@ -359,7 +366,8 @@
                 mDragStartListener::onMouseDragEvent,
                 gestureSel,
                 mInputHandler,
-                mBandController);
+                mBandController,
+                this::scaleLayout);
 
         mMenuManager = mActivity.getMenuManager();
 
@@ -497,6 +505,7 @@
      * @param mode The new view mode.
      */
     private void updateLayout(@ViewMode int mode) {
+        mMode = mode;
         mColumnCount = calculateColumnCount(mode);
         if (mLayout != null) {
             mLayout.setSpanCount(mColumnCount);
@@ -511,22 +520,65 @@
         mIconHelper.setViewMode(mode);
     }
 
+    /**
+     * Updates the layout after the view mode switches.
+     * @param mode The new view mode.
+     */
+    private void scaleLayout(float scale) {
+        assert(Build.IS_DEBUGGABLE);
+        if (DEBUG) Log.v(TAG, "Handling scale event: " + scale + ", existing scale: " + mLiveScale);
+
+        if (mMode == MODE_GRID) {
+            float minScale = getFraction(R.fraction.grid_scale_min);
+            float maxScale = getFraction(R.fraction.grid_scale_max);
+            float nextScale = mLiveScale * scale;
+
+            if (DEBUG) Log.v(TAG,
+                    "Next scale " + nextScale + ", Min/max scale " + minScale + "/" + maxScale);
+
+            if (nextScale > minScale && nextScale < maxScale) {
+                if (DEBUG) Log.d(TAG, "Updating grid scale: " + scale);
+                mLiveScale = nextScale;
+                updateLayout(mMode);
+            }
+
+        } else {
+            if (DEBUG) Log.d(TAG, "List mode, ignoring scale: " + scale);
+            mLiveScale = 1.0f;
+        }
+    }
+
     private int calculateColumnCount(@ViewMode int mode) {
         if (mode == MODE_LIST) {
             // List mode is a "grid" with 1 column.
             return 1;
         }
 
-        int cellWidth = getResources().getDimensionPixelSize(R.dimen.grid_width);
-        int cellMargin = 2 * getResources().getDimensionPixelSize(R.dimen.grid_item_margin);
-        int viewPadding = mRecView.getPaddingLeft() + mRecView.getPaddingRight();
+        int cellWidth = getScaledSize(R.dimen.grid_width);
+        int cellMargin = 2 * getScaledSize(R.dimen.grid_item_margin);
+        int viewPadding =
+                (int) ((mRecView.getPaddingLeft() + mRecView.getPaddingRight()) * mLiveScale);
 
-        // RecyclerView sometimes gets a width of 0 (see b/27150284).  Clamp so that we always lay
-        // out the grid with at least 2 columns.
+        // RecyclerView sometimes gets a width of 0 (see b/27150284).
+        // Clamp so that we always lay out the grid with at least 2 columns by default.
         int columnCount = Math.max(2,
                 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin));
 
-        return columnCount;
+        // Finally with our grid count logic firmly in place, we apply any live scaling
+        // captured by the scale gesture detector.
+        return Math.max(1, Math.round(columnCount / mLiveScale));
+    }
+
+
+    /**
+     * Moderately abuse the "fraction" resource type for our purposes.
+     */
+    private float getFraction(@FractionRes int id) {
+        return getResources().getFraction(id, 1, 0);
+    }
+
+    private int getScaledSize(@DimenRes int id) {
+        return (int) (getResources().getDimensionPixelSize(id) * mLiveScale);
     }
 
     private int getDirectoryPadding(@ViewMode int mode) {
@@ -685,14 +737,6 @@
                 .withSrcs(srcs)
                 .build();
 
-        // Relay any config overrides bits present in the original intent.
-        Intent original = getActivity().getIntent();
-        if (original != null && original.hasExtra(Shared.EXTRA_PRODUCTIVITY_MODE)) {
-            intent.putExtra(
-                    Shared.EXTRA_PRODUCTIVITY_MODE,
-                    original.getBooleanExtra(Shared.EXTRA_PRODUCTIVITY_MODE, false));
-        }
-
         // Set an appropriate title on the drawer when it is shown in the picker.
         // Coupled with the fact that we auto-open the drawer for copy/move operations
         // it should basically be the thing people see first.
diff --git a/src/com/android/documentsui/dirlist/DocumentHolder.java b/src/com/android/documentsui/dirlist/DocumentHolder.java
index 48a05d8..b5c1174 100644
--- a/src/com/android/documentsui/dirlist/DocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/DocumentHolder.java
@@ -20,7 +20,7 @@
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Rect;
-import android.support.annotation.Nullable;
+import android.os.Build;
 import android.support.v7.widget.RecyclerView;
 import android.text.TextUtils;
 import android.view.KeyEvent;
@@ -28,28 +28,36 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewPropertyAnimator;
+import android.widget.FrameLayout;
 import android.widget.ImageView;
 
-import com.android.documentsui.base.Shared;
-import com.android.documentsui.base.State;
-import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.R;
+import com.android.documentsui.base.DebugFlags;
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.Events.InputEvent;
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.ui.DocumentDebugInfo;
+
+import javax.annotation.Nullable;
 
 public abstract class DocumentHolder
-        extends RecyclerView.ViewHolder implements View.OnKeyListener, DocumentDetails {
+        extends RecyclerView.ViewHolder
+        implements View.OnKeyListener, DocumentDetails {
 
     static final float DISABLED_ALPHA = 0.3f;
 
-    protected @Nullable String modelId;
+    protected final Context mContext;
+    protected final @ColorInt int mDefaultBgColor;
+    protected final @ColorInt int mSelectedBgColor;
 
-    final Context mContext;
-    final @ColorInt int mDefaultBgColor;
-    final @ColorInt int mSelectedBgColor;
+    protected @Nullable String mModelId;
+
+    private final View mSelectionHotspot;
+    private final @Nullable FrameLayout mDebugContainer;
+    private @Nullable DocumentDebugInfo mDebugInfo;
 
     // See #addKeyEventListener for details on the need for this field.
-    KeyboardEventListener mKeyEventListener;
-
-    private View mSelectionHotspot;
+    private KeyboardEventListener mKeyEventListener;
 
     public DocumentHolder(Context context, ViewGroup parent, int layout) {
         this(context, inflateLayout(context, parent, layout));
@@ -64,8 +72,9 @@
 
         mDefaultBgColor = context.getColor(R.color.item_doc_background);
         mSelectedBgColor = context.getColor(R.color.item_doc_background_selected);
-
         mSelectionHotspot = itemView.findViewById(R.id.icon_check);
+
+        mDebugContainer = (FrameLayout) itemView.findViewById(R.id.debug_info);
     }
 
     /**
@@ -74,16 +83,16 @@
      * @param modelId
      * @param state
      */
-    public abstract void bind(Cursor cursor, String modelId, State state);
+    public abstract void bind(Cursor cursor, String modelId);
 
     @Override
     public boolean hasModelId() {
-        return !TextUtils.isEmpty(modelId);
+        return !TextUtils.isEmpty(mModelId);
     }
 
     @Override
     public String getModelId() {
-        return modelId;
+        return mModelId;
     }
 
     /**
@@ -158,6 +167,24 @@
         return false;
     }
 
+    protected void includeDebugInfo(DocumentInfo doc) {
+        if (mDebugContainer == null) {
+            return;
+        }
+        if (DebugFlags.getDocumentDetailsEnabled()) {
+            assert(Build.IS_DEBUGGABLE);
+            if (mDebugInfo == null) {
+                assert(mDebugContainer.getChildAt(0) == null);
+                mDebugInfo = inflateLayout(mContext, mDebugContainer, R.layout.document_debug_info);
+                mDebugContainer.addView(mDebugInfo);
+            }
+            mDebugInfo.update(doc);
+            mDebugContainer.setVisibility(View.VISIBLE);
+        } else {
+            mDebugContainer.setVisibility(View.GONE);
+        }
+    }
+
     static void setEnabledRecursive(View itemView, boolean enabled) {
         if (itemView == null || itemView.isEnabled() == enabled) {
             return;
@@ -172,9 +199,9 @@
         }
     }
 
-    private static View inflateLayout(Context context, ViewGroup parent, int layout) {
+    private static <V extends View> V inflateLayout(Context context, ViewGroup parent, int layout) {
         final LayoutInflater inflater = LayoutInflater.from(context);
-        return inflater.inflate(layout, parent, false);
+        return (V) inflater.inflate(layout, parent, false);
     }
 
     static ViewPropertyAnimator fade(ImageView view, float alpha) {
diff --git a/src/com/android/documentsui/dirlist/GridDirectoryHolder.java b/src/com/android/documentsui/dirlist/GridDirectoryHolder.java
index 962f6bc..fb6c3c3 100644
--- a/src/com/android/documentsui/dirlist/GridDirectoryHolder.java
+++ b/src/com/android/documentsui/dirlist/GridDirectoryHolder.java
@@ -26,8 +26,10 @@
 import android.widget.TextView;
 
 import com.android.documentsui.R;
-import com.android.documentsui.base.State;
+import com.android.documentsui.base.DebugFlags;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Events.InputEvent;
+import com.android.documentsui.roots.RootCursorWrapper;
 
 final class GridDirectoryHolder extends DocumentHolder {
 
@@ -71,13 +73,18 @@
      * @param state Current display state.
      */
     @Override
-    public void bind(Cursor cursor, String modelId, State state) {
+    public void bind(Cursor cursor, String modelId) {
         assert(cursor != null);
 
-        this.modelId = modelId;
+        this.mModelId = modelId;
 
-        final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
-        mTitle.setText(docDisplayName, TextView.BufferType.SPANNABLE);
+        mTitle.setText(
+                getCursorString(cursor, Document.COLUMN_DISPLAY_NAME),
+                TextView.BufferType.SPANNABLE);
 
+        if (DebugFlags.getDocumentDetailsEnabled()) {
+            String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
+            includeDebugInfo(DocumentInfo.fromCursor(cursor, authority));
+        }
     }
 }
diff --git a/src/com/android/documentsui/dirlist/GridDocumentHolder.java b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
index 051114d..f4f7d7c 100644
--- a/src/com/android/documentsui/dirlist/GridDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/GridDocumentHolder.java
@@ -16,15 +16,12 @@
 
 package com.android.documentsui.dirlist;
 
-import static com.android.documentsui.base.DocumentInfo.getCursorInt;
 import static com.android.documentsui.base.DocumentInfo.getCursorLong;
 import static com.android.documentsui.base.DocumentInfo.getCursorString;
 
 import android.annotation.ColorInt;
 import android.content.Context;
 import android.database.Cursor;
-import android.net.Uri;
-import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.text.format.Formatter;
 import android.view.View;
@@ -33,15 +30,14 @@
 import android.widget.TextView;
 
 import com.android.documentsui.R;
-import com.android.documentsui.base.Shared;
-import com.android.documentsui.base.State;
+import com.android.documentsui.base.DebugFlags;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Events.InputEvent;
+import com.android.documentsui.base.Shared;
 import com.android.documentsui.roots.RootCursorWrapper;
 
 final class GridDocumentHolder extends DocumentHolder {
 
-    private static boolean mHideTitles;
-
     final TextView mTitle;
     final TextView mDate;
     final TextView mDetails;
@@ -52,6 +48,8 @@
     final IconHelper mIconHelper;
 
     private final @ColorInt int mDisabledBgColor;
+    // This is used in as a convenience in our bind method.
+    private final DocumentInfo mDoc = new DocumentInfo();
 
     public GridDocumentHolder(Context context, ViewGroup parent, IconHelper iconHelper) {
         super(context, parent, R.layout.item_doc_grid);
@@ -123,18 +121,12 @@
      * @param state Current display state.
      */
     @Override
-    public void bind(Cursor cursor, String modelId, State state) {
+    public void bind(Cursor cursor, String modelId) {
         assert(cursor != null);
 
-        this.modelId = modelId;
+        mModelId = modelId;
 
-        final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
-        final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
-        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-        final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
-        final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
-        final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
-        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+        mDoc.updateFromCursor(cursor, getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY));
 
         mIconHelper.stopLoading(mIconThumb);
 
@@ -143,38 +135,36 @@
         mIconThumb.animate().cancel();
         mIconThumb.setAlpha(0f);
 
-        final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
-        mIconHelper.load(uri, docMimeType, docFlags, docIcon, docLastModified, mIconThumb,
-                mIconMimeLg, mIconMimeSm);
+        mIconHelper.load(mDoc, mIconThumb, mIconMimeLg, mIconMimeSm);
 
-        if (mHideTitles) {
-            mTitle.setVisibility(View.GONE);
-        } else {
-            mTitle.setText(docDisplayName, TextView.BufferType.SPANNABLE);
-            mTitle.setVisibility(View.VISIBLE);
-        }
+        mTitle.setText(mDoc.displayName, TextView.BufferType.SPANNABLE);
+        mTitle.setVisibility(View.VISIBLE);
 
         // If file is partial, we want to show summary field as that's more relevant than fileSize
         // and date
-        if ((docFlags & Document.FLAG_PARTIAL) != 0) {
+        if (mDoc.isPartial()) {
             final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
             mDetails.setVisibility(View.VISIBLE);
             mDate.setText(null);
             mDetails.setText(docSummary);
         } else {
-            if (docLastModified == -1) {
+            if (mDoc.lastModified == -1) {
                 mDate.setText(null);
             } else {
-                mDate.setText(Shared.formatTime(mContext, docLastModified));
+                mDate.setText(Shared.formatTime(mContext, mDoc.lastModified));
             }
 
             final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
-            if (Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
+            if (mDoc.isDirectory() || docSize == -1) {
                 mDetails.setVisibility(View.GONE);
             } else {
                 mDetails.setVisibility(View.VISIBLE);
                 mDetails.setText(Formatter.formatFileSize(mContext, docSize));
             }
         }
+
+        if (DebugFlags.getDocumentDetailsEnabled()) {
+            includeDebugInfo(mDoc);
+        }
     }
 }
diff --git a/src/com/android/documentsui/dirlist/IconHelper.java b/src/com/android/documentsui/dirlist/IconHelper.java
index 6f6327b..fa1c121 100644
--- a/src/com/android/documentsui/dirlist/IconHelper.java
+++ b/src/com/android/documentsui/dirlist/IconHelper.java
@@ -215,6 +215,24 @@
     /**
      * Load thumbnails for a directory list item.
      *
+     * @param doc The document
+     * @param iconThumb The itemview's thumbnail icon.
+     * @param iconMime The itemview's mime icon. Hidden when iconThumb is shown.
+     * @param subIconMime The second itemview's mime icon. Always visible.
+     * @return
+     */
+    public void load(
+            DocumentInfo doc,
+            ImageView iconThumb,
+            ImageView iconMime,
+            @Nullable ImageView subIconMime) {
+        load(doc.derivedUri, doc.mimeType, doc.flags, doc.icon, doc.lastModified,
+                iconThumb, iconMime, subIconMime);
+    }
+
+    /**
+     * Load thumbnails for a directory list item.
+     *
      * @param uri The URI for the file being represented.
      * @param mimeType The mime type of the file being represented.
      * @param docFlags Flags for the file being represented.
diff --git a/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
index 8921489..c65839f 100644
--- a/src/com/android/documentsui/dirlist/ListDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
@@ -16,16 +16,11 @@
 
 package com.android.documentsui.dirlist;
 
-import static com.android.documentsui.base.DocumentInfo.getCursorInt;
-import static com.android.documentsui.base.DocumentInfo.getCursorLong;
 import static com.android.documentsui.base.DocumentInfo.getCursorString;
 
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Rect;
-import android.net.Uri;
-import android.provider.DocumentsContract;
-import android.provider.DocumentsContract.Document;
 import android.text.format.Formatter;
 import android.view.View;
 import android.view.ViewGroup;
@@ -34,22 +29,25 @@
 import android.widget.TextView;
 
 import com.android.documentsui.R;
-import com.android.documentsui.base.Shared;
-import com.android.documentsui.base.State;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Events.InputEvent;
+import com.android.documentsui.base.Shared;
 import com.android.documentsui.roots.RootCursorWrapper;
 
 final class ListDocumentHolder extends DocumentHolder {
-    final TextView mTitle;
-    final LinearLayout mDetails;  // Container of date/size/summary
-    final TextView mDate;
-    final TextView mSize;
-    final TextView mSummary;
-    final ImageView mIconMime;
-    final ImageView mIconThumb;
-    final ImageView mIconCheck;
-    final IconHelper mIconHelper;
-    final View mIconLayout;
+
+    private final TextView mTitle;
+    private final LinearLayout mDetails;  // Container of date/size/summary
+    private final TextView mDate;
+    private final TextView mSize;
+    private final TextView mSummary;
+    private final ImageView mIconMime;
+    private final ImageView mIconThumb;
+    private final ImageView mIconCheck;
+    private final IconHelper mIconHelper;
+    private final View mIconLayout;
+    // This is used in as a convenience in our bind method.
+    private final DocumentInfo mDoc;
 
     public ListDocumentHolder(Context context, ViewGroup parent, IconHelper iconHelper) {
         super(context, parent, R.layout.item_doc_list);
@@ -66,6 +64,7 @@
         mDetails = (LinearLayout) itemView.findViewById(R.id.line2);
 
         mIconHelper = iconHelper;
+        mDoc = new DocumentInfo();
     }
 
     @Override
@@ -138,21 +137,12 @@
      * @param state Current display state.
      */
     @Override
-    public void bind(Cursor cursor, String modelId, State state) {
+    public void bind(Cursor cursor, String modelId) {
         assert(cursor != null);
 
-        this.modelId = modelId;
+        mModelId = modelId;
 
-        final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
-        final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
-        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-        final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
-        final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
-        final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
-        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
-        final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
-        final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
-        final boolean isDirectory = Document.MIME_TYPE_DIR.equals(docMimeType);
+        mDoc.updateFromCursor(cursor, getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY));
 
         mIconHelper.stopLoading(mIconThumb);
 
@@ -161,38 +151,36 @@
         mIconThumb.animate().cancel();
         mIconThumb.setAlpha(0f);
 
-        final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
-        mIconHelper.load(uri, docMimeType, docFlags, docIcon, docLastModified, mIconThumb,
-                mIconMime, null);
+        mIconHelper.load(mDoc, mIconThumb, mIconMime, null);
 
-        mTitle.setText(docDisplayName, TextView.BufferType.SPANNABLE);
+        mTitle.setText(mDoc.displayName, TextView.BufferType.SPANNABLE);
         mTitle.setVisibility(View.VISIBLE);
 
 
         boolean hasDetails = false;
-        if (isDirectory) {
+        if (mDoc.isDirectory()) {
             // Note, we don't show any details for any directory...ever.
             hasDetails = false;
         } else {
-            if (docSummary != null) {
+            if (mDoc.summary != null) {
                 hasDetails = true;
-                mSummary.setText(docSummary);
+                mSummary.setText(mDoc.summary);
                 mSummary.setVisibility(View.VISIBLE);
             } else {
                 mSummary.setVisibility(View.INVISIBLE);
             }
 
-            if (docLastModified > 0) {
+            if (mDoc.lastModified > 0) {
                 hasDetails = true;
-                mDate.setText(Shared.formatTime(mContext, docLastModified));
+                mDate.setText(Shared.formatTime(mContext, mDoc.lastModified));
             } else {
                 mDate.setText(null);
             }
 
-            if (docSize > -1) {
+            if (mDoc.size > -1) {
                 hasDetails = true;
                 mSize.setVisibility(View.VISIBLE);
-                mSize.setText(Formatter.formatFileSize(mContext, docSize));
+                mSize.setText(Formatter.formatFileSize(mContext, mDoc.size));
             } else {
                 mSize.setVisibility(View.GONE);
             }
@@ -202,5 +190,8 @@
         if (mDetails != null) {
             mDetails.setVisibility(hasDetails ? View.VISIBLE : View.GONE);
         }
+
+        // TODO: Add document debug info
+        // Call includeDebugInfo
     }
 }
diff --git a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
index 296fa70..17ac867 100644
--- a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
+++ b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
@@ -16,15 +16,21 @@
 
 package com.android.documentsui.dirlist;
 
+import static com.android.documentsui.base.Shared.DEBUG;
+
 import android.annotation.Nullable;
 import android.content.Context;
+import android.os.Build;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.OnItemTouchListener;
+import android.util.Log;
 import android.view.GestureDetector;
 import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
 import android.view.View;
 import android.view.View.OnTouchListener;
 
+import com.android.documentsui.base.DebugFlags;
 import com.android.documentsui.base.EventHandler;
 import com.android.documentsui.base.Events;
 import com.android.documentsui.base.Events.InputEvent;
@@ -32,18 +38,25 @@
 import com.android.documentsui.selection.BandController;
 import com.android.documentsui.selection.GestureSelector;
 
+import java.util.function.Consumer;
+
 //Receives event meant for both directory and empty view, and either pass them to
 //{@link UserInputHandler} for simple gestures (Single Tap, Long-Press), or intercept them for
 //other types of gestures (drag n' drop)
 final class ListeningGestureDetector extends GestureDetector
         implements OnItemTouchListener, OnTouchListener {
 
+    private static final String TAG = "ListeningGestureDetector";
+
     private final GestureSelector mGestureSelector;
     private final EventHandler<InputEvent> mMouseDragListener;
     private final BandController mBandController;
     private final MouseDelegate mMouseDelegate = new MouseDelegate();
     private final TouchDelegate mTouchDelegate = new TouchDelegate();
 
+    // Currently only initialized on IS_DEBUGGABLE builds.
+    private final @Nullable ScaleGestureDetector mScaleDetector;
+
     public ListeningGestureDetector(
             Context context,
             RecyclerView recView,
@@ -51,19 +64,45 @@
             EventHandler<InputEvent> mouseDragListener,
             GestureSelector gestureSelector,
             UserInputHandler<? extends InputEvent> handler,
-            @Nullable BandController bandController) {
+            @Nullable BandController bandController,
+            Consumer<Float> scaleHandler) {
+
         super(context, handler);
+
         mMouseDragListener = mouseDragListener;
         mGestureSelector = gestureSelector;
         mBandController = bandController;
         recView.addOnItemTouchListener(this);
         emptyView.setOnTouchListener(this);
+
+        mScaleDetector = !Build.IS_DEBUGGABLE
+                ? null
+                : new ScaleGestureDetector(
+                        context,
+                        new ScaleGestureDetector.SimpleOnScaleGestureListener() {
+                            @Override
+                            public boolean onScale(ScaleGestureDetector detector) {
+                                if (DEBUG) Log.v(TAG,
+                                        "Received scale event: " + detector.getScaleFactor());
+                                scaleHandler.accept(detector.getScaleFactor());
+                                return true;
+                            }
+                        });
     }
 
     @Override
     public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
         boolean handled = false;
 
+        // This is an in-development feature.
+        // TODO: Re-wire event handling so that we're not dispatching
+        //     events to to scaledetector's #onTouchEvent from this
+        //     #onInterceptTouchEvent touch event.
+        if (DebugFlags.getGestureScaleEnabled()
+                && mScaleDetector != null) {
+            mScaleDetector.onTouchEvent(e);
+        }
+
         try (InputEvent event = MotionInputEvent.obtain(e, rv)) {
             if (event.isMouseEvent()) {
                 handled |= mMouseDelegate.onInterceptTouchEvent(event);
diff --git a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
index adc4b04..99f9371 100644
--- a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
@@ -129,7 +129,7 @@
     public void onBindViewHolder(DocumentHolder holder, int position) {
         String modelId = mModelIds.get(position);
         Cursor cursor = mEnv.getModel().getItem(modelId);
-        holder.bind(cursor, modelId, mEnv.getDisplayState());
+        holder.bind(cursor, modelId);
 
         final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
         final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
diff --git a/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java b/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java
index 69f9566..4cb55b3 100644
--- a/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java
+++ b/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java
@@ -240,6 +240,7 @@
      */
     private static final class EmptyDocumentHolder extends DocumentHolder {
         final int mVisibleHeight;
+        private State mState;
 
         public EmptyDocumentHolder(Context context) {
             super(context, new Space(context));
@@ -249,12 +250,13 @@
         }
 
         public void bind(State state) {
-            bind(null, null, state);
+            mState = state;
+            bind(null, null);
         }
 
         @Override
-        public void bind(Cursor cursor, String modelId, State state) {
-            if (state.derivedMode == State.MODE_GRID) {
+        public void bind(Cursor cursor, String modelId) {
+            if (mState.derivedMode == State.MODE_GRID) {
                 itemView.setMinimumHeight(mVisibleHeight);
             } else {
                 itemView.setMinimumHeight(0);
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 7a2284f..0c67b20 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -217,7 +217,7 @@
         Intent intent = getIntent();
         return (intent != null && intent.hasExtra(Intent.EXTRA_TITLE))
                 ? intent.getStringExtra(Intent.EXTRA_TITLE)
-                : getTitle().toString();
+                : getString(R.string.app_label);
     }
 
     @Override
diff --git a/src/com/android/documentsui/files/LauncherActivity.java b/src/com/android/documentsui/files/LauncherActivity.java
index bb25c35..a73d439 100644
--- a/src/com/android/documentsui/files/LauncherActivity.java
+++ b/src/com/android/documentsui/files/LauncherActivity.java
@@ -29,8 +29,6 @@
 import android.support.annotation.Nullable;
 import android.util.Log;
 
-import com.android.documentsui.base.Shared;
-
 import java.util.List;
 
 /**
@@ -50,25 +48,34 @@
     // Array of boolean extras that should be copied when creating new launch intents.
     // Missing intents will be ignored.
     private static final String[] PERSISTENT_BOOLEAN_EXTRAS = {
-        DocumentsContract.EXTRA_SHOW_ADVANCED,
-        DocumentsContract.EXTRA_FANCY_FEATURES,
-        Shared.EXTRA_PRODUCTIVITY_MODE
+        DocumentsContract.EXTRA_SHOW_ADVANCED
     };
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
+        launch();
+
+        finish();
+    }
+
+    private void launch() {
         ActivityManager activities = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
 
         Intent intent = findTask(activities);
         if (intent != null) {
-            restoreTask(intent);
-        } else {
-            startTask();
+            if (restoreTask(intent)) {
+                return;
+            } else {
+                // We failed to restore the task. It may happen when system was just updated and we
+                // moved the location of the targeted activity. Chances is that the rest of tasks
+                // can't be restored either, so clean those tasks and start a new one.
+                clearTask(activities);
+            }
         }
 
-        finish();
+        startTask();
     }
 
     private @Nullable Intent findTask(ActivityManager activities) {
@@ -92,10 +99,26 @@
         startActivity(intent);
     }
 
-    private void restoreTask(Intent intent) {
+    private boolean restoreTask(Intent intent) {
         if (DEBUG) Log.d(TAG, "Restoring existing task > " + intent.getData());
-        // TODO: This doesn't appear to restore a task once it has stopped running.
-        startActivity(intent);
+        try {
+            // TODO: This doesn't appear to restore a task once it has stopped running.
+            startActivity(intent);
+
+            return true;
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to restore task > " + intent.getData() +
+                    ". Clear all existing tasks and start a new one.", e);
+        }
+
+        return false;
+    }
+
+    private void clearTask(ActivityManager activities) {
+        List<AppTask> tasks = activities.getAppTasks();
+        for (AppTask task : tasks) {
+            task.finishAndRemoveTask();
+        }
     }
 
     public static final Intent createLaunchIntent(Activity activity) {
diff --git a/src/com/android/documentsui/files/MenuManager.java b/src/com/android/documentsui/files/MenuManager.java
index 839c245..522f448 100644
--- a/src/com/android/documentsui/files/MenuManager.java
+++ b/src/com/android/documentsui/files/MenuManager.java
@@ -135,7 +135,7 @@
 
     @Override
     protected void updateNewWindow(MenuItem newWindow) {
-        newWindow.setVisible(mDirDetails.shouldShowFancyFeatures());
+        newWindow.setVisible(true);
     }
 
     @Override
diff --git a/src/com/android/documentsui/files/QuickViewIntentBuilder.java b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
index 3304cc1..c85af8a 100644
--- a/src/com/android/documentsui/files/QuickViewIntentBuilder.java
+++ b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
@@ -36,9 +36,9 @@
 import android.util.Range;
 
 import com.android.documentsui.R;
+import com.android.documentsui.base.DebugFlags;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.dirlist.Model;
-import com.android.documentsui.queries.SetQuickViewerCommand;
 import com.android.documentsui.roots.RootCursorWrapper;
 
 import java.util.ArrayList;
@@ -142,8 +142,9 @@
         // Allow users of debug devices to override default quick viewer
         // for the purposes of testing.
         if (Build.IS_DEBUGGABLE) {
-            if (SetQuickViewerCommand.sQuickViewer != null) {
-                return SetQuickViewerCommand.sQuickViewer;
+            String quickViewer = DebugFlags.getQuickViewer();
+            if (quickViewer != null) {
+                return quickViewer;
             }
             return android.os.SystemProperties.get("debug.quick_viewer", resValue);
         }
diff --git a/src/com/android/documentsui/queries/DebugCommandProcessor.java b/src/com/android/documentsui/queries/DebugCommandProcessor.java
index 70d9d64..7a12a18 100644
--- a/src/com/android/documentsui/queries/DebugCommandProcessor.java
+++ b/src/com/android/documentsui/queries/DebugCommandProcessor.java
@@ -17,8 +17,10 @@
 
 import android.os.Build;
 import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.documentsui.base.DebugFlags;
 import com.android.documentsui.base.EventHandler;
 
 import java.util.ArrayList;
@@ -26,11 +28,18 @@
 
 final class DebugCommandProcessor implements EventHandler<String> {
 
+    @VisibleForTesting
+    static final String COMMAND_PREFIX = "dbg:";
+
+    private static final String TAG = "DebugCommandProcessor";
+
     private final List<EventHandler<String[]>> mCommands = new ArrayList<>();
 
     public DebugCommandProcessor() {
         if (Build.IS_DEBUGGABLE) {
-            mCommands.add(new SetQuickViewerCommand());
+            mCommands.add(DebugCommandProcessor::quickViewer);
+            mCommands.add(DebugCommandProcessor::gestureScale);
+            mCommands.add(DebugCommandProcessor::docDetails);
         }
     }
 
@@ -43,8 +52,8 @@
 
     @Override
     public boolean accept(String query) {
-        if (query.length() > 6 && query.substring(0, 6).equals("#debug")) {
-            String[] tokens = query.substring(7).split("\\s+");
+        if (query.length() > COMMAND_PREFIX.length() && query.startsWith(COMMAND_PREFIX)) {
+            String[] tokens = query.substring(COMMAND_PREFIX.length()).split("\\s+");
             for (EventHandler<String[]> command : mCommands) {
                 if (command.accept(tokens)) {
                     return true;
@@ -54,4 +63,57 @@
         }
         return false;
     }
+
+    private static boolean quickViewer(String[] tokens) {
+        if ("qv".equals(tokens[0])) {
+            if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
+                DebugFlags.setQuickViewer(tokens[1]);
+                Log.i(TAG, "Set quick viewer to: " + tokens[1]);
+                return true;
+            } else {
+                Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
+            }
+        } else if ("deqv".equals(tokens[0])) {
+            Log.i(TAG, "Unset quick viewer");
+            DebugFlags.setQuickViewer(null);
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean gestureScale(String[] tokens) {
+        if ("gs".equals(tokens[0])) {
+            if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
+                boolean enabled = asBool(tokens[1]);
+                DebugFlags.setGestureScaleEnabled(enabled);
+                Log.i(TAG, "Set gesture scale enabled to: " + enabled);
+                return true;
+            }
+            Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
+        }
+        return false;
+    }
+
+    private static boolean docDetails(String[] tokens) {
+        if ("docinfo".equals(tokens[0])) {
+            if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
+                boolean enabled = asBool(tokens[1]);
+                DebugFlags.setDocumentDetailsEnabled(enabled);
+                Log.i(TAG, "Set gesture scale enabled to: " + enabled);
+                return true;
+            }
+            Log.w(TAG, "Invalid command structure: " + TextUtils.join(" ", tokens));
+        }
+        return false;
+    }
+
+    private static final boolean asBool(String val) {
+        if (val == null || val.equals("0")) {
+            return false;
+        }
+        if (val.equals("1")) {
+            return true;
+        }
+        return Boolean.valueOf(val);
+    }
 }
diff --git a/src/com/android/documentsui/queries/SetQuickViewerCommand.java b/src/com/android/documentsui/queries/SetQuickViewerCommand.java
deleted file mode 100644
index 37fe0db..0000000
--- a/src/com/android/documentsui/queries/SetQuickViewerCommand.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.documentsui.queries;
-
-import android.text.TextUtils;
-import android.util.Log;
-
-import com.android.documentsui.base.EventHandler;
-
-public class SetQuickViewerCommand implements EventHandler<String[]> {
-
-    // This is a quick/easy shortcut to sharing quick viewer debug settings
-    // with QuickViewIntent builder. Tried setting at a system property
-    // but got a native error. This being quick and easy, didn't investigate that err.
-    public static String sQuickViewer;
-    private static final String TAG = "SetQuickViewerCommand";
-
-    @Override
-    public boolean accept(String[] tokens) {
-        if ("setqv".equals(tokens[0])) {
-            if (tokens.length == 2 && !TextUtils.isEmpty(tokens[1])) {
-                sQuickViewer = tokens[1];
-                Log.i(TAG, "Set quick viewer to: " + sQuickViewer);
-                return true;
-            } else {
-                Log.w(TAG, "Invalid command structure: " + tokens);
-            }
-        } else if ("unsetqv".equals(tokens[0])) {
-            Log.i(TAG, "Unset quick viewer");
-            sQuickViewer = null;
-            return true;
-        }
-        return false;
-    }
-}
diff --git a/src/com/android/documentsui/roots/RootsAccess.java b/src/com/android/documentsui/roots/RootsAccess.java
index 0d0a975..8259337 100644
--- a/src/com/android/documentsui/roots/RootsAccess.java
+++ b/src/com/android/documentsui/roots/RootsAccess.java
@@ -25,6 +25,7 @@
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.State;
 
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -112,7 +113,7 @@
             if (!overlap) {
                 if (DEBUG) Log.v(
                         tag, "Excluding root because: unsupported content types > "
-                        + state.acceptMimes);
+                        + Arrays.toString(state.acceptMimes));
                 continue;
             }
 
diff --git a/src/com/android/documentsui/selection/GestureSelector.java b/src/com/android/documentsui/selection/GestureSelector.java
index 959080c..ff77bd0 100644
--- a/src/com/android/documentsui/selection/GestureSelector.java
+++ b/src/com/android/documentsui/selection/GestureSelector.java
@@ -128,10 +128,6 @@
             handled = handleInterceptedDownEvent(e);
         }
 
-        if (e.isActionUp()) {
-            handled = handleUpEvent(e);
-        }
-
         if (e.isActionMove()) {
             handled = handleInterceptedMoveEvent(e);
         }
@@ -148,6 +144,10 @@
             handleUpEvent(e);
         }
 
+        if (e.isActionCancel()) {
+            handleCancelEvent(e);
+        }
+
         if (e.isActionMove()) {
             handleOnTouchMoveEvent(rv, e);
         }
@@ -157,9 +157,9 @@
     // If down event happens on a file/doc, we mark that item's position as last started.
     private boolean handleInterceptedDownEvent(InputEvent e) {
         View itemView = mViewFinder.findView(e.getX(), e.getY());
-            if (itemView != null) {
-                mLastStartedItemPos = e.getItemPosition();
-            }
+        if (itemView != null) {
+            mLastStartedItemPos = e.getItemPosition();
+        }
         return false;
     }
 
@@ -175,14 +175,27 @@
         return false;
     }
 
-    // Called when ACTION_UP event is intercepted.
-    // Essentially, since this means all gesture movement is over, reset everything.
-    private boolean handleUpEvent(InputEvent e) {
+    // Called when ACTION_UP event is to be handled.
+    // Essentially, since this means all gesture movement is over, reset everything and apply
+    // provisional selection.
+    private void handleUpEvent(InputEvent e) {
+        mSelectionMgr.getSelection().applyProvisionalSelection();
+        endSelection();
+    }
+
+    // Called when ACTION_CANCEL event is to be handled.
+    // This means this gesture selection is aborted, so reset everything and abandon provisional
+    // selection.
+    private void handleCancelEvent(InputEvent e) {
+        mSelectionMgr.cancelProvisionalSelection();
+        endSelection();
+    }
+
+    private void endSelection() {
+        assert(mStarted);
         mLastStartedItemPos = -1;
         mStarted = false;
-        mSelectionMgr.getSelection().applyProvisionalSelection();
         mLock.unblock();
-        return false;
     }
 
     // Call when an intercepted ACTION_MOVE event is passed down.
diff --git a/src/com/android/documentsui/selection/SelectionManager.java b/src/com/android/documentsui/selection/SelectionManager.java
index 98d1cda..103ef8f 100644
--- a/src/com/android/documentsui/selection/SelectionManager.java
+++ b/src/com/android/documentsui/selection/SelectionManager.java
@@ -322,6 +322,13 @@
         notifySelectionChanged();
     }
 
+    void cancelProvisionalSelection() {
+        for (String id : mSelection.mProvisionalSelection) {
+            notifyItemStateChanged(id, false);
+        }
+        mSelection.cancelProvisionalSelection();
+    }
+
     /**
      * Stops an in-progress range selection. All selection done with
      * {@link #snapRangeSelection(int, int)} with type RANGE_PROVISIONAL will be lost if
@@ -330,7 +337,7 @@
     public void endRangeSelection() {
         mRanger = null;
         // Clean up in case there was any leftover provisional selection
-        mSelection.cancelProvisionalSelection();
+        cancelProvisionalSelection();
     }
 
     /**
diff --git a/src/com/android/documentsui/services/CopyJob.java b/src/com/android/documentsui/services/CopyJob.java
index b9bc651..30bbbf1 100644
--- a/src/com/android/documentsui/services/CopyJob.java
+++ b/src/com/android/documentsui/services/CopyJob.java
@@ -401,7 +401,7 @@
         if (DEBUG) Log.d(TAG, "Doing byte copy of document: " + src);
         // If the file is virtual, but can be converted to another format, then try to copy it
         // as such format. Also, append an extension for the target mime type (if known).
-        if (src.isVirtualDocument()) {
+        if (src.isVirtual()) {
             String[] streamTypes = null;
             try {
                 streamTypes = getContentResolver().getStreamTypes(src.derivedUri, "*/*");
@@ -538,7 +538,7 @@
         try {
             // If the file is virtual, but can be converted to another format, then try to copy it
             // as such format.
-            if (src.isVirtualDocument()) {
+            if (src.isVirtual()) {
                 try {
                     srcFileAsAsset = getClient(src).openTypedAssetFileDescriptor(
                                 src.derivedUri, mimeType, null, canceller);
@@ -592,7 +592,7 @@
                         src.derivedUri, dest.derivedUri, e);
             }
 
-            if (src.isVirtualDocument()) {
+            if (src.isVirtual()) {
                convertedFiles.add(src);
             }
 
diff --git a/src/com/android/documentsui/services/FileOperationService.java b/src/com/android/documentsui/services/FileOperationService.java
index 3fa23c9..d28a578 100644
--- a/src/com/android/documentsui/services/FileOperationService.java
+++ b/src/com/android/documentsui/services/FileOperationService.java
@@ -88,13 +88,13 @@
     // Use a handler to schedule monitor tasks.
     @VisibleForTesting Handler handler;
 
+    @GuardedBy("mRunning")
+    private final Map<String, JobRecord> mRunning = new HashMap<>();
+
     private PowerManager mPowerManager;
     private PowerManager.WakeLock mWakeLock;  // the wake lock, if held.
     private NotificationManager mNotificationManager;
 
-    @GuardedBy("mRunning")
-    private Map<String, JobRecord> mRunning = new HashMap<>();
-
     private int mLastServiceId;
 
     @Override
diff --git a/src/com/android/documentsui/services/MoveJob.java b/src/com/android/documentsui/services/MoveJob.java
index 16bda15..7e66cb0 100644
--- a/src/com/android/documentsui/services/MoveJob.java
+++ b/src/com/android/documentsui/services/MoveJob.java
@@ -154,7 +154,7 @@
         // Moving virtual files by bytes is not supported. This is because, it would involve
         // conversion, and the source file should not be deleted in such case (as it's a different
         // file).
-        if (src.isVirtualDocument()) {
+        if (src.isVirtual()) {
             throw new ResourceException("Cannot move virtual file %s byte by byte.",
                     src.derivedUri);
         }
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 4e9287e..88c98a4 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -259,7 +259,7 @@
             final RootItem item = new RootItem(root, mActionHandler);
 
             Activity activity = getActivity();
-            if (root.isHome() && !Shared.shouldShowDocumentsRoot(activity, activity.getIntent())) {
+            if (root.isHome() && !Shared.shouldShowDocumentsRoot(activity)) {
                 continue;
             } else if (root.isLibrary()) {
                 libraries.add(item);
diff --git a/src/com/android/documentsui/sorting/SortDimension.java b/src/com/android/documentsui/sorting/SortDimension.java
index 9d235a1..6411b98 100644
--- a/src/com/android/documentsui/sorting/SortDimension.java
+++ b/src/com/android/documentsui/sorting/SortDimension.java
@@ -124,6 +124,11 @@
     }
 
     @Override
+    public int hashCode() {
+        return mId;
+    }
+
+    @Override
     public boolean equals(Object o) {
         if (o == null || !(o instanceof SortDimension)) {
             return false;
diff --git a/src/com/android/documentsui/sorting/SortingCursorWrapper.java b/src/com/android/documentsui/sorting/SortingCursorWrapper.java
index 89869d9..691c1d7 100644
--- a/src/com/android/documentsui/sorting/SortingCursorWrapper.java
+++ b/src/com/android/documentsui/sorting/SortingCursorWrapper.java
@@ -21,6 +21,7 @@
 
 import android.database.AbstractCursor;
 import android.database.Cursor;
+import android.os.Bundle;
 import android.provider.DocumentsContract.Document;
 
 import com.android.documentsui.base.Shared;
@@ -156,6 +157,11 @@
         return mCursor.isNull(column);
     }
 
+    @Override
+    public Bundle getExtras() {
+        return mCursor.getExtras();
+    }
+
     /**
      * @return Timestamp for the given document. Some docs (e.g. active downloads) have a null
      * timestamp - these will be replaced with MAX_LONG so that such files get sorted to the top
diff --git a/src/com/android/documentsui/ui/DocumentDebugInfo.java b/src/com/android/documentsui/ui/DocumentDebugInfo.java
new file mode 100644
index 0000000..18d66f2
--- /dev/null
+++ b/src/com/android/documentsui/ui/DocumentDebugInfo.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.ui;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+import com.android.documentsui.base.DocumentInfo;
+
+/**
+ * Document debug info view.
+ */
+public class DocumentDebugInfo extends TextView {
+    public DocumentDebugInfo(Context context) {
+        super(context);
+
+    }
+
+    public DocumentDebugInfo(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public void update(DocumentInfo doc) {
+
+        String dbgInfo = new StringBuilder()
+                .append("** PROPERTIES **\n\n")
+                .append("docid: " + doc.documentId).append("\n")
+                .append("name: " + doc.displayName).append("\n")
+                .append("mimetype: " + doc.mimeType).append("\n")
+                .append("container: " + doc.isContainer()).append("\n")
+                .append("virtual: " + doc.isVirtual()).append("\n")
+                .append("\n")
+                .append("** OPERATIONS **\n\n")
+                .append("create: " + doc.isCreateSupported()).append("\n")
+                .append("delete: " + doc.isDeleteSupported()).append("\n")
+                .append("rename: " + doc.isRenameSupported()).append("\n")
+                .toString();
+
+        setText(dbgInfo);
+    }
+}
diff --git a/tests/common/com/android/documentsui/StubProvider.java b/tests/common/com/android/documentsui/StubProvider.java
index f71ce5d..dbd948c 100644
--- a/tests/common/com/android/documentsui/StubProvider.java
+++ b/tests/common/com/android/documentsui/StubProvider.java
@@ -766,7 +766,7 @@
             this.file = file;
             this.documentId = getDocumentIdForFile(file);
             this.mimeType = Document.MIME_TYPE_DIR;
-            this.streamTypes = new ArrayList<String>();
+            this.streamTypes = new ArrayList<>();
             this.flags = Document.FLAG_DIR_SUPPORTS_CREATE | Document.FLAG_SUPPORTS_RENAME;
             this.parentId = null;
             this.rootInfo = rootInfo;
diff --git a/tests/common/com/android/documentsui/testing/Parcelables.java b/tests/common/com/android/documentsui/testing/Parcelables.java
index cd44a9a..5267065 100644
--- a/tests/common/com/android/documentsui/testing/Parcelables.java
+++ b/tests/common/com/android/documentsui/testing/Parcelables.java
@@ -17,26 +17,28 @@
 package com.android.documentsui.testing;
 
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
 
 import android.os.Parcel;
 import android.os.Parcelable;
 
-import java.util.Comparator;
+import java.util.function.BiPredicate;
 
 public class Parcelables {
 
     private Parcelables() {}
 
-    public static <T extends Parcelable> void testParceling(T p, int flags) {
+    public static <T extends Parcelable> void assertParcelable(T p, int flags) {
         final T restored = parcel(p, flags);
 
         assertEquals(p, restored);
     }
 
-    public static <T extends Parcelable> void testParceling(T p, int flags, Comparator<T> comp) {
+    public static <T extends Parcelable> void assertParcelable(
+            T p, int flags, BiPredicate<T, T> pred) {
         T restored = parcel(p, flags);
 
-        assertEquals(0, comp.compare(p, restored));
+        assertTrue(pred.test(p, restored));
     }
 
     private static <T extends Parcelable> T parcel(T p, int flags) {
diff --git a/tests/common/com/android/documentsui/testing/TestDirectoryDetails.java b/tests/common/com/android/documentsui/testing/TestDirectoryDetails.java
index 82bd047..a1b29cd 100644
--- a/tests/common/com/android/documentsui/testing/TestDirectoryDetails.java
+++ b/tests/common/com/android/documentsui/testing/TestDirectoryDetails.java
@@ -23,7 +23,6 @@
  */
 public class TestDirectoryDetails extends DirectoryDetails {
 
-    public boolean shouldShowFancyFeatures;
     public boolean isInRecents;
     public boolean hasRootSettings;
     public boolean hasItemsToPaste;
@@ -35,11 +34,6 @@
     }
 
     @Override
-    public boolean shouldShowFancyFeatures() {
-        return shouldShowFancyFeatures;
-    }
-
-    @Override
     public boolean hasRootSettings() {
         return hasRootSettings;
     }
diff --git a/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java b/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java
index ff86a7f..aaace3e 100644
--- a/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java
+++ b/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java
@@ -72,6 +72,6 @@
     }
 
     private boolean docsRootEnabled() {
-        return Shared.shouldShowDocumentsRoot(context, new Intent(DocumentsContract.ACTION_BROWSE));
+        return Shared.shouldShowDocumentsRoot(context);
     }
 }
diff --git a/tests/unit/com/android/documentsui/base/DocumentStackTest.java b/tests/unit/com/android/documentsui/base/DocumentStackTest.java
index b7e3cc7..b007583 100644
--- a/tests/unit/com/android/documentsui/base/DocumentStackTest.java
+++ b/tests/unit/com/android/documentsui/base/DocumentStackTest.java
@@ -18,28 +18,50 @@
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
+import static junit.framework.TestCase.assertNull;
 import static junit.framework.TestCase.assertTrue;
 
+import android.provider.DocumentsContract;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
+import com.android.documentsui.testing.Parcelables;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Objects;
+
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class DocumentStackTest {
+    private static final RootInfo ROOT_1;
+    private static final RootInfo ROOT_2;
+
     private static final DocumentInfo DIR_1;
     private static final DocumentInfo DIR_2;
 
     private DocumentStack mStack;
 
     static {
-        DIR_1 = new DocumentInfo();
-        DIR_1.displayName = "firstDirectory";
-        DIR_2 = new DocumentInfo();
-        DIR_2.displayName = "secondDirectory";
+        ROOT_1 = new RootInfo();
+        ROOT_1.rootId = "home";
+        ROOT_2 = new RootInfo();
+        ROOT_2.rootId = "downloads";
+
+        DIR_1 = createDir("first");
+        DIR_2 = createDir("second");
+    }
+
+    private static DocumentInfo createDir(String docId) {
+        DocumentInfo info = new DocumentInfo();
+        info.authority = "authority";
+        info.documentId = docId;
+        info.displayName = docId;
+        info.mimeType = DocumentsContract.Document.MIME_TYPE_DIR;
+        info.deriveFields();
+        return info;
     }
 
     @Before
@@ -50,13 +72,10 @@
     @Test
     public void testInitialStateEmpty() {
         assertFalse(mStack.hasLocationChanged());
-    }
-
-    @Test
-    public void testPushDocument_ChangesLocation() {
-        mStack.push(DIR_1);
-        mStack.push(DIR_2);
-        assertTrue(mStack.hasLocationChanged());
+        assertFalse(mStack.hasInitialLocationChanged());
+        assertTrue(mStack.isEmpty());
+        assertEquals(0, mStack.size());
+        assertNull(mStack.getRoot());
     }
 
     @Test
@@ -73,4 +92,116 @@
         mStack.pop();
         assertEquals(DIR_1, mStack.peek());
     }
+
+    @Test
+    public void testGetDocument() {
+        mStack.push(DIR_1);
+        mStack.push(DIR_2);
+
+        assertEquals(DIR_1, mStack.get(0));
+        assertEquals(DIR_2, mStack.get(1));
+    }
+
+    @Test
+    public void testChangeRoot() {
+        mStack.changeRoot(ROOT_1);
+
+        assertEquals(ROOT_1, mStack.getRoot());
+    }
+
+    @Test
+    public void testChangeRoot_ClearsStack() {
+        mStack.push(DIR_1);
+
+        mStack.changeRoot(ROOT_1);
+
+        assertTrue(mStack.isEmpty());
+        assertEquals(0, mStack.size());
+    }
+
+    @Test
+    public void testReset() {
+        mStack.changeRoot(ROOT_1);
+        mStack.push(DIR_1);
+
+        mStack.reset();
+
+        assertNull(mStack.getRoot());
+        assertTrue(mStack.isEmpty());
+        assertEquals(0, mStack.size());
+    }
+
+    @Test
+    public void testCopyConstructor() {
+        mStack.changeRoot(ROOT_1);
+        mStack.push(DIR_1);
+        mStack.push(DIR_2);
+
+        DocumentStack stack = new DocumentStack(mStack);
+
+        assertEquals(2, stack.size());
+        assertEquals(DIR_1, stack.get(0));
+        assertEquals(DIR_2, stack.get(1));
+        assertEquals(ROOT_1, stack.getRoot());
+    }
+
+    @Test
+    public void testCopyConstructor_MakesDeepCopy() {
+        mStack.changeRoot(ROOT_1);
+        mStack.push(DIR_1);
+        mStack.push(DIR_2);
+
+        DocumentStack stack = new DocumentStack(mStack);
+
+        mStack.changeRoot(ROOT_2);
+
+        assertEquals(2, stack.size());
+        assertEquals(DIR_1, stack.get(0));
+        assertEquals(DIR_2, stack.get(1));
+        assertEquals(ROOT_1, stack.getRoot());
+    }
+
+    @Test
+    public void testPushDocument_ChangesLocation() {
+        mStack.push(DIR_1);
+
+        assertTrue(mStack.hasLocationChanged());
+    }
+
+    @Test
+    public void testPushDocument_ChangesInitialLocation() {
+        mStack.push(DIR_1);
+        mStack.push(DIR_2);
+
+        assertTrue(mStack.hasInitialLocationChanged());
+    }
+
+    @Test
+    public void testChangeRoot_ChangesInitialLocation() {
+        mStack.changeRoot(ROOT_1);
+        mStack.changeRoot(ROOT_2);
+
+        assertTrue(mStack.hasInitialLocationChanged());
+    }
+
+    @Test
+    public void testParceling() {
+        mStack.changeRoot(ROOT_1);
+        mStack.push(DIR_1);
+        mStack.push(DIR_2);
+
+        Parcelables.assertParcelable(mStack, 0, (DocumentStack left, DocumentStack right) -> {
+            if (!Objects.equals(left.getRoot(), right.getRoot()) || left.size() != right.size()) {
+                return false;
+            }
+
+            for (int i = 0; i < left.size(); ++i) {
+                if (!Objects.equals(left.get(i), right.get(i))) {
+                    return false;
+                }
+            }
+
+            return true;
+        });
+    }
 }
diff --git a/tests/unit/com/android/documentsui/dirlist/DocumentHolderTest.java b/tests/unit/com/android/documentsui/dirlist/DocumentHolderTest.java
index 9f9663f..9d9256d 100644
--- a/tests/unit/com/android/documentsui/dirlist/DocumentHolderTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/DocumentHolderTest.java
@@ -30,7 +30,6 @@
 import android.view.MotionEvent.PointerProperties;
 
 import com.android.documentsui.R;
-import com.android.documentsui.base.State;
 
 @SmallTest
 public class DocumentHolderTest extends AndroidTestCase {
@@ -44,7 +43,7 @@
         LayoutInflater inflater = LayoutInflater.from(context);
         mHolder = new DocumentHolder(getContext(), inflater.inflate(R.layout.item_doc_list, null)) {
             @Override
-            public void bind(Cursor cursor, String modelId, State state) {}
+            public void bind(Cursor cursor, String modelId) {}
         };
 
         mListener = new TestListener();
diff --git a/tests/unit/com/android/documentsui/files/MenuManagerTest.java b/tests/unit/com/android/documentsui/files/MenuManagerTest.java
index 5d1fb7d..473f3a8 100644
--- a/tests/unit/com/android/documentsui/files/MenuManagerTest.java
+++ b/tests/unit/com/android/documentsui/files/MenuManagerTest.java
@@ -173,7 +173,7 @@
     @Test
     public void testOptionMenu_showAdvanced() {
         state.showAdvanced = true;
-        state.showAdvancedOption = true;
+        state.showDeviceStorageOption = true;
         mgr.updateOptionMenu(testMenu);
 
         advanced.assertVisible();
@@ -197,14 +197,6 @@
     }
 
     @Test
-    public void testOptionMenu_shouldShowFancyFeatures() {
-        dirDetails.shouldShowFancyFeatures = true;
-        mgr.updateOptionMenu(testMenu);
-
-        newWindow.assertVisible();
-    }
-
-    @Test
     public void testInflateContextMenu_Files() {
         TestMenuInflater inflater = new TestMenuInflater();
 
diff --git a/tests/unit/com/android/documentsui/picker/MenuManagerTest.java b/tests/unit/com/android/documentsui/picker/MenuManagerTest.java
index f282bd7..0e6a53a 100644
--- a/tests/unit/com/android/documentsui/picker/MenuManagerTest.java
+++ b/tests/unit/com/android/documentsui/picker/MenuManagerTest.java
@@ -164,7 +164,7 @@
     @Test
     public void testOptionMenu_showAdvanced() {
         state.showAdvanced = true;
-        state.showAdvancedOption = true;
+        state.showDeviceStorageOption = true;
         mgr.updateOptionMenu(testMenu);
 
         advanced.assertVisible();
diff --git a/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java b/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java
index 0f6e996..2398bb2 100644
--- a/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java
+++ b/tests/unit/com/android/documentsui/queries/DebugCommandProcessorTest.java
@@ -16,11 +16,14 @@
 
 package com.android.documentsui.queries;
 
+import static com.android.documentsui.queries.DebugCommandProcessor.COMMAND_PREFIX;
+
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
 import com.android.documentsui.testing.TestEventHandler;
 
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -42,7 +45,7 @@
 
     @Test
     public void testTriesAllCommands() {
-        mProcessor.accept("#debug poodles");
+        mProcessor.accept(COMMAND_PREFIX + "poodles");
         mCommand0.assertCalled();
         mCommand1.assertCalled();
     }
@@ -50,21 +53,23 @@
     @Test
     public void testStopsAfterCommandHandled() {
         mCommand0.nextReturn(true);
-        mProcessor.accept("#debug poodles");
+        mProcessor.accept("dbg:poodles");
         mCommand0.assertCalled();
         mCommand1.assertNotCalled();
     }
 
     @Test
-    public void testMissingCommand() {
-        mProcessor.accept("#debug");
-        mCommand0.assertNotCalled();
-        mCommand1.assertNotCalled();
+    public void testConveysArguments() {
+        mCommand0.nextReturn(true);
+        mProcessor.accept(COMMAND_PREFIX + "cheese doodles");
+
+        String[] expected = {"cheese", "doodles"};
+        Assert.assertArrayEquals(expected, mCommand0.getLastValue());
     }
 
     @Test
-    public void testEmptyInput() {
-        mProcessor.accept("#debug");
+    public void testMissingCommand() {
+        mProcessor.accept(COMMAND_PREFIX);
         mCommand0.assertNotCalled();
         mCommand1.assertNotCalled();
     }
diff --git a/tests/unit/com/android/documentsui/sorting/SortDimensionTest.java b/tests/unit/com/android/documentsui/sorting/SortDimensionTest.java
index 9468b84..c7abb9a 100644
--- a/tests/unit/com/android/documentsui/sorting/SortDimensionTest.java
+++ b/tests/unit/com/android/documentsui/sorting/SortDimensionTest.java
@@ -84,6 +84,6 @@
     }
     @Test
     public void testParceling() {
-        Parcelables.testParceling(mDimension, 0);
+        Parcelables.assertParcelable(mDimension, 0);
     }
 }
diff --git a/tests/unit/com/android/documentsui/sorting/SortModelTest.java b/tests/unit/com/android/documentsui/sorting/SortModelTest.java
index aa50fb0..c0acdb6 100644
--- a/tests/unit/com/android/documentsui/sorting/SortModelTest.java
+++ b/tests/unit/com/android/documentsui/sorting/SortModelTest.java
@@ -211,12 +211,12 @@
         mModel.sortByUser(DIMENSION_2.getId(), SortDimension.SORT_DIRECTION_DESCENDING);
         mModel.setDimensionVisibility(DIMENSION_3.getId(), View.GONE);
 
-        Parcelables.testParceling(mModel, 0);
+        Parcelables.assertParcelable(mModel, 0);
     }
 
     @Test
     public void testParceling_NoSortedDimension() {
-        Parcelables.testParceling(mModel, 0);
+        Parcelables.assertParcelable(mModel, 0);
     }
 
     private @Nullable SortDimension getSortedDimension() {
diff --git a/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java b/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java
index 1d6a09c..eb1c54f 100644
--- a/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java
+++ b/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java
@@ -17,13 +17,14 @@
 package com.android.documentsui.sorting;
 
 import static com.android.documentsui.base.DocumentInfo.getCursorString;
-
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
 import android.database.Cursor;
 import android.database.MatrixCursor;
+import android.os.Bundle;
+import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.support.test.runner.AndroidJUnit4;
 
@@ -366,6 +367,27 @@
         }
     }
 
+    @Test
+    public void testReturnsWrappedExtras() {
+        MatrixCursor c = new MatrixCursor(COLUMNS);
+        Bundle extras = new Bundle();
+        extras.putBoolean(DocumentsContract.EXTRA_LOADING, true);
+        extras.putString(DocumentsContract.EXTRA_INFO, "cheddar");
+        extras.putString(DocumentsContract.EXTRA_ERROR, "flop");
+        c.setExtras(extras);
+
+        // set sorting to avoid an NPE.
+        sortModel.sortByUser(
+                SortModel.SORT_DIMENSION_ID_DATE,
+                SortDimension.SORT_DIRECTION_DESCENDING);
+
+        Bundle actual = createSortingCursorWrapper(c).getExtras();
+
+        assertTrue(actual.getBoolean(DocumentsContract.EXTRA_LOADING, false));
+        assertEquals("cheddar", actual.getString(DocumentsContract.EXTRA_INFO));
+        assertEquals("flop", actual.getString(DocumentsContract.EXTRA_ERROR));
+    }
+
     private Cursor createSortingCursorWrapper() {
         return createSortingCursorWrapper(cursor);
     }