Add a type column to show user-friendly type info.

Also add a special map from mime type to user friendly strings.

Test: Auto tests & smoke tests.
Bug: 34844878
Change-Id: I773f2bae524b7dffc6eeda1a1e92330b5ad64473
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 e10cdba..fda6b77 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);
     }