Merge "Cleaning up mRestoredSelection after restoring selection." into arc-apps
diff --git a/res/layout-sw720dp-land/column_headers.xml b/res/layout-sw720dp-land/column_headers.xml
index 7c4adeb..d5bcb76 100644
--- a/res/layout-sw720dp-land/column_headers.xml
+++ b/res/layout-sw720dp-land/column_headers.xml
@@ -52,7 +52,7 @@
                 android:id="@android:id/title"
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
-                android:layout_weight="0.5"
+                android:layout_weight="0.375"
                 android:layout_marginEnd="12dp"
                 android:focusable="true"
                 android:gravity="center_vertical"
@@ -77,6 +77,20 @@
             </com.android.documentsui.sorting.HeaderCell>
 
             <com.android.documentsui.sorting.HeaderCell
+                android:id="@+id/file_type"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="0.125"
+                android:layout_marginEnd="12dp"
+                android:focusable="true"
+                android:gravity="center_vertical"
+                android:orientation="horizontal"
+                android:animateLayoutChanges="true">
+
+                <include layout="@layout/shared_cell_content" />
+            </com.android.documentsui.sorting.HeaderCell>
+
+            <com.android.documentsui.sorting.HeaderCell
                 android:id="@+id/size"
                 android:layout_width="0dp"
                 android:layout_height="match_parent"
diff --git a/res/layout-sw720dp-land/item_doc_list.xml b/res/layout-sw720dp-land/item_doc_list.xml
index ca43827..e097d23 100644
--- a/res/layout-sw720dp-land/item_doc_list.xml
+++ b/res/layout-sw720dp-land/item_doc_list.xml
@@ -81,7 +81,7 @@
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
                 android:layout_marginEnd="12dp"
-                android:layout_weight="0.5"
+                android:layout_weight="0.375"
                 android:ellipsize="middle"
                 android:singleLine="true"
                 android:textAlignment="viewStart"
@@ -101,6 +101,18 @@
                 android:textColor="?android:attr/textColorSecondary" />
 
             <TextView
+                android:id="@+id/file_type"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginEnd="12dp"
+                android:layout_weight="0.125"
+                android:ellipsize="end"
+                android:singleLine="true"
+                android:textAlignment="viewStart"
+                android:textAppearance="@android:style/TextAppearance.Material.Body1"
+                android:textColor="?android:attr/textColorSecondary" />
+
+            <TextView
                 android:id="@+id/size"
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
diff --git a/res/layout/item_doc_list.xml b/res/layout/item_doc_list.xml
index 9269b28..f6212f0 100644
--- a/res/layout/item_doc_list.xml
+++ b/res/layout/item_doc_list.xml
@@ -114,6 +114,17 @@
                     android:textColor="@color/item_details" />
 
                 <TextView
+                    android:id="@+id/file_type"
+                    android:layout_width="90dp"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="8dp"
+                    android:ellipsize="end"
+                    android:singleLine="true"
+                    android:textAlignment="viewStart"
+                    android:textAppearance="@android:style/TextAppearance.Material.Caption"
+                    android:textColor="@color/item_details" />
+
+                <TextView
                     android:id="@android:id/summary"
                     android:layout_width="0dp"
                     android:layout_height="wrap_content"
diff --git a/res/values/mimes.xml b/res/values/mimes.xml
new file mode 100644
index 0000000..38c0276
--- /dev/null
+++ b/res/values/mimes.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 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.
+  -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <!-- Generic file type with an extention shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="generic_extention_file_type"><xliff:g id="extension" example="APE">%1$s</xliff:g> file</string>
+    <!-- Generic file type without an extention shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="generic_file_type">File</string>
+    <!-- Generic image file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="image_file_type">Image</string>
+    <!-- Image file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="image_extension_file_type"><xliff:g id="fileType" example="JPG">%1$s</xliff:g> image</string>
+    <!-- Generic audio file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="audio_file_type">Audio</string>
+    <!-- Audio file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="audio_extension_file_type"><xliff:g id="fileType" example="MP3">%1$s</xliff:g> audio</string>
+    <!-- Generic video file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="video_file_type">Video</string>
+    <!-- Video file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="video_extension_file_type"><xliff:g id="fileType" example="AVI">%1$s</xliff:g> video</string>
+    <!-- Archive file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="archive_file_type"><xliff:g id="fileType" example="Zip">%1$s</xliff:g> archive</string>
+    <!-- Android application file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="apk_file_type">Android application</string>
+    <!-- Plain text file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="txt_file_type">Plain text</string>
+    <!-- HTML file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="html_file_type">HTML document</string>
+    <!-- PDF file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="pdf_file_type">PDF document</string>
+    <!-- Word document file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="word_file_type">Word document</string>
+    <!-- PowerPoint presentation file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="ppt_file_type">PowerPoint presentation</string>
+    <!-- Excel spreadsheet file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="excel_file_type">Excel spreadsheet</string>
+    <!-- Google document file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="gdoc_file_type">Google document</string>
+    <!-- Google spreadsheet file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="gsheet_file_type">Google spreadsheet</string>
+    <!-- Google presentation file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="gslides_file_type">Google presentation</string>
+    <!-- Google drawing file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="gdraw_file_type">Google drawing</string>
+    <!-- Google table file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="gtable_file_type">Google table</string>
+    <!-- Google form file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="gform_file_type">Google form</string>
+    <!-- Google map file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="gmap_file_type">Google map</string>
+    <!-- Google site file type shown in type column in list view. [CHAR LIMIT=24] -->
+    <string name="gsite_file_type">Google site</string>
+</resources>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 731dfa3..67215d8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -108,10 +108,12 @@
     <string name="sort_dimension_name">Name</string>
     <!-- Table header for metadata of downloaded files, such as download source and progress. [CHAR_LIMIT=24] -->
     <string name="sort_dimension_summary">Summary</string>
-    <!-- Table header for last modified time. [CHAR_LIMIT=18] -->
-    <string name="sort_dimension_date">Modified</string>
+    <!-- Table header for file type. [CHAR_LIMIT=12] -->
+    <string name="sort_dimension_file_type">Type</string>
     <!-- Table header for file size. [CHAR_LIMIT=12] -->
     <string name="sort_dimension_size">Size</string>
+    <!-- Table header for last modified time. [CHAR_LIMIT=18] -->
+    <string name="sort_dimension_date">Modified</string>
 
     <!-- content description to describe ascending sorting used with upward arrow in table header. -->
     <string name="sort_direction_ascending">Ascending</string>
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index b2bccd3..fa474bb 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -533,8 +533,12 @@
 
                 if (DEBUG) Log.d(TAG, "Creating new loader recents.");
                 return new RecentsLoader(
-                        context, mProviders, mState, mInjector.features, mExecutors);
-
+                        context,
+                        mProviders,
+                        mState,
+                        mInjector.features,
+                        mExecutors,
+                        mInjector.fileTypeLookup);
             } else {
 
                 Uri contentsUri = mSearchMgr.isSearching()
@@ -561,6 +565,7 @@
                         mState.stack.peek(),
                         contentsUri,
                         mState.sortModel,
+                        mInjector.fileTypeLookup,
                         mDirectoryReloadLock,
                         mSearchMgr.isSearching());
             }
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index 7b50cc6..30a447c 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -40,6 +40,7 @@
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Features;
 import com.android.documentsui.base.FilteringCursorWrapper;
+import com.android.documentsui.base.Lookup;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.roots.RootCursorWrapper;
 import com.android.documentsui.sorting.SortModel;
@@ -56,6 +57,7 @@
     private final RootInfo mRoot;
     private final Uri mUri;
     private final SortModel mModel;
+    private final Lookup<String, String> mFileTypeLookup;
     private final boolean mSearchMode;
 
     private DocumentInfo mDoc;
@@ -65,21 +67,23 @@
     private Features mFeatures;
 
     public DirectoryLoader(
-            Features freatures,
+            Features features,
             Context context,
             RootInfo root,
             DocumentInfo doc,
             Uri uri,
             SortModel model,
+            Lookup<String, String> fileTypeLookup,
             DirectoryReloadLock lock,
             boolean inSearchMode) {
 
         super(context, ProviderExecutor.forAuthority(root.authority));
-        mFeatures = freatures;
+        mFeatures = features;
         mRoot = root;
         mUri = uri;
         mModel = model;
         mDoc = doc;
+        mFileTypeLookup = fileTypeLookup;
         mSearchMode = inSearchMode;
         mObserver = new LockingContentObserver(lock, this::onContentChanged);
     }
@@ -142,7 +146,7 @@
                         && cursor.getExtras().containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) {
                 if (VERBOSE) Log.d(TAG, "Skipping sort of pre-sorted cursor. Booya!");
             } else {
-                cursor = mModel.sortCursor(cursor);
+                cursor = mModel.sortCursor(cursor, mFileTypeLookup);
             }
             result.cursor = cursor;
         } catch (Exception e) {
diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java
index e0b2559..4c9c65f 100644
--- a/src/com/android/documentsui/DocumentsApplication.java
+++ b/src/com/android/documentsui/DocumentsApplication.java
@@ -28,6 +28,7 @@
 import android.os.RemoteException;
 import android.text.format.DateUtils;
 
+import com.android.documentsui.base.Lookup;
 import com.android.documentsui.clipping.ClipStorage;
 import com.android.documentsui.clipping.ClipStore;
 import com.android.documentsui.clipping.DocumentClipper;
@@ -41,6 +42,7 @@
     private ClipStorage mClipStore;
     private DocumentClipper mClipper;
     private DragAndDropManager mDragAndDropManager;
+    private Lookup<String, String> mFileTypeLookup;
 
     public static ProvidersCache getProvidersCache(Context context) {
         return ((DocumentsApplication) context.getApplicationContext()).mProviders;
@@ -74,6 +76,10 @@
         return ((DocumentsApplication) context.getApplicationContext()).mDragAndDropManager;
     }
 
+    public static Lookup<String, String> getFileTypeLookup(Context context) {
+        return ((DocumentsApplication) context.getApplicationContext()).mFileTypeLookup;
+    }
+
     @Override
     public void onCreate() {
         super.onCreate();
@@ -93,6 +99,8 @@
 
         mDragAndDropManager = DragAndDropManager.create(this, mClipper);
 
+        mFileTypeLookup = new FileTypeMap(this);
+
         final IntentFilter packageFilter = new IntentFilter();
         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
         packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
diff --git a/src/com/android/documentsui/FileTypeMap.java b/src/com/android/documentsui/FileTypeMap.java
new file mode 100644
index 0000000..29f7e4c
--- /dev/null
+++ b/src/com/android/documentsui/FileTypeMap.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.annotation.StringRes;
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.documentsui.base.Lookup;
+import com.android.documentsui.base.MimeTypes;
+
+import libcore.net.MimeUtils;
+
+import java.util.HashMap;
+
+/**
+ * A map from mime type to user friendly type string.
+ */
+public class FileTypeMap implements Lookup<String, String> {
+
+    private static final String TAG = "FileTypeMap";
+
+    private final Resources mRes;
+
+    private final SparseArray<Integer> mMediaTypeStringMap = new SparseArray<>();
+
+    private final HashMap<String, Integer> mFileTypeMap = new HashMap<>();
+    private final HashMap<String, String> mArchiveTypeMap = new HashMap<>();
+    private final HashMap<String, Integer> mSpecialMediaMimeType = new HashMap<>();
+
+    FileTypeMap(Context context) {
+        mRes = context.getResources();
+
+        // Mapping from generic media type string to extension media type string
+        mMediaTypeStringMap.put(R.string.video_file_type, R.string.video_extension_file_type);
+        mMediaTypeStringMap.put(R.string.audio_file_type, R.string.audio_extension_file_type);
+        mMediaTypeStringMap.put(R.string.image_file_type, R.string.image_extension_file_type);
+
+        // Common file types
+        mFileTypeMap.put(MimeTypes.APK_TYPE, R.string.apk_file_type);
+        mFileTypeMap.put("text/plain", R.string.txt_file_type);
+        mFileTypeMap.put("text/html", R.string.html_file_type);
+        mFileTypeMap.put("application/xhtml+xml", R.string.html_file_type);
+        mFileTypeMap.put("application/pdf", R.string.pdf_file_type);
+
+        // MS file types
+        mFileTypeMap.put("application/msword", R.string.word_file_type);
+        mFileTypeMap.put(
+                "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+                R.string.word_file_type);
+        mFileTypeMap.put("application/vnd.ms-powerpoint", R.string.ppt_file_type);
+        mFileTypeMap.put(
+                "application/vnd.openxmlformats-officedocument.presentationml.presentation",
+                R.string.ppt_file_type);
+        mFileTypeMap.put("application/vnd.ms-excel", R.string.excel_file_type);
+        mFileTypeMap.put(
+                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+                R.string.excel_file_type);
+
+        // Google doc types
+        mFileTypeMap.put("application/vnd.google-apps.document", R.string.gdoc_file_type);
+        mFileTypeMap.put("application/vnd.google-apps.spreadsheet", R.string.gsheet_file_type);
+        mFileTypeMap.put("application/vnd.google-apps.presentation", R.string.gslides_file_type);
+        mFileTypeMap.put("application/vnd.google-apps.drawing", R.string.gdraw_file_type);
+        mFileTypeMap.put("application/vnd.google-apps.fusiontable", R.string.gtable_file_type);
+        mFileTypeMap.put("application/vnd.google-apps.form", R.string.gform_file_type);
+        mFileTypeMap.put("application/vnd.google-apps.map", R.string.gmap_file_type);
+        mFileTypeMap.put("application/vnd.google-apps.sites", R.string.gsite_file_type);
+
+        // Archive types
+        mArchiveTypeMap.put("application/rar", "RAR");
+        mArchiveTypeMap.put("application/zip", "Zip");
+        mArchiveTypeMap.put("application/x-tar", "Tar");
+        mArchiveTypeMap.put("application/gzip", "Gzip");
+        mArchiveTypeMap.put("application/x-7z-compressed", "7z");
+        mArchiveTypeMap.put("application/x-rar-compressed", "RAR");
+
+        // Special media mime types
+        mSpecialMediaMimeType.put("application/ogg", R.string.audio_file_type);
+        mSpecialMediaMimeType.put("application/x-flac", R.string.audio_file_type);
+    }
+
+    @Override
+    public String lookup(String mimeType) {
+        if (mFileTypeMap.containsKey(mimeType)) {
+            return getPredefinedFileTypeString(mimeType);
+        }
+
+        if (mArchiveTypeMap.containsKey(mimeType)) {
+            return buildArchiveTypeString(mimeType);
+        }
+
+        if (mSpecialMediaMimeType.containsKey(mimeType)) {
+            int genericType = mSpecialMediaMimeType.get(mimeType);
+            return getFileTypeString(mimeType, mMediaTypeStringMap.get(genericType), genericType);
+        }
+
+        final String[] type = MimeTypes.splitMimeType(mimeType);
+        if (type == null) {
+            Log.w(TAG, "Unexpected mime type " + mimeType);
+            return getGenericFileTypeString();
+        }
+
+        switch (type[0]) {
+            case MimeTypes.IMAGE_PREFIX:
+                return getFileTypeString(
+                        mimeType, R.string.image_extension_file_type, R.string.image_file_type);
+            case MimeTypes.AUDIO_PREFIX:
+                return getFileTypeString(
+                        mimeType, R.string.audio_extension_file_type, R.string.audio_file_type);
+            case MimeTypes.VIDEO_PREFIX:
+                return getFileTypeString(
+                        mimeType, R.string.video_extension_file_type, R.string.video_file_type);
+            default:
+                return getFileTypeString(
+                        mimeType, R.string.generic_extention_file_type, R.string.generic_file_type);
+        }
+    }
+
+    private String buildArchiveTypeString(String mimeType) {
+        final String archiveType = mArchiveTypeMap.get(mimeType);
+
+        assert(!TextUtils.isEmpty(archiveType));
+
+        final String format = mRes.getString(R.string.archive_file_type);
+        return String.format(format, archiveType);
+    }
+
+    private String getPredefinedFileTypeString(String mimeType) {
+        return mRes.getString(mFileTypeMap.get(mimeType));
+    }
+
+    private String getFileTypeString(
+            String mimeType, @StringRes int formatStringId, @StringRes int defaultStringId) {
+        final String extension = MimeUtils.guessExtensionFromMimeType(mimeType);
+
+        return TextUtils.isEmpty(extension)
+                ? mRes.getString(defaultStringId)
+                : String.format(mRes.getString(formatStringId), extension.toUpperCase());
+    }
+
+    private String getGenericFileTypeString() {
+        return mRes.getString(R.string.generic_file_type);
+    }
+}
diff --git a/src/com/android/documentsui/Injector.java b/src/com/android/documentsui/Injector.java
index 5ac897d..dbc0475 100644
--- a/src/com/android/documentsui/Injector.java
+++ b/src/com/android/documentsui/Injector.java
@@ -26,6 +26,7 @@
 import com.android.documentsui.MenuManager.SelectionDetails;
 import com.android.documentsui.base.EventHandler;
 import com.android.documentsui.base.Features;
+import com.android.documentsui.base.Lookup;
 import com.android.documentsui.dirlist.DocumentsAdapter;
 import com.android.documentsui.base.DebugHelper;
 import com.android.documentsui.prefs.ScopedPreferences;
@@ -48,6 +49,7 @@
     public final ActivityConfig config;
     public final ScopedPreferences prefs;
     public final MessageBuilder messages;
+    public final Lookup<String, String> fileTypeLookup;
 
     public MenuManager menuManager;
     public DialogController dialogs;
@@ -76,8 +78,9 @@
             ActivityConfig config,
             ScopedPreferences prefs,
             MessageBuilder messages,
-            DialogController dialogs) {
-        this(features, config, prefs, messages, dialogs, new Model(features));
+            DialogController dialogs,
+            Lookup<String, String> fileTypeLookup) {
+        this(features, config, prefs, messages, dialogs, fileTypeLookup, new Model(features));
     }
 
     @VisibleForTesting
@@ -87,6 +90,7 @@
             ScopedPreferences prefs,
             MessageBuilder messages,
             DialogController dialogs,
+            Lookup<String, String> fileTypeLookup,
             Model model) {
 
         this.features = features;
@@ -94,6 +98,7 @@
         this.prefs = prefs;
         this.messages = messages;
         this.dialogs = dialogs;
+        this.fileTypeLookup = fileTypeLookup;
         this.mModel = model;
         this.debugHelper = new DebugHelper(this);
     }
diff --git a/src/com/android/documentsui/RecentsLoader.java b/src/com/android/documentsui/RecentsLoader.java
index 43209ec..38a3cfa 100644
--- a/src/com/android/documentsui/RecentsLoader.java
+++ b/src/com/android/documentsui/RecentsLoader.java
@@ -88,6 +88,7 @@
     private final State mState;
     private final Features mFeatures;
     private final Lookup<String, Executor> mExecutors;
+    private final Lookup<String, String> mFileTypeMap;
 
     @GuardedBy("mTasks")
     /** A authority -> RecentsTask map */
@@ -99,13 +100,14 @@
     private DirectoryResult mResult;
 
     public RecentsLoader(Context context, ProvidersAccess providers, State state, Features features,
-            Lookup<String, Executor> executors) {
+            Lookup<String, Executor> executors, Lookup<String, String> fileTypeMap) {
 
         super(context);
         mProviders = providers;
         mState = state;
         mFeatures = features;
         mExecutors = executors;
+        mFileTypeMap = fileTypeMap;
 
         // Keep clients around on high-RAM devices, since we'd be spinning them
         // up moments later to fetch thumbnails anyway.
@@ -204,7 +206,7 @@
         }
 
         final Cursor notMovableMasked = new NotMovableMaskCursor(merged);
-        final Cursor sorted = mState.sortModel.sortCursor(notMovableMasked);
+        final Cursor sorted = mState.sortModel.sortCursor(notMovableMasked, mFileTypeMap);
 
         // Tell the UI if this is an in-progress result. When loading is complete, another update is
         // sent with EXTRA_LOADING set to false.
diff --git a/src/com/android/documentsui/base/MimeTypes.java b/src/com/android/documentsui/base/MimeTypes.java
index 0f61e67..44b61ca 100644
--- a/src/com/android/documentsui/base/MimeTypes.java
+++ b/src/com/android/documentsui/base/MimeTypes.java
@@ -24,16 +24,31 @@
 
     private MimeTypes() {}
 
-    private static final String APK_TYPE = "application/vnd.android.package-archive";
+    public static final String APK_TYPE = "application/vnd.android.package-archive";
+
+    public static final String IMAGE_PREFIX = "image";
+    public static final String AUDIO_PREFIX = "audio";
+    public static final String VIDEO_PREFIX = "video";
+
     /**
      * MIME types that are visual in nature. For example, they should always be
      * shown as thumbnails in list mode.
      */
     public static final String[] VISUAL_MIMES = new String[] { "image/*", "video/*" };
 
+    public static @Nullable String[] splitMimeType(String mimeType) {
+        final String[] groups = mimeType.split("/");
+
+        if (groups.length != 2 || groups[0].isEmpty() || groups[1].isEmpty()) {
+            return null;
+        }
+
+        return groups;
+    }
+
     public static String findCommonMimeType(List<String> mimeTypes) {
-        String[] commonType = mimeTypes.get(0).split("/");
-        if (commonType.length != 2) {
+        String[] commonType = splitMimeType(mimeTypes.get(0));
+        if (commonType == null) {
             return "*/*";
         }
 
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 4399a95..ff9edc2 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -84,6 +84,7 @@
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.base.Events.MotionInputEvent;
 import com.android.documentsui.base.Features;
+import com.android.documentsui.base.Lookup;
 import com.android.documentsui.base.RootInfo;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.base.State;
@@ -307,7 +308,9 @@
         mIconHelper = new IconHelper(mActivity, MODE_GRID);
 
         mAdapter = new DirectoryAddonsAdapter(
-                mAdapterEnv, new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper));
+                mAdapterEnv,
+                new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper, mInjector.fileTypeLookup)
+        );
 
         mRecView.setAdapter(mAdapter);
 
diff --git a/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
index d4d13a1..42be8f9 100644
--- a/src/com/android/documentsui/dirlist/ListDocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/ListDocumentHolder.java
@@ -18,6 +18,7 @@
 
 import static com.android.documentsui.base.DocumentInfo.getCursorString;
 
+import android.annotation.Nullable;
 import android.content.Context;
 import android.database.Cursor;
 import android.graphics.Rect;
@@ -31,25 +32,30 @@
 import com.android.documentsui.R;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Events.InputEvent;
+import com.android.documentsui.base.Lookup;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.roots.RootCursorWrapper;
 
 final class ListDocumentHolder extends DocumentHolder {
 
     private final TextView mTitle;
-    private final LinearLayout mDetails;  // Container of date/size/summary
+    private final @Nullable LinearLayout mDetails;  // Container of date/size/summary
     private final TextView mDate;
     private final TextView mSize;
+    private final TextView mType;
     private final TextView mSummary;
     private final ImageView mIconMime;
     private final ImageView mIconThumb;
     private final ImageView mIconCheck;
-    private final IconHelper mIconHelper;
     private final View mIconLayout;
+
+    private final IconHelper mIconHelper;
+    private final Lookup<String, String> mFileTypeLookup;
     // This is used in as a convenience in our bind method.
     private final DocumentInfo mDoc;
 
-    public ListDocumentHolder(Context context, ViewGroup parent, IconHelper iconHelper) {
+    public ListDocumentHolder(Context context, ViewGroup parent, IconHelper iconHelper,
+            Lookup<String, String> fileTypeLookup) {
         super(context, parent, R.layout.item_doc_list);
 
         mIconLayout = itemView.findViewById(android.R.id.icon);
@@ -60,10 +66,12 @@
         mSummary = (TextView) itemView.findViewById(android.R.id.summary);
         mSize = (TextView) itemView.findViewById(R.id.size);
         mDate = (TextView) itemView.findViewById(R.id.date);
+        mType = (TextView) itemView.findViewById(R.id.file_type);
         // Warning: mDetails view doesn't exists in layout-sw720dp-land layout
         mDetails = (LinearLayout) itemView.findViewById(R.id.line2);
 
         mIconHelper = iconHelper;
+        mFileTypeLookup = fileTypeLookup;
         mDoc = new DocumentInfo();
     }
 
@@ -142,7 +150,6 @@
      * Bind this view to the given document for display.
      * @param cursor Pointing to the item to be bound.
      * @param modelId The model ID of the item.
-     * @param state Current display state.
      */
     @Override
     public void bind(Cursor cursor, String modelId) {
@@ -190,8 +197,10 @@
                 mSize.setVisibility(View.VISIBLE);
                 mSize.setText(Formatter.formatFileSize(mContext, mDoc.size));
             } else {
-                mSize.setVisibility(View.GONE);
+                mSize.setVisibility(View.INVISIBLE);
             }
+
+            mType.setText(mFileTypeLookup.lookup(mDoc.mimeType));
         }
 
         // mDetails view doesn't exists in layout-sw720dp-land layout
diff --git a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
index be1091f..921917c 100644
--- a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
@@ -28,6 +28,7 @@
 
 import com.android.documentsui.Model;
 import com.android.documentsui.base.EventListener;
+import com.android.documentsui.base.Lookup;
 import com.android.documentsui.base.State;
 import com.android.documentsui.Model.Update;
 
@@ -45,6 +46,7 @@
     // isn't an ideal pattern (more transitive dependency stuff) but good enough for now.
     private final Environment mEnv;
     private final IconHelper mIconHelper;  // a transitive dependency of the holders.
+    private final Lookup<String, String> mFileTypeLookup;
 
     /**
      * An ordered list of model IDs. This is the data structure that determines what shows up in
@@ -53,9 +55,11 @@
     private List<String> mModelIds = new ArrayList<>();
     private EventListener<Model.Update> mModelUpdateListener;
 
-    public ModelBackedDocumentsAdapter(Environment env, IconHelper iconHelper) {
+    public ModelBackedDocumentsAdapter(
+            Environment env, IconHelper iconHelper, Lookup<String, String> fileTypeLookup) {
         mEnv = env;
         mIconHelper = iconHelper;
+        mFileTypeLookup = fileTypeLookup;
 
         mModelUpdateListener = new EventListener<Model.Update>() {
             @Override
@@ -92,7 +96,8 @@
                 }
                 break;
             case MODE_LIST:
-                holder = new ListDocumentHolder(mEnv.getContext(), parent, mIconHelper);
+                holder = new ListDocumentHolder(
+                        mEnv.getContext(), parent, mIconHelper, mFileTypeLookup);
                 break;
             default:
                 throw new IllegalStateException("Unsupported layout mode.");
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 125f8c3..aeef645 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -84,7 +84,8 @@
                 new Config(),
                 ScopedPreferences.create(this, PREFERENCES_SCOPE),
                 messages,
-                DialogController.create(features, this, messages));
+                DialogController.create(features, this, messages),
+                DocumentsApplication.getFileTypeLookup(this));
 
         super.onCreate(icicle);
 
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index a49c58c..a2a5053 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -34,6 +34,7 @@
 
 import com.android.documentsui.ActionModeController;
 import com.android.documentsui.BaseActivity;
+import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.Injector;
 import com.android.documentsui.MenuManager.DirectoryDetails;
@@ -80,7 +81,8 @@
                 new Config(),
                 ScopedPreferences.create(this, PREFERENCES_SCOPE),
                 new MessageBuilder(this),
-                DialogController.create(features, this, null));
+                DialogController.create(features, this, null),
+                DocumentsApplication.getFileTypeLookup(this));
 
         super.onCreate(icicle);
 
diff --git a/src/com/android/documentsui/sorting/SortModel.java b/src/com/android/documentsui/sorting/SortModel.java
index 05833f7..65b7fc1 100644
--- a/src/com/android/documentsui/sorting/SortModel.java
+++ b/src/com/android/documentsui/sorting/SortModel.java
@@ -26,11 +26,13 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.provider.DocumentsContract.Document;
+import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.View;
 
 import com.android.documentsui.R;
+import com.android.documentsui.base.Lookup;
 import com.android.documentsui.sorting.SortDimension.SortDirection;
 
 import java.lang.annotation.Retention;
@@ -48,8 +50,9 @@
             SORT_DIMENSION_ID_UNKNOWN,
             SORT_DIMENSION_ID_TITLE,
             SORT_DIMENSION_ID_SUMMARY,
-            SORT_DIMENSION_ID_DATE,
-            SORT_DIMENSION_ID_SIZE
+            SORT_DIMENSION_ID_SIZE,
+            SORT_DIMENSION_ID_FILE_TYPE,
+            SORT_DIMENSION_ID_DATE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface SortDimensionId {}
@@ -57,6 +60,7 @@
     public static final int SORT_DIMENSION_ID_TITLE = android.R.id.title;
     public static final int SORT_DIMENSION_ID_SUMMARY = android.R.id.summary;
     public static final int SORT_DIMENSION_ID_SIZE = R.id.size;
+    public static final int SORT_DIMENSION_ID_FILE_TYPE = R.id.file_type;
     public static final int SORT_DIMENSION_ID_DATE = R.id.date;
 
     @IntDef(flag = true, value = {
@@ -96,7 +100,8 @@
     private boolean mIsUserSpecified = false;
     private @Nullable SortDimension mSortedDimension;
 
-    public SortModel(Collection<SortDimension> columns) {
+    @VisibleForTesting
+    SortModel(Collection<SortDimension> columns) {
         mDimensions = new SparseArray<>(columns.size());
 
         for (SortDimension column : columns) {
@@ -221,9 +226,9 @@
         notifyListeners(UPDATE_TYPE_VISIBILITY);
     }
 
-    public Cursor sortCursor(Cursor cursor) {
+    public Cursor sortCursor(Cursor cursor, Lookup<String, String> fileTypesMap) {
         if (mSortedDimension != null) {
-            return new SortingCursorWrapper(cursor, mSortedDimension);
+            return new SortingCursorWrapper(cursor, mSortedDimension, fileTypesMap);
         } else {
             return cursor;
         }
@@ -468,6 +473,16 @@
                 .build()
         );
 
+        // Type column
+        dimensions.add(builder
+            .withId(SORT_DIMENSION_ID_FILE_TYPE)
+            .withLabelId(R.string.sort_dimension_file_type)
+            .withDataType(SortDimension.DATA_TYPE_STRING)
+            .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
+            .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
+            .withVisibility(View.VISIBLE)
+            .build());
+
         // Date column
         dimensions.add(builder
                 .withId(SORT_DIMENSION_ID_DATE)
diff --git a/src/com/android/documentsui/sorting/SortingCursorWrapper.java b/src/com/android/documentsui/sorting/SortingCursorWrapper.java
index 691c1d7..ce50513 100644
--- a/src/com/android/documentsui/sorting/SortingCursorWrapper.java
+++ b/src/com/android/documentsui/sorting/SortingCursorWrapper.java
@@ -24,6 +24,7 @@
 import android.os.Bundle;
 import android.provider.DocumentsContract.Document;
 
+import com.android.documentsui.base.Lookup;
 import com.android.documentsui.base.Shared;
 import com.android.documentsui.sorting.SortModel.SortDimensionId;
 
@@ -36,20 +37,22 @@
 
     private final int[] mPosition;
 
-    public SortingCursorWrapper(Cursor cursor, SortDimension dimension) {
+    public SortingCursorWrapper(
+            Cursor cursor, SortDimension dimension, Lookup<String, String> fileTypeLookup) {
         mCursor = cursor;
 
         final int count = cursor.getCount();
         mPosition = new int[count];
         boolean[] isDirs = new boolean[count];
-        String[] displayNames = null;
+        String[] stringValues = null;
         long[] longValues = null;
         String[] ids = null;
 
         final @SortDimensionId int id = dimension.getId();
         switch (id) {
             case SortModel.SORT_DIMENSION_ID_TITLE:
-                displayNames = new String[count];
+            case SortModel.SORT_DIMENSION_ID_FILE_TYPE:
+                stringValues = new String[count];
                 break;
             case SortModel.SORT_DIMENSION_ID_DATE:
             case SortModel.SORT_DIMENSION_ID_SIZE:
@@ -70,7 +73,10 @@
                 case SortModel.SORT_DIMENSION_ID_TITLE:
                     final String displayName = getCursorString(
                             mCursor, Document.COLUMN_DISPLAY_NAME);
-                    displayNames[i] = displayName;
+                    stringValues[i] = displayName;
+                    break;
+                case SortModel.SORT_DIMENSION_ID_FILE_TYPE:
+                    stringValues[i] = fileTypeLookup.lookup(mimeType);
                     break;
                 case SortModel.SORT_DIMENSION_ID_DATE:
                     longValues[i] = getLastModified(mCursor);
@@ -86,7 +92,8 @@
 
         switch (id) {
             case SortModel.SORT_DIMENSION_ID_TITLE:
-                binarySort(displayNames, isDirs, mPosition, dimension.getSortDirection());
+            case SortModel.SORT_DIMENSION_ID_FILE_TYPE:
+                binarySort(stringValues, isDirs, mPosition, dimension.getSortDirection());
                 break;
             case SortModel.SORT_DIMENSION_ID_DATE:
             case SortModel.SORT_DIMENSION_ID_SIZE:
diff --git a/src/com/android/documentsui/sorting/TableHeaderController.java b/src/com/android/documentsui/sorting/TableHeaderController.java
index da82a6e..72aec53 100644
--- a/src/com/android/documentsui/sorting/TableHeaderController.java
+++ b/src/com/android/documentsui/sorting/TableHeaderController.java
@@ -32,6 +32,7 @@
     private final HeaderCell mTitleCell;
     private final HeaderCell mSummaryCell;
     private final HeaderCell mSizeCell;
+    private final HeaderCell mFileTypeCell;
     private final HeaderCell mDateCell;
 
     // We assign this here porque each method reference creates a new object
@@ -50,6 +51,7 @@
         mTitleCell = (HeaderCell) tableHeader.findViewById(android.R.id.title);
         mSummaryCell = (HeaderCell) tableHeader.findViewById(android.R.id.summary);
         mSizeCell = (HeaderCell) tableHeader.findViewById(R.id.size);
+        mFileTypeCell = (HeaderCell) tableHeader.findViewById(R.id.file_type);
         mDateCell = (HeaderCell) tableHeader.findViewById(R.id.date);
 
         onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED);
@@ -61,6 +63,7 @@
         bindCell(mTitleCell, SortModel.SORT_DIMENSION_ID_TITLE);
         bindCell(mSummaryCell, SortModel.SORT_DIMENSION_ID_SUMMARY);
         bindCell(mSizeCell, SortModel.SORT_DIMENSION_ID_SIZE);
+        bindCell(mFileTypeCell, SortModel.SORT_DIMENSION_ID_FILE_TYPE);
         bindCell(mDateCell, SortModel.SORT_DIMENSION_ID_DATE);
     }
 
diff --git a/tests/common/com/android/documentsui/DocumentsProviderHelper.java b/tests/common/com/android/documentsui/DocumentsProviderHelper.java
index 3045feb..f56e1be 100644
--- a/tests/common/com/android/documentsui/DocumentsProviderHelper.java
+++ b/tests/common/com/android/documentsui/DocumentsProviderHelper.java
@@ -163,7 +163,7 @@
     }
 
     public void assertChildCount(String parentId, int expected) throws Exception {
-        List<DocumentInfo> children = listChildren(parentId);
+        List<DocumentInfo> children = listChildren(parentId, -1);
         assertEquals("Incorrect file count after copy", expected, children.size());
     }
 
@@ -264,10 +264,19 @@
     }
 
     public List<DocumentInfo> listChildren(String documentId) throws Exception {
+        return listChildren(documentId, 100);
+    }
+
+    public List<DocumentInfo> listChildren(Uri parentUri, int maxCount) throws Exception {
+        String id = DocumentsContract.getDocumentId(parentUri);
+        return listChildren(id, maxCount);
+    }
+
+    public List<DocumentInfo> listChildren(String documentId, int maxCount) throws Exception {
         Uri uri = buildChildDocumentsUri(mAuthority, documentId);
         List<DocumentInfo> children = new ArrayList<>();
         try (Cursor cursor = mClient.query(uri, null, null, null, null, null)) {
-            Cursor wrapper = new RootCursorWrapper(mAuthority, "totally-fake", cursor, 100);
+            Cursor wrapper = new RootCursorWrapper(mAuthority, "totally-fake", cursor, maxCount);
             while (wrapper.moveToNext()) {
                 children.add(DocumentInfo.fromDirectoryCursor(wrapper));
             }
@@ -314,4 +323,8 @@
         extra.putLong(DocumentsContract.EXTRA_LOADING, duration);
         mClient.call("setLoadingDuration", null, extra);
     }
+
+    public void configure(String args, Bundle configuration) throws RemoteException {
+        mClient.call("configure", args, configuration);
+    }
 }
diff --git a/tests/common/com/android/documentsui/StubProvider.java b/tests/common/com/android/documentsui/StubProvider.java
index 9b6dbd4..0468bb4 100644
--- a/tests/common/com/android/documentsui/StubProvider.java
+++ b/tests/common/com/android/documentsui/StubProvider.java
@@ -65,6 +65,8 @@
     public static final String EXTRA_STREAM_TYPES
             = "com.android.documentsui.stubprovider.STREAM_TYPES";
     public static final String EXTRA_CONTENT = "com.android.documentsui.stubprovider.CONTENT";
+    public static final String EXTRA_ENABLE_ROOT_NOTIFICATION
+            = "com.android.documentsui.stubprovider.ROOT_NOTIFICATION";
 
     public static final String EXTRA_FLAGS = "com.android.documentsui.stubprovider.FLAGS";
     public static final String EXTRA_PARENT_ID = "com.android.documentsui.stubprovider.PARENT";
@@ -91,6 +93,7 @@
     private SharedPreferences mPrefs;
     private Set<String> mSimulateReadErrorIds = new HashSet<>();
     private long mLoadingDuration = 0;
+    private boolean mRootNotification = true;
 
     @Override
     public void attachInfo(Context context, ProviderInfo info) {
@@ -630,16 +633,19 @@
     private void configure(String arg, Bundle extras) {
         Log.d(TAG, "Configure " + arg);
         String rootName = extras.getString(EXTRA_ROOT, ROOT_0_ID);
-        long rootSize = extras.getLong(EXTRA_SIZE, 1) * 1024 * 1024;
+        long rootSize = extras.getLong(EXTRA_SIZE, 100) * 1024 * 1024;
         setSize(rootName, rootSize);
+        mRootNotification = extras.getBoolean(EXTRA_ENABLE_ROOT_NOTIFICATION, true);
     }
 
     private void notifyParentChanged(String parentId) {
         getContext().getContentResolver().notifyChange(
                 DocumentsContract.buildChildDocumentsUri(mAuthority, parentId), null, false);
-        // Notify also about possible change in remaining space on the root.
-        getContext().getContentResolver().notifyChange(DocumentsContract.buildRootsUri(mAuthority),
-                null, false);
+        if (mRootNotification) {
+            // Notify also about possible change in remaining space on the root.
+            getContext().getContentResolver().notifyChange(
+                    DocumentsContract.buildRootsUri(mAuthority), null, false);
+        }
     }
 
     private void includeDocument(MatrixCursor result, StubDocument document) {
diff --git a/tests/common/com/android/documentsui/bots/UiBot.java b/tests/common/com/android/documentsui/bots/UiBot.java
index 82f4822..93c53e3 100644
--- a/tests/common/com/android/documentsui/bots/UiBot.java
+++ b/tests/common/com/android/documentsui/bots/UiBot.java
@@ -32,7 +32,6 @@
 
 import android.content.Context;
 import android.support.test.espresso.Espresso;
-import android.support.test.espresso.NoMatchingViewException;
 import android.support.test.espresso.action.ViewActions;
 import android.support.test.espresso.matcher.BoundedMatcher;
 import android.support.test.espresso.matcher.ViewMatchers;
diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java
index 8d1b8a0..487585d 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -88,6 +88,7 @@
                 null,       //ScopedPreferences are not required for tests
                 null,   //a MessageBuilder is not required for tests
                 dialogs,
+                new TestFileTypeLookup(),
                 model);
         injector.selectionMgr = selectionMgr;
         injector.focusManager = new FocusManager(features, selectionMgr, null, null, 0);
diff --git a/tests/common/com/android/documentsui/testing/TestFileTypeLookup.java b/tests/common/com/android/documentsui/testing/TestFileTypeLookup.java
new file mode 100644
index 0000000..98d3d01
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestFileTypeLookup.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2017 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.testing;
+
+import com.android.documentsui.base.Lookup;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class TestFileTypeLookup implements Lookup<String, String> {
+
+    public static final String DEFAULT_TYPE = "default_type";
+    public final Map<String, String> fileTypes = new HashMap<>();
+
+    @Override
+    public String lookup(String mimeType) {
+       final String type = fileTypes.get(mimeType);
+
+       return type == null ? DEFAULT_TYPE : type;
+    }
+}
diff --git a/tests/functional/com/android/documentsui/ActivityTest.java b/tests/functional/com/android/documentsui/ActivityTest.java
index 1f97b8b..2c31c7b 100644
--- a/tests/functional/com/android/documentsui/ActivityTest.java
+++ b/tests/functional/com/android/documentsui/ActivityTest.java
@@ -22,6 +22,7 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
@@ -127,6 +128,9 @@
         // so if a drawer is on top of a file we want to select, it will actually click the drawer.
         // Thus to start a clean state, we always try to close first.
         bots.roots.closeDrawer();
+
+        // Configure the provider back to default.
+        mDocsHelper.configure(null, Bundle.EMPTY);
     }
 
     @Override
diff --git a/tests/functional/com/android/documentsui/FileManagementUiTest.java b/tests/functional/com/android/documentsui/FileManagementUiTest.java
index 567429e..5157ec3 100644
--- a/tests/functional/com/android/documentsui/FileManagementUiTest.java
+++ b/tests/functional/com/android/documentsui/FileManagementUiTest.java
@@ -20,12 +20,19 @@
 import static com.android.documentsui.StubProvider.ROOT_1_ID;
 
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.support.test.filters.LargeTest;
 import android.support.test.filters.Suppress;
 import android.view.KeyEvent;
 
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.Shared;
 import com.android.documentsui.files.FilesActivity;
+import com.android.documentsui.sorting.SortDimension;
+import com.android.documentsui.sorting.SortModel;
+
+import java.util.List;
 
 @LargeTest
 public class FileManagementUiTest extends ActivityTest<FilesActivity> {
@@ -122,4 +129,72 @@
 
         bots.directory.waitForDocument("file1.png");
     }
+
+    public void testCopyLargeAmountOfFiles() throws Exception {
+        // Suppress root notification. We're gonna create tons of files and it will soon crash
+        // DocsUI because too many root refreshes are queued in an executor.
+        Bundle conf = new Bundle();
+        conf.putBoolean(StubProvider.EXTRA_ENABLE_ROOT_NOTIFICATION, false);
+        mDocsHelper.configure(null, conf);
+
+        final Uri test = mDocsHelper.createFolder(rootDir0, "test");
+        final Uri target = mDocsHelper.createFolder(rootDir0, "target");
+        String nameOfLastFile = "";
+        for (int i = 0; i <= Shared.MAX_DOCS_IN_INTENT; ++i) {
+            final String name = i + ".txt";
+            final Uri doc =
+                    mDocsHelper.createDocument(test, "text/plain", name);
+            mDocsHelper.writeDocument(doc, Integer.toString(i).getBytes());
+            nameOfLastFile = nameOfLastFile.compareTo(name) < 0 ? name : nameOfLastFile;
+        }
+
+        bots.roots.openRoot(ROOT_0_ID);
+        bots.directory.openDocument("test");
+        bots.sortHeader.sortBy(
+                SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_ASCENDING);
+        bots.directory.waitForDocument("0.txt");
+        bots.keyboard.pressKey(
+                KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_LEFT_ON | KeyEvent.META_CTRL_ON);
+        bots.keyboard.pressKey(
+                KeyEvent.KEYCODE_C, KeyEvent.META_CTRL_LEFT_ON | KeyEvent.META_CTRL_ON);
+
+        bots.roots.openRoot(ROOT_0_ID);
+        bots.directory.openDocument("target");
+        bots.directory.pasteFilesFromClipboard();
+
+        // Use these 2 events as a signal that many files have already been copied. Only considering
+        // Android devices a more reliable way is to wait until notification goes away, but ARC++
+        // uses Chrome OS notifications so it isn't even an option.
+        bots.directory.waitForDocument("0.txt");
+        bots.directory.waitForDocument(nameOfLastFile);
+
+        final int expectedCount = Shared.MAX_DOCS_IN_INTENT + 1;
+        List<DocumentInfo> children = mDocsHelper.listChildren(target, -1);
+        if (children.size() == expectedCount) {
+            return;
+        }
+
+        // Files weren't copied fast enough, so gonna do some polling until they all arrive or copy
+        // seems stalled.
+        while (true) {
+            Thread.sleep(200);
+            List<DocumentInfo> newChildren = mDocsHelper.listChildren(target, -1);
+            if (newChildren.size() == expectedCount) {
+                return;
+            }
+
+            if (newChildren.size() > expectedCount) {
+                // Should never happen
+                fail("Something wrong with this test case. Copied file count "
+                        + newChildren.size() + " exceeds expected number " + expectedCount);
+            }
+
+            if (newChildren.size() <= children.size()) {
+                fail("Only copied " + children.size()
+                        + " files, expected to copy " + expectedCount + " files.");
+            }
+
+            children = newChildren;
+        }
+    }
 }
diff --git a/tests/unit/com/android/documentsui/FileTypeMapTest.java b/tests/unit/com/android/documentsui/FileTypeMapTest.java
new file mode 100644
index 0000000..13f2137
--- /dev/null
+++ b/tests/unit/com/android/documentsui/FileTypeMapTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 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;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.annotation.StringRes;
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FileTypeMapTest {
+
+    private Resources mRes;
+    private FileTypeMap mMap;
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getTargetContext();
+        mRes = context.getResources();
+        mMap = new FileTypeMap(context);
+    }
+
+    @Test
+    public void testPlainTextType() {
+        String expected = mRes.getString(R.string.txt_file_type);
+        assertEquals(expected, mMap.lookup("text/plain"));
+    }
+
+    @Test
+    public void testPortableDocumentFormatType() {
+        String expected = mRes.getString(R.string.pdf_file_type);
+        assertEquals(expected, mMap.lookup("application/pdf"));
+    }
+
+    @Test
+    public void testMsWordType() {
+        String expected = mRes.getString(R.string.word_file_type);
+        assertEquals(expected, mMap.lookup("application/msword"));
+    }
+
+    @Test
+    public void testGoogleDocType() {
+        String expected = mRes.getString(R.string.gdoc_file_type);
+        assertEquals(expected, mMap.lookup("application/vnd.google-apps.document"));
+    }
+
+    @Test
+    public void testZipType() {
+        String expected = getExtensionType(R.string.archive_file_type, "Zip");
+        assertEquals(expected, mMap.lookup("application/zip"));
+    }
+
+    @Test
+    public void testMp3Type() {
+        String expected = getExtensionType(R.string.audio_extension_file_type, "MP3");
+        assertEquals(expected, mMap.lookup("audio/mpeg"));
+    }
+
+    @Test
+    public void testMkvType() {
+        String expected = getExtensionType(R.string.video_extension_file_type, "AVI");
+        assertEquals(expected, mMap.lookup("video/avi"));
+    }
+
+    @Test
+    public void testJpgType() {
+        String expected = getExtensionType(R.string.image_extension_file_type, "JPG");
+        assertEquals(expected, mMap.lookup("image/jpeg"));
+    }
+
+    @Test
+    public void testOggType() {
+        String expected = getExtensionType(R.string.audio_extension_file_type, "OGG");
+        assertEquals(expected, mMap.lookup("application/ogg"));
+    }
+
+    @Test
+    public void testFlacType() {
+        String expected = getExtensionType(R.string.audio_extension_file_type, "FLAC");
+        assertEquals(expected, mMap.lookup("application/x-flac"));
+    }
+
+    private String getExtensionType(@StringRes int formatStringId, String extension) {
+        String format = mRes.getString(formatStringId);
+        return String.format(format, extension);
+    }
+}
diff --git a/tests/unit/com/android/documentsui/RecentsLoaderTests.java b/tests/unit/com/android/documentsui/RecentsLoaderTests.java
index 80993ba..657432f 100644
--- a/tests/unit/com/android/documentsui/RecentsLoaderTests.java
+++ b/tests/unit/com/android/documentsui/RecentsLoaderTests.java
@@ -29,6 +29,7 @@
 import com.android.documentsui.testing.ActivityManagers;
 import com.android.documentsui.testing.TestEnv;
 import com.android.documentsui.testing.TestFeatures;
+import com.android.documentsui.testing.TestFileTypeLookup;
 import com.android.documentsui.testing.TestImmediateExecutor;
 import com.android.documentsui.testing.TestProvidersAccess;
 
@@ -54,7 +55,7 @@
         mEnv.state.acceptMimes = new String[] { "*/*" };
 
         mLoader = new RecentsLoader(mActivity, mEnv.providers, mEnv.state, mEnv.features,
-                TestImmediateExecutor.createLookup());
+                TestImmediateExecutor.createLookup(), new TestFileTypeLookup());
     }
 
     @Test
diff --git a/tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java b/tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java
index 4ce5e21..68169bc 100644
--- a/tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java
@@ -31,6 +31,7 @@
 import com.android.documentsui.base.State;
 import com.android.documentsui.testing.TestActionHandler;
 import com.android.documentsui.testing.TestEnv;
+import com.android.documentsui.testing.TestFileTypeLookup;
 
 @MediumTest
 public class DirectoryAddonsAdapterTest extends AndroidTestCase {
@@ -53,7 +54,7 @@
         mAdapter = new DirectoryAddonsAdapter(
             env,
             new ModelBackedDocumentsAdapter(
-                    env, new IconHelper(testContext, State.MODE_GRID)));
+                    env, new IconHelper(testContext, State.MODE_GRID), new TestFileTypeLookup()));
 
         mEnv.model.addUpdateListener(mAdapter.getModelUpdateListener());
     }
diff --git a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
index 18c5ef9..88ea13d 100644
--- a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
@@ -28,6 +28,7 @@
 import com.android.documentsui.base.State;
 import com.android.documentsui.testing.TestActionHandler;
 import com.android.documentsui.testing.TestEnv;
+import com.android.documentsui.testing.TestFileTypeLookup;
 
 @MediumTest
 public class ModelBackedDocumentsAdapterTest extends AndroidTestCase {
@@ -47,7 +48,7 @@
         DocumentsAdapter.Environment env = new TestEnvironment(testContext);
 
         mAdapter = new ModelBackedDocumentsAdapter(
-                env, new IconHelper(testContext, State.MODE_GRID));
+                env, new IconHelper(testContext, State.MODE_GRID), new TestFileTypeLookup());
         mAdapter.getModelUpdateListener().accept(Model.Update.UPDATE);
     }
 
diff --git a/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java b/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java
index eb1c54f..6705c4d 100644
--- a/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java
+++ b/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java
@@ -26,6 +26,7 @@
 import android.os.Bundle;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
+import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
 import com.android.documentsui.base.DocumentInfo;
@@ -33,6 +34,7 @@
 import com.android.documentsui.roots.RootCursorWrapper;
 import com.android.documentsui.sorting.SortModel.SortDimensionId;
 import com.android.documentsui.testing.SortModels;
+import com.android.documentsui.testing.TestFileTypeLookup;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -46,6 +48,7 @@
 import java.util.Set;
 
 @RunWith(AndroidJUnit4.class)
+@SmallTest
 public class SortingCursorWrapperTest {
     private static final int ITEM_COUNT = 10;
     private static final String AUTHORITY = "test_authority";
@@ -73,12 +76,40 @@
             "%$%VD"
     };
 
+    private static final String[] MIMES = new String[] {
+            "application/zip",
+            "video/3gp",
+            "image/png",
+            "text/plain",
+            "application/msword",
+            "text/html",
+            "application/pdf",
+            "image/png",
+            "audio/flac",
+            "audio/mp3"
+    };
+
+    private static final String[] TYPES = new String[] {
+            "Zip archive",
+            "3GP video",
+            "PNG image",
+            "Plain text",
+            "Word document",
+            "HTML document",
+            "PDF document",
+            "PNG image",
+            "FLAC audio",
+            "MP3 audio"
+    };
+
+    private TestFileTypeLookup fileTypeLookup;
     private SortModel sortModel;
     private Cursor cursor;
 
     @Before
     public void setUp() {
         sortModel = SortModels.createTestSortModel();
+        fileTypeLookup = new TestFileTypeLookup();
 
         Random rand = new Random();
         MatrixCursor c = new MatrixCursor(COLUMNS);
@@ -91,6 +122,7 @@
             // to actually do something.
             row.add(Document.COLUMN_DISPLAY_NAME, NAMES[i]);
             row.add(Document.COLUMN_SIZE, rand.nextInt());
+            row.add(Document.COLUMN_MIME_TYPE, MIMES[i]);
         }
 
         cursor = c;
@@ -368,6 +400,76 @@
     }
 
     @Test
+    public void testSort_type_ascending() {
+        populateTypeMap();
+
+        sortModel.sortByUser(
+                SortModel.SORT_DIMENSION_ID_FILE_TYPE, SortDimension.SORT_DIRECTION_ASCENDING);
+
+        final Cursor cursor = createSortingCursorWrapper();
+
+        assertEquals(ITEM_COUNT, cursor.getCount());
+        final BitSet seen = new BitSet(ITEM_COUNT);
+        List<String> types = new ArrayList<>(ITEM_COUNT);
+        for (int i = 0; i < ITEM_COUNT; ++i) {
+            cursor.moveToPosition(i);
+            final String mime =
+                    DocumentInfo.getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+            final String type = fileTypeLookup.lookup(mime);
+            types.add(type);
+
+            seen.set(DocumentInfo.getCursorInt(cursor, Document.COLUMN_DOCUMENT_ID));
+        }
+
+        // Check all items were accounted for
+        assertEquals(ITEM_COUNT, seen.cardinality());
+        for (int i = 0; i < ITEM_COUNT - 1; ++i) {
+            final String lhs = types.get(i);
+            final String rhs = types.get(i + 1);
+            assertTrue(lhs + " is not smaller than " + rhs,
+                    Shared.compareToIgnoreCaseNullable(lhs, rhs) <= 0);
+        }
+    }
+
+    @Test
+    public void testSort_type_descending() {
+        populateTypeMap();
+
+        sortModel.sortByUser(
+                SortModel.SORT_DIMENSION_ID_FILE_TYPE, SortDimension.SORT_DIRECTION_DESCENDING);
+
+        final Cursor cursor = createSortingCursorWrapper();
+
+        assertEquals(ITEM_COUNT, cursor.getCount());
+        final BitSet seen = new BitSet(ITEM_COUNT);
+        List<String> types = new ArrayList<>(ITEM_COUNT);
+        for (int i = 0; i < ITEM_COUNT; ++i) {
+            cursor.moveToPosition(i);
+            final String mime =
+                    DocumentInfo.getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+            final String type = fileTypeLookup.lookup(mime);
+            types.add(type);
+
+            seen.set(DocumentInfo.getCursorInt(cursor, Document.COLUMN_DOCUMENT_ID));
+        }
+
+        // Check all items were accounted for
+        assertEquals(ITEM_COUNT, seen.cardinality());
+        for (int i = 0; i < ITEM_COUNT - 1; ++i) {
+            final String lhs = types.get(i);
+            final String rhs = types.get(i + 1);
+            assertTrue(lhs + " is not smaller than " + rhs,
+                    Shared.compareToIgnoreCaseNullable(lhs, rhs) >= 0);
+        }
+    }
+
+    private void populateTypeMap() {
+        for (int i = 0; i < ITEM_COUNT; ++i) {
+            fileTypeLookup.fileTypes.put(MIMES[i], TYPES[i]);
+        }
+    }
+
+    @Test
     public void testReturnsWrappedExtras() {
         MatrixCursor c = new MatrixCursor(COLUMNS);
         Bundle extras = new Bundle();
@@ -394,6 +496,6 @@
 
     private Cursor createSortingCursorWrapper(Cursor c) {
         final @SortDimensionId int id = sortModel.getSortedDimensionId();
-        return new SortingCursorWrapper(c, sortModel.getDimensionById(id));
+        return new SortingCursorWrapper(c, sortModel.getDimensionById(id), fileTypeLookup);
     }
 }