Merge "Set SCALING_MODE_NO_SCALE_CROP for SurfaceView."
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 5df6ba8..eda0982 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -4142,6 +4142,10 @@
      * <p>When account management is disabled for an account type, adding or removing an account
      * of that type will not be possible.
      *
+     * <p>From {@link android.os.Build.VERSION_CODES#N} the profile or device owner can still use
+     * {@link android.accounts.AccountManager} APIs to add or remove accounts when account
+     * management for a specific type is disabled.
+     *
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
      * @param accountType For which account management is disabled or enabled.
      * @param disabled The boolean indicating that account management will be disabled (true) or
diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java
index 135f369..d5491d3 100644
--- a/core/java/android/os/ParcelFileDescriptor.java
+++ b/core/java/android/os/ParcelFileDescriptor.java
@@ -233,6 +233,19 @@
         final FileDescriptor fd = openInternal(file, mode);
         if (fd == null) return null;
 
+        return fromFd(fd, handler, listener);
+    }
+
+    /** {@hide} */
+    public static ParcelFileDescriptor fromFd(
+            FileDescriptor fd, Handler handler, final OnCloseListener listener) throws IOException {
+        if (handler == null) {
+            throw new IllegalArgumentException("Handler must not be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener must not be null");
+        }
+
         final FileDescriptor[] comm = createCommSocketPair();
         final ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd, comm[0]);
         final MessageQueue queue = handler.getLooper().getQueue();
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 16696af..037916a 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -57,6 +57,10 @@
      * Authenticator.
      * The default value is <code>false</code>.
      *
+     * <p>From {@link android.os.Build.VERSION_CODES#N} a profile or device owner app can still
+     * use {@link android.accounts.AccountManager} APIs to add or remove accounts when account
+     * management is disallowed.
+     *
      * <p/>Key for user restrictions.
      * <p/>Type: Boolean
      * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index ec24af5..ef6b467 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -47,7 +47,7 @@
 
     <!-- How much the content in the divider is inset from the window bounds when resting. Used to
          calculate the bounds of the stacks-->
-    <dimen name="docked_stack_divider_insets">18dp</dimen>
+    <dimen name="docked_stack_divider_insets">19dp</dimen>
 
     <!-- Min width for a tablet device -->
     <dimen name="min_xlarge_screen_width">800dp</dimen>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Shared.java b/packages/DocumentsUI/src/com/android/documentsui/Shared.java
index 570c9bf..c3366c3 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Shared.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Shared.java
@@ -17,6 +17,8 @@
 package com.android.documentsui;
 
 import android.content.Context;
+import android.text.format.DateUtils;
+import android.text.format.Time;
 
 /** @hide */
 public final class Shared {
@@ -40,4 +42,26 @@
     public static final String getQuantityString(Context context, int resourceId, int quantity) {
         return context.getResources().getQuantityString(resourceId, quantity, quantity);
     }
+
+    public static String formatTime(Context context, long when) {
+        // TODO: DateUtils should make this easier
+        Time then = new Time();
+        then.set(when);
+        Time now = new Time();
+        now.setToNow();
+
+        int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
+                | DateUtils.FORMAT_ABBREV_ALL;
+
+        if (then.year != now.year) {
+            flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
+        } else if (then.yearDay != now.yearDay) {
+            flags |= DateUtils.FORMAT_SHOW_DATE;
+        } else {
+            flags |= DateUtils.FORMAT_SHOW_TIME;
+        }
+
+        return DateUtils.formatDateTime(context, when, flags);
+    }
+
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index b340cd0..035ae77 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -23,7 +23,6 @@
 import static com.android.documentsui.State.MODE_UNKNOWN;
 import static com.android.documentsui.State.SORT_ORDER_UNKNOWN;
 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
-import static com.android.documentsui.model.DocumentInfo.getCursorLong;
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
 import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.internal.util.Preconditions.checkState;
@@ -36,22 +35,18 @@
 import android.app.FragmentTransaction;
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.ClipData;
-import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.Loader;
 import android.database.Cursor;
-import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Point;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Bundle;
-import android.os.CancellationSignal;
-import android.os.OperationCanceledException;
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
@@ -65,16 +60,12 @@
 import android.support.v7.widget.RecyclerView.RecyclerListener;
 import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.text.format.Formatter;
-import android.text.format.Time;
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.TypedValue;
 import android.view.ActionMode;
 import android.view.DragEvent;
 import android.view.GestureDetector;
-import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
@@ -93,22 +84,17 @@
 import com.android.documentsui.DocumentsActivity;
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.Events;
-import com.android.documentsui.IconUtils;
 import com.android.documentsui.Menus;
 import com.android.documentsui.MessageBar;
 import com.android.documentsui.MimePredicate;
-import com.android.documentsui.ProviderExecutor;
-import com.android.documentsui.ProviderExecutor.Preemptable;
 import com.android.documentsui.R;
 import com.android.documentsui.RecentLoader;
 import com.android.documentsui.RecentsProvider;
 import com.android.documentsui.RecentsProvider.StateColumns;
-import com.android.documentsui.RootCursorWrapper;
 import com.android.documentsui.RootsCache;
 import com.android.documentsui.Shared;
 import com.android.documentsui.Snackbars;
 import com.android.documentsui.State;
-import com.android.documentsui.ThumbnailCache;
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
@@ -153,6 +139,8 @@
     private Model.UpdateListener mModelUpdateListener = new ModelUpdateListener();
     private ItemClickListener mItemClickListener = new ItemClickListener();
 
+    private IconHelper mIconHelper;
+
     private View mEmptyView;
     private RecyclerView mRecView;
 
@@ -162,9 +150,6 @@
     private int mLastMode = MODE_UNKNOWN;
     private int mLastSortOrder = SORT_ORDER_UNKNOWN;
     private boolean mLastShowSize;
-    private boolean mHideGridTitles;
-    private boolean mSvelteRecents;
-    private Point mThumbSize;
     private DocumentsAdapter mAdapter;
     private LoaderCallbacks<DirectoryResult> mCallbacks;
     private FragmentTuner mTuner;
@@ -177,9 +162,6 @@
     private MessageBar mMessageBar;
     private View mProgressBar;
 
-    private int mSelectedItemColor;
-    private int mDefaultItemColor;
-
     public static void showNormal(FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) {
         show(fm, TYPE_NORMAL, root, doc, null, anim);
     }
@@ -307,12 +289,9 @@
         final RootInfo root = getArguments().getParcelable(EXTRA_ROOT);
         final DocumentInfo doc = getArguments().getParcelable(EXTRA_DOC);
 
-        mAdapter = new DocumentsAdapter(context);
+        mAdapter = new DocumentsAdapter();
         mRecView.setAdapter(mAdapter);
 
-        mDefaultItemColor = context.getResources().getColor(R.color.item_doc_background);
-        mSelectedItemColor = context.getResources().getColor(R.color.item_doc_background_selected);
-
         GestureDetector.SimpleOnGestureListener listener =
                 new GestureDetector.SimpleOnGestureListener() {
                     @Override
@@ -364,17 +343,22 @@
         mTuner = FragmentTuner.pick(state);
         mClipper = new DocumentClipper(context);
 
+        mIconHelper = new IconHelper(context, state.derivedMode);
+
+        boolean hideGridTitles;
         if (mType == TYPE_RECENT_OPEN) {
             // Hide titles when showing recents for picking images/videos
-            mHideGridTitles = MimePredicate.mimeMatches(
+            hideGridTitles = MimePredicate.mimeMatches(
                     MimePredicate.VISUAL_MIMES, state.acceptMimes);
         } else {
-            mHideGridTitles = (doc != null) && doc.isGridTitlesHidden();
+            hideGridTitles = (doc != null) && doc.isGridTitlesHidden();
         }
+        GridDocumentHolder.setHideTitles(hideGridTitles);
 
         final ActivityManager am = (ActivityManager) context.getSystemService(
                 Context.ACTIVITY_SERVICE);
-        mSvelteRecents = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
+        boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
+        mIconHelper.setThumbnailsEnabled(!svelte);
 
         mCallbacks = new LoaderCallbacks<DirectoryResult>() {
             @Override
@@ -585,12 +569,9 @@
      * classes as needed.
      */
     private void updateLayout(int mode) {
-        final int thumbSize;
-
         final LayoutManager layout;
         switch (mode) {
             case MODE_GRID:
-                thumbSize = getResources().getDimensionPixelSize(R.dimen.grid_width);
                 if (mGridLayout == null) {
                     mGridLayout = new GridLayoutManager(getContext(), mColumnCount);
                     mGridLayout.setSpanSizeLookup(mAdapter.createSpanSizeLookup());
@@ -598,7 +579,6 @@
                 layout = mGridLayout;
                 break;
             case MODE_LIST:
-                thumbSize = getResources().getDimensionPixelSize(R.dimen.icon_size);
                 if (mListLayout == null) {
                     mListLayout = new LinearLayoutManager(getContext());
                 }
@@ -614,7 +594,7 @@
         // imperatively calling this function.
         mSelectionManager.handleLayoutChanged();
         // setting layout manager automatically invalidates existing ViewHolders.
-        mThumbSize = new Point(thumbSize, thumbSize);
+        mIconHelper.setMode(mode);
     }
 
     private int calculateColumnCount() {
@@ -775,14 +755,10 @@
         }
     }
 
-    private static void cancelThumbnailTask(View view) {
+    private void cancelThumbnailTask(View view) {
         final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb);
         if (iconThumb != null) {
-            final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
-            if (oldTask != null) {
-                oldTask.preempt();
-                iconThumb.setTag(null);
-            }
+            mIconHelper.stopLoading(iconThumb);
         }
     }
 
@@ -921,59 +897,6 @@
         return ((BaseActivity) getActivity()).getDisplayState();
     }
 
-    // Provide a reference to the views for each data item
-    // Complex data items may need more than one view per item, and
-    // you provide access to all the views for a data item in a view holder
-    final class DocumentHolder
-            extends RecyclerView.ViewHolder
-            implements View.OnKeyListener
-    {
-        public String modelId;
-        private ClickListener mClickListener;
-        private View.OnKeyListener mKeyListener;
-
-        public DocumentHolder(View view) {
-            super(view);
-            view.setOnKeyListener(this);
-        }
-
-        public void setSelected(boolean selected) {
-            itemView.setActivated(selected);
-            itemView.setBackgroundColor(selected ? mSelectedItemColor : mDefaultItemColor);
-        }
-
-        @Override
-        public boolean onKey(View v, int keyCode, KeyEvent event) {
-            // Intercept enter key-up events, and treat them as clicks.  Forward other events.
-            if (event.getAction() == KeyEvent.ACTION_UP &&
-                    keyCode == KeyEvent.KEYCODE_ENTER) {
-                if (mClickListener != null) {
-                    mClickListener.onClick(this);
-                }
-                return true;
-            } else if (mKeyListener != null) {
-                return mKeyListener.onKey(v, keyCode, event);
-            }
-            return false;
-        }
-
-        public void addClickListener(ClickListener listener) {
-            // Just handle one for now; switch to a list if necessary.
-            checkState(mClickListener == null);
-            mClickListener = listener;
-        }
-
-        public void addOnKeyListener(View.OnKeyListener listener) {
-            // Just handle one for now; switch to a list if necessary.
-            checkState(mKeyListener == null);
-            mKeyListener = listener;
-        }
-    }
-
-    interface ClickListener {
-        public void onClick(DocumentHolder doc);
-    }
-
     void showEmptyView() {
         mEmptyView.setVisibility(View.VISIBLE);
         mRecView.setVisibility(View.GONE);
@@ -1002,11 +925,9 @@
             implements Model.UpdateListener {
 
         private static final String TAG = "DocumentsAdapter";
-        private static final int ITEM_TYPE_LAYOUT_DIVIDER = 0;
-        private static final int ITEM_TYPE_DOCUMENT = 1;
-        private static final int ITEM_TYPE_DIRECTORY = 2;
-
-        private final Context mContext;
+        public static final int ITEM_TYPE_LAYOUT_DIVIDER = 0;
+        public static final int ITEM_TYPE_DOCUMENT = 1;
+        public static final int ITEM_TYPE_DIRECTORY = 2;
 
         /**
          * An ordered list of model IDs. This is the data structure that determines what shows up in
@@ -1018,10 +939,6 @@
         // position where the transition happens.
         private int mDividerPosition;
 
-        public DocumentsAdapter(Context context) {
-            mContext = context;
-        }
-
         public GridLayoutManager.SpanSizeLookup createSpanSizeLookup() {
             return new GridLayoutManager.SpanSizeLookup() {
                 @Override
@@ -1039,43 +956,27 @@
 
         @Override
         public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-            View item = null;
+            if (viewType == ITEM_TYPE_LAYOUT_DIVIDER) {
+                return new EmptyDocumentHolder(getContext());
+            };
 
-            switch (viewType) {
-                case ITEM_TYPE_DIRECTORY:
-                case ITEM_TYPE_DOCUMENT:
-                    item = createItemView(parent);
-                    break;
-                case ITEM_TYPE_LAYOUT_DIVIDER:
-                    item = createLayoutWhitespace();
-                    break;
-            }
-
-            DocumentHolder holder = new DocumentHolder(item);
-            holder.addClickListener(mItemClickListener);
-            holder.addOnKeyListener(mSelectionManager);
-            return holder;
-        }
-
-        private View createItemView(ViewGroup parent) {
+            DocumentHolder holder = null;
             final State state = getDisplayState();
-            final LayoutInflater inflater = LayoutInflater.from(getContext());
-
             switch (state.derivedMode) {
                 case MODE_GRID:
-                    return  inflater.inflate(R.layout.item_doc_grid, parent, false);
+                    holder = new GridDocumentHolder(getContext(), parent, mIconHelper, viewType);
+                    break;
                 case MODE_LIST:
-                    return inflater.inflate(R.layout.item_doc_list, parent, false);
+                    holder = new ListDocumentHolder(getContext(), parent, mIconHelper);
+                    break;
                 case MODE_UNKNOWN:
                 default:
                     throw new IllegalStateException("Unsupported layout mode.");
             }
-        }
 
-        private View createLayoutWhitespace() {
-            View whitespace = new View(getContext());
-            whitespace.setVisibility(View.GONE);
-            return whitespace;
+            holder.addClickListener(mItemClickListener);
+            holder.addOnKeyListener(mSelectionManager);
+            return holder;
         }
 
         /**
@@ -1108,165 +1009,17 @@
                 return;
             }
 
-            final Context context = getContext();
-            final State state = getDisplayState();
-            final RootsCache roots = DocumentsApplication.getRootsCache(context);
-            final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
-                    context, mThumbSize);
+            String modelId = mModelIds.get(position);
+            Cursor cursor = mModel.getItem(modelId);
+            holder.bind(cursor, modelId, getDisplayState());
 
-            holder.modelId = mModelIds.get(position);
-            final Cursor cursor = mModel.getItem(holder.modelId);
-            checkNotNull(cursor, "Cursor cannot be null.");
-
-            final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
-            final String docRootId = getCursorString(cursor, RootCursorWrapper.COLUMN_ROOT_ID);
-            final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
             final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
-            final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
-            final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
-            final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
             final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
-            final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
-            final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
 
-            final View itemView = holder.itemView;
-
-            holder.setSelected(isSelected(holder.modelId));
-
-            final ImageView iconMime = (ImageView) itemView.findViewById(R.id.icon_mime);
-            final ImageView iconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb);
-            final TextView title = (TextView) itemView.findViewById(android.R.id.title);
-            final ImageView icon1 = (ImageView) itemView.findViewById(android.R.id.icon1);
-            final TextView summary = (TextView) itemView.findViewById(android.R.id.summary);
-            final TextView date = (TextView) itemView.findViewById(R.id.date);
-            final TextView size = (TextView) itemView.findViewById(R.id.size);
-
-            final ThumbnailAsyncTask oldTask = (ThumbnailAsyncTask) iconThumb.getTag();
-            if (oldTask != null) {
-                oldTask.preempt();
-                iconThumb.setTag(null);
-            }
-
-            iconMime.animate().cancel();
-            iconThumb.animate().cancel();
-
-            final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
-            final boolean allowThumbnail = (state.derivedMode == MODE_GRID)
-                    || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, docMimeType);
-            final boolean showThumbnail = supportsThumbnail && allowThumbnail && !mSvelteRecents;
-
-            final boolean enabled = mTuner.isDocumentEnabled(docMimeType, docFlags);
-            final float iconAlpha = (state.derivedMode == MODE_LIST && !enabled) ? 0.5f : 1f;
-
-            boolean cacheHit = false;
-            if (showThumbnail) {
-                final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
-                final Bitmap cachedResult = thumbs.get(uri);
-                if (cachedResult != null) {
-                    iconThumb.setImageBitmap(cachedResult);
-                    cacheHit = true;
-                } else {
-                    iconThumb.setImageDrawable(null);
-                    // TODO: Hang this off DocumentHolder?
-                    final ThumbnailAsyncTask task = new ThumbnailAsyncTask(
-                            uri, iconMime, iconThumb, mThumbSize, iconAlpha);
-                    iconThumb.setTag(task);
-                    ProviderExecutor.forAuthority(docAuthority).execute(task);
-                }
-            }
-
-            // Always throw MIME icon into place, even when a thumbnail is being
-            // loaded in background.
-            if (cacheHit) {
-                iconMime.setAlpha(0f);
-                iconMime.setImageDrawable(null);
-                iconThumb.setAlpha(1f);
-            } else {
-                iconMime.setAlpha(1f);
-                iconThumb.setAlpha(0f);
-                iconThumb.setImageDrawable(null);
-                iconMime.setImageDrawable(
-                        getDocumentIcon(mContext, docAuthority, docId, docMimeType, docIcon, state));
-            }
-
-            if ((state.derivedMode == MODE_GRID) && mHideGridTitles) {
-                title.setVisibility(View.GONE);
-            } else {
-                title.setText(docDisplayName);
-                title.setVisibility(View.VISIBLE);
-            }
-
-            Drawable iconDrawable = null;
-            if (mType == TYPE_RECENT_OPEN) {
-                // We've already had to enumerate roots before any results can
-                // be shown, so this will never block.
-                final RootInfo root = roots.getRootBlocking(docAuthority, docRootId);
-                iconDrawable = root.loadIcon(mContext);
-
-                if (summary != null) {
-                    final boolean alwaysShowSummary = getResources()
-                            .getBoolean(R.bool.always_show_summary);
-                    if (alwaysShowSummary) {
-                        summary.setText(root.getDirectoryString());
-                        summary.setVisibility(View.VISIBLE);
-                    } else {
-                        if (iconDrawable != null && roots.isIconUniqueBlocking(root)) {
-                            // No summary needed if icon speaks for itself
-                            summary.setVisibility(View.INVISIBLE);
-                        } else {
-                            summary.setText(root.getDirectoryString());
-                            summary.setVisibility(View.VISIBLE);
-                            summary.setTextAlignment(TextView.TEXT_ALIGNMENT_TEXT_END);
-                        }
-                    }
-                }
-            } else {
-                // Directories showing thumbnails in grid mode get a little icon
-                // hint to remind user they're a directory.
-                if (Document.MIME_TYPE_DIR.equals(docMimeType) && state.derivedMode == MODE_GRID
-                        && showThumbnail) {
-                    iconDrawable = IconUtils.applyTintAttr(mContext, R.drawable.ic_doc_folder,
-                            android.R.attr.textColorPrimaryInverse);
-                }
-
-                if (summary != null) {
-                    if (docSummary != null) {
-                        summary.setText(docSummary);
-                        summary.setVisibility(View.VISIBLE);
-                    } else {
-                        summary.setVisibility(View.INVISIBLE);
-                    }
-                }
-            }
-
-            if (iconDrawable != null) {
-                icon1.setVisibility(View.VISIBLE);
-                icon1.setImageDrawable(iconDrawable);
-            } else {
-                icon1.setVisibility(View.GONE);
-            }
-
-            if (docLastModified == -1) {
-                date.setText(null);
-            } else {
-                date.setText(formatTime(mContext, docLastModified));
-            }
-
-            if (!state.showSize || Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
-                size.setVisibility(View.GONE);
-            } else {
-                size.setVisibility(View.VISIBLE);
-                size.setText(Formatter.formatFileSize(mContext, docSize));
-            }
-
-            setEnabledRecursive(itemView, enabled);
-
-            iconMime.setAlpha(iconAlpha);
-            iconThumb.setAlpha(iconAlpha);
-            icon1.setAlpha(iconAlpha);
-
+            holder.setSelected(isSelected(modelId));
+            holder.setEnabled(mTuner.isDocumentEnabled(docMimeType, docFlags));
             if (DEBUG_ENABLE_DND) {
-                setupDragAndDropOnDocumentView(itemView, cursor);
+                setupDragAndDropOnDocumentView(holder.itemView, cursor);
             }
         }
 
@@ -1389,27 +1142,6 @@
         }
     }
 
-    private static String formatTime(Context context, long when) {
-        // TODO: DateUtils should make this easier
-        Time then = new Time();
-        then.set(when);
-        Time now = new Time();
-        now.setToNow();
-
-        int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
-                | DateUtils.FORMAT_ABBREV_ALL;
-
-        if (then.year != now.year) {
-            flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
-        } else if (then.yearDay != now.yearDay) {
-            flags |= DateUtils.FORMAT_SHOW_DATE;
-        } else {
-            flags |= DateUtils.FORMAT_SHOW_TIME;
-        }
-
-        return DateUtils.formatDateTime(context, when, flags);
-    }
-
     private String findCommonMimeType(List<String> mimeTypes) {
         String[] commonType = mimeTypes.get(0).split("/");
         if (commonType.length != 2) {
@@ -1434,19 +1166,6 @@
         return commonType[0] + "/" + commonType[1];
     }
 
-    private void setEnabledRecursive(View v, boolean enabled) {
-        if (v == null) return;
-        if (v.isEnabled() == enabled) return;
-        v.setEnabled(enabled);
-
-        if (v instanceof ViewGroup) {
-            final ViewGroup vg = (ViewGroup) v;
-            for (int i = vg.getChildCount() - 1; i >= 0; i--) {
-                setEnabledRecursive(vg.getChildAt(i), enabled);
-            }
-        }
-    }
-
     private void copyFromClipboard() {
         new AsyncTask<Void, Void, List<DocumentInfo>>() {
 
@@ -1721,89 +1440,12 @@
     private Drawable getDragShadowIcon(List<DocumentInfo> docs) {
         if (docs.size() == 1) {
             final DocumentInfo doc = docs.get(0);
-            return getDocumentIcon(getActivity(), doc.authority, doc.documentId,
-                    doc.mimeType, doc.icon, getDisplayState());
+            return mIconHelper.getDocumentIcon(getActivity(), doc.authority, doc.documentId,
+                    doc.mimeType, doc.icon);
         }
         return getActivity().getDrawable(R.drawable.ic_doc_generic);
     }
 
-    public static Drawable getDocumentIcon(Context context, String docAuthority, String docId,
-            String docMimeType, int docIcon, State state) {
-        if (docIcon != 0) {
-            return IconUtils.loadPackageIcon(context, docAuthority, docIcon);
-        } else {
-            return IconUtils.loadMimeIcon(context, docMimeType, docAuthority, docId,
-                    state.derivedMode);
-        }
-    }
-
-    private static class ThumbnailAsyncTask extends AsyncTask<Uri, Void, Bitmap>
-            implements Preemptable {
-        private final Uri mUri;
-        private final ImageView mIconMime;
-        private final ImageView mIconThumb;
-        private final Point mThumbSize;
-        private final float mTargetAlpha;
-        private final CancellationSignal mSignal;
-
-        public ThumbnailAsyncTask(Uri uri, ImageView iconMime, ImageView iconThumb, Point thumbSize,
-                float targetAlpha) {
-            mUri = uri;
-            mIconMime = iconMime;
-            mIconThumb = iconThumb;
-            mThumbSize = thumbSize;
-            mTargetAlpha = targetAlpha;
-            mSignal = new CancellationSignal();
-        }
-
-        @Override
-        public void preempt() {
-            cancel(false);
-            mSignal.cancel();
-        }
-
-        @Override
-        protected Bitmap doInBackground(Uri... params) {
-            if (isCancelled()) return null;
-
-            final Context context = mIconThumb.getContext();
-            final ContentResolver resolver = context.getContentResolver();
-
-            ContentProviderClient client = null;
-            Bitmap result = null;
-            try {
-                client = DocumentsApplication.acquireUnstableProviderOrThrow(
-                        resolver, mUri.getAuthority());
-                result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
-                if (result != null) {
-                    final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
-                            context, mThumbSize);
-                    thumbs.put(mUri, result);
-                }
-            } catch (Exception e) {
-                if (!(e instanceof OperationCanceledException)) {
-                    Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e);
-                }
-            } finally {
-                ContentProviderClient.releaseQuietly(client);
-            }
-            return result;
-        }
-
-        @Override
-        protected void onPostExecute(Bitmap result) {
-            if (mIconThumb.getTag() == this && result != null) {
-                mIconThumb.setTag(null);
-                mIconThumb.setImageBitmap(result);
-
-                mIconMime.setAlpha(mTargetAlpha);
-                mIconMime.animate().alpha(0f).start();
-                mIconThumb.setAlpha(0f);
-                mIconThumb.animate().alpha(mTargetAlpha).start();
-            }
-        }
-    }
-
     private class DrawableShadowBuilder extends View.DragShadowBuilder {
 
         private final Drawable mShadow;
@@ -1854,7 +1496,7 @@
         return mSelectionManager.getSelection().contains(modelId);
     }
 
-    private class ItemClickListener implements ClickListener {
+    private class ItemClickListener implements DocumentHolder.ClickListener {
         @Override
         public void onClick(DocumentHolder doc) {
             if (mSelectionManager.hasSelection()) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
new file mode 100644
index 0000000..a01021f
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DocumentHolder.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import static com.android.internal.util.Preconditions.checkState;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.documentsui.R;
+import com.android.documentsui.State;
+
+public abstract class DocumentHolder
+        extends RecyclerView.ViewHolder
+        implements View.OnKeyListener {
+
+    public @Nullable String modelId;
+
+    final int mSelectedItemColor;
+    final int mDefaultItemColor;
+    final boolean mAlwaysShowSummary;
+    final Context mContext;
+    final IconHelper mIconHelper;
+
+    private ListDocumentHolder.ClickListener mClickListener;
+    private View.OnKeyListener mKeyListener;
+
+    public DocumentHolder(Context context, ViewGroup parent, int layout, IconHelper iconHelper) {
+        this(context, inflateLayout(context, parent, layout), iconHelper);
+    }
+
+    public DocumentHolder(Context context, View item, IconHelper iconHelper) {
+        super(item);
+
+        itemView.setOnKeyListener(this);
+
+        mContext = context;
+
+        mDefaultItemColor = context.getColor(R.color.item_doc_background);
+        mSelectedItemColor = context.getColor(R.color.item_doc_background_selected);
+        mAlwaysShowSummary = context.getResources().getBoolean(R.bool.always_show_summary);
+
+        mIconHelper = iconHelper;
+    }
+
+    /**
+     * Binds the view to the given item data.
+     * @param cursor
+     * @param modelId
+     * @param state
+     */
+    public abstract void bind(Cursor cursor, String modelId, State state);
+
+    public void setSelected(boolean selected) {
+        itemView.setActivated(selected);
+        itemView.setBackgroundColor(selected ? mSelectedItemColor : mDefaultItemColor);
+    }
+
+    @Override
+    public boolean onKey(View v, int keyCode, KeyEvent event) {
+        // Intercept enter key-up events, and treat them as clicks.  Forward other events.
+        if (event.getAction() == KeyEvent.ACTION_UP &&
+                keyCode == KeyEvent.KEYCODE_ENTER) {
+            if (mClickListener != null) {
+                mClickListener.onClick(this);
+            }
+            return true;
+        } else if (mKeyListener != null) {
+            return mKeyListener.onKey(v, keyCode, event);
+        }
+        return false;
+    }
+
+    public void addClickListener(ListDocumentHolder.ClickListener listener) {
+        // Just handle one for now; switch to a list if necessary.
+        checkState(mClickListener == null);
+        mClickListener = listener;
+    }
+
+    public void addOnKeyListener(View.OnKeyListener listener) {
+        // Just handle one for now; switch to a list if necessary.
+        checkState(mKeyListener == null);
+        mKeyListener = listener;
+    }
+
+    public void setEnabled(boolean enabled) {
+        setEnabledRecursive(itemView, enabled);
+    }
+
+    static void setEnabledRecursive(View itemView, boolean enabled) {
+        if (itemView == null) return;
+        if (itemView.isEnabled() == enabled) return;
+        itemView.setEnabled(enabled);
+
+        if (itemView instanceof ViewGroup) {
+            final ViewGroup vg = (ViewGroup) itemView;
+            for (int i = vg.getChildCount() - 1; i >= 0; i--) {
+                setEnabledRecursive(vg.getChildAt(i), enabled);
+            }
+        }
+    }
+
+    private static View inflateLayout(Context context, ViewGroup parent, int layout) {
+        final LayoutInflater inflater = LayoutInflater.from(context);
+        return inflater.inflate(layout, parent, false);
+    }
+
+    interface ClickListener {
+        public void onClick(DocumentHolder doc);
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/EmptyDocumentHolder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/EmptyDocumentHolder.java
new file mode 100644
index 0000000..0bdf530
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/EmptyDocumentHolder.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.View;
+
+import com.android.documentsui.State;
+
+final class EmptyDocumentHolder extends DocumentHolder {
+    public EmptyDocumentHolder(Context context) {
+        super(context, new View(context), null);
+        itemView.setVisibility(View.GONE);
+    }
+
+    public void bind(Cursor cursor, String modelId, State state) {
+        // Nothing to bind.
+        return;
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/GridDocumentHolder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/GridDocumentHolder.java
new file mode 100644
index 0000000..43256c3
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/GridDocumentHolder.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import static com.android.documentsui.model.DocumentInfo.getCursorInt;
+import static com.android.documentsui.model.DocumentInfo.getCursorLong;
+import static com.android.documentsui.model.DocumentInfo.getCursorString;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.text.format.Formatter;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.documentsui.R;
+import com.android.documentsui.RootCursorWrapper;
+import com.android.documentsui.Shared;
+import com.android.documentsui.State;
+
+final class GridDocumentHolder extends DocumentHolder {
+    private static boolean mHideTitles;
+
+    public GridDocumentHolder(
+            Context context, ViewGroup parent, IconHelper thumbnailLoader, int viewType) {
+        super(context, parent, R.layout.item_doc_grid, thumbnailLoader);
+    }
+
+    /**
+     * 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.
+     */
+    public void bind(Cursor cursor, String modelId, State state) {
+        this.modelId = modelId;
+
+        checkNotNull(cursor, "Cursor cannot be null.");
+
+        final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
+        final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
+        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+        final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
+        final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
+        final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
+        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+        final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
+
+        final TextView title = (TextView) itemView.findViewById(android.R.id.title);
+        final TextView date = (TextView) itemView.findViewById(R.id.date);
+        final TextView size = (TextView) itemView.findViewById(R.id.size);
+        final ImageView iconMime = (ImageView) itemView.findViewById(R.id.icon_mime);
+        final ImageView iconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb);
+
+        mIconHelper.stopLoading(iconThumb);
+
+        iconMime.animate().cancel();
+        iconMime.setAlpha(1f);
+        iconThumb.animate().cancel();
+        iconThumb.setAlpha(0f);
+
+        final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
+        mIconHelper.loadThumbnail(uri, docMimeType, docFlags, docIcon, iconThumb, iconMime);
+
+        if (mHideTitles) {
+            title.setVisibility(View.GONE);
+        } else {
+            title.setText(docDisplayName);
+            title.setVisibility(View.VISIBLE);
+        }
+
+        if (docLastModified == -1) {
+            date.setText(null);
+        } else {
+            date.setText(Shared.formatTime(mContext, docLastModified));
+        }
+
+        if (!state.showSize || Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
+            size.setVisibility(View.GONE);
+        } else {
+            size.setVisibility(View.VISIBLE);
+            size.setText(Formatter.formatFileSize(mContext, docSize));
+        }
+    }
+
+    /**
+     * Sets whether to hide titles on subsequently created GridDocumentHolder items.
+     * @param hideTitles
+     */
+    public static void setHideTitles(boolean hideTitles) {
+        mHideTitles = hideTitles;
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/IconHelper.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/IconHelper.java
new file mode 100644
index 0000000..ff70eaf
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/IconHelper.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import static com.android.documentsui.Shared.DEBUG;
+import static com.android.documentsui.State.MODE_GRID;
+import static com.android.documentsui.State.MODE_LIST;
+import static com.android.documentsui.State.MODE_UNKNOWN;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.util.Log;
+import android.widget.ImageView;
+
+import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.IconUtils;
+import com.android.documentsui.MimePredicate;
+import com.android.documentsui.ProviderExecutor;
+import com.android.documentsui.ProviderExecutor.Preemptable;
+import com.android.documentsui.R;
+import com.android.documentsui.ThumbnailCache;
+
+/**
+ * A class to assist with loading and managing the Images (i.e. thumbnails and icons) associated
+ * with items in the directory listing.
+ */
+public class IconHelper {
+    private static String TAG = "IconHelper";
+
+    private Context mContext;
+    private ThumbnailCache mCache;
+    private Point mThumbSize;
+    // The display mode (MODE_GRID, MODE_LIST, etc).
+    private int mMode;
+    private boolean mThumbnailsEnabled = true;
+
+    /**
+     * @param context
+     * @param mode MODE_GRID or MODE_LIST
+     */
+    public IconHelper(Context context, int mode) {
+        mContext = context;
+        setMode(mode);
+        mCache = DocumentsApplication.getThumbnailsCache(context, mThumbSize);
+    }
+
+    /**
+     * Enables or disables thumbnails. When thumbnails are disabled, mime icons (or custom icons, if
+     * specified by the document) are used instead.
+     *
+     * @param enabled
+     */
+    public void setThumbnailsEnabled(boolean enabled) {
+        mThumbnailsEnabled = enabled;
+    }
+
+    /**
+     * Sets the current display mode.  This affects the thumbnail sizes that are loaded.
+     * @param mode See {@link State.MODE_LIST} and {@link State.MODE_GRID}.
+     */
+    public void setMode(int mode) {
+        // TODO: Instead of exposing setMode, make the mode final, and make separate instances for
+        // grid/list.
+        int thumbSize;
+        switch (mode) {
+            case MODE_GRID:
+                thumbSize = mContext.getResources().getDimensionPixelSize(R.dimen.grid_width);
+                break;
+            case MODE_LIST:
+                thumbSize = mContext.getResources().getDimensionPixelSize(R.dimen.icon_size);
+                break;
+            case MODE_UNKNOWN:
+            default:
+                throw new IllegalArgumentException("Unsupported layout mode: " + mode);
+        }
+        mMode = mode;
+        mThumbSize = new Point(thumbSize, thumbSize);
+    }
+
+    /**
+     * Cancels any ongoing load operations associated with the given ImageView.
+     * @param icon
+     */
+    public void stopLoading(ImageView icon) {
+        final LoaderTask oldTask = (LoaderTask) icon.getTag();
+        if (oldTask != null) {
+            oldTask.preempt();
+            icon.setTag(null);
+        }
+    }
+
+    /** Internal task for loading thumbnails asynchronously. */
+    private static class LoaderTask
+            extends AsyncTask<Uri, Void, Bitmap>
+            implements Preemptable {
+        private final Uri mUri;
+        private final ImageView mIconMime;
+        private final ImageView mIconThumb;
+        private final Point mThumbSize;
+        private final CancellationSignal mSignal;
+
+        public LoaderTask(Uri uri, ImageView iconMime, ImageView iconThumb,
+                Point thumbSize) {
+            mUri = uri;
+            mIconMime = iconMime;
+            mIconThumb = iconThumb;
+            mThumbSize = thumbSize;
+            mSignal = new CancellationSignal();
+            if (DEBUG) Log.d(TAG, "Starting icon loader task for " + mUri);
+        }
+
+        @Override
+        public void preempt() {
+            if (DEBUG) Log.d(TAG, "Icon loader task for " + mUri + " was cancelled.");
+            cancel(false);
+            mSignal.cancel();
+        }
+
+        @Override
+        protected Bitmap doInBackground(Uri... params) {
+            if (isCancelled())
+                return null;
+
+            final Context context = mIconThumb.getContext();
+            final ContentResolver resolver = context.getContentResolver();
+
+            ContentProviderClient client = null;
+            Bitmap result = null;
+            try {
+                client = DocumentsApplication.acquireUnstableProviderOrThrow(
+                        resolver, mUri.getAuthority());
+                result = DocumentsContract.getDocumentThumbnail(client, mUri, mThumbSize, mSignal);
+                if (result != null) {
+                    final ThumbnailCache thumbs = DocumentsApplication.getThumbnailsCache(
+                            context, mThumbSize);
+                    thumbs.put(mUri, result);
+                }
+            } catch (Exception e) {
+                if (!(e instanceof OperationCanceledException)) {
+                    Log.w(TAG, "Failed to load thumbnail for " + mUri + ": " + e);
+                }
+            } finally {
+                ContentProviderClient.releaseQuietly(client);
+            }
+            return result;
+        }
+
+        @Override
+        protected void onPostExecute(Bitmap result) {
+            if (DEBUG) Log.d(TAG, "Loader task for " + mUri + " completed");
+
+            if (mIconThumb.getTag() == this && result != null) {
+                mIconThumb.setTag(null);
+                mIconThumb.setImageBitmap(result);
+
+                float alpha = mIconMime.getAlpha();
+                mIconMime.animate().alpha(0f).start();
+                mIconThumb.setAlpha(0f);
+                mIconThumb.animate().alpha(alpha).start();
+            }
+        }
+    }
+
+    /**
+     * Load thumbnails for a directory list item.
+     * @param uri The URI for the file being represented.
+     * @param mimeType The mime type of the file being represented.
+     * @param docFlags Flags for the file being represented.
+     * @param docIcon Custom icon (if any) for the file being requested.
+     * @param iconThumb The itemview's thumbnail icon.
+     * @param iconMime The itemview's mime icon.
+     * @return
+     */
+    public void loadThumbnail(Uri uri, String mimeType, int docFlags, int docIcon,
+            ImageView iconThumb, ImageView iconMime) {
+        boolean cacheHit = false;
+
+        final String docAuthority = uri.getAuthority();
+
+        final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
+        final boolean allowThumbnail = (mMode == MODE_GRID)
+                || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mimeType);
+        final boolean showThumbnail = supportsThumbnail && allowThumbnail && mThumbnailsEnabled;
+        if (showThumbnail) {
+            final Bitmap cachedResult = mCache.get(uri);
+            if (cachedResult != null) {
+                iconThumb.setImageBitmap(cachedResult);
+                cacheHit = true;
+            } else {
+                iconThumb.setImageDrawable(null);
+                final LoaderTask task = new LoaderTask(uri, iconMime, iconThumb, mThumbSize);
+                iconThumb.setTag(task);
+                ProviderExecutor.forAuthority(docAuthority).execute(task);
+            }
+        }
+
+        if (cacheHit) {
+            iconMime.setImageDrawable(null);
+            iconMime.setAlpha(0f);
+            iconThumb.setAlpha(1f);
+        } else {
+            // Add a mime icon if the thumbnail is being loaded in the background.
+            iconThumb.setImageDrawable(null);
+            iconMime.setImageDrawable(getDocumentIcon(
+                    mContext, docAuthority, DocumentsContract.getDocumentId(uri), mimeType, docIcon));
+            iconMime.setAlpha(1f);
+            iconThumb.setAlpha(0f);
+        }
+    }
+
+    /**
+     * Gets a mime icon or package icon for a file.
+     * @param context
+     * @param authority The authority string of the file.
+     * @param id The document ID of the file.
+     * @param mimeType The mime type of the file.
+     * @param icon The custom icon (if any) of the file.
+     * @return
+     */
+    public Drawable getDocumentIcon(Context context, String authority, String id,
+            String mimeType, int icon) {
+        if (icon != 0) {
+            return IconUtils.loadPackageIcon(context, authority, icon);
+        } else {
+            return IconUtils.loadMimeIcon(context, mimeType, authority, id, mMode);
+        }
+    }
+
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListDocumentHolder.java
new file mode 100644
index 0000000..b46a0e5a
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ListDocumentHolder.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import static com.android.documentsui.model.DocumentInfo.getCursorInt;
+import static com.android.documentsui.model.DocumentInfo.getCursorLong;
+import static com.android.documentsui.model.DocumentInfo.getCursorString;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.text.format.Formatter;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.documentsui.R;
+import com.android.documentsui.RootCursorWrapper;
+import com.android.documentsui.Shared;
+import com.android.documentsui.State;
+
+final class ListDocumentHolder extends DocumentHolder {
+    final ImageView mIconMime;
+    final ImageView mIconThumb;
+    final ImageView mIcon1;
+
+    public ListDocumentHolder(Context context, ViewGroup parent, IconHelper thumbnailLoader) {
+        super(context, parent, R.layout.item_doc_list, thumbnailLoader);
+
+        mIconMime = (ImageView) itemView.findViewById(R.id.icon_mime);
+        mIconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb);
+        mIcon1 = (ImageView) itemView.findViewById(android.R.id.icon1);
+    }
+
+    /**
+     * 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, State state) {
+        this.modelId = modelId;
+
+        checkNotNull(cursor, "Cursor cannot be null.");
+
+        final String docAuthority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
+        final String docId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
+        final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+        final String docDisplayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
+        final long docLastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
+        final int docIcon = getCursorInt(cursor, Document.COLUMN_ICON);
+        final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
+        final String docSummary = getCursorString(cursor, Document.COLUMN_SUMMARY);
+        final long docSize = getCursorLong(cursor, Document.COLUMN_SIZE);
+
+        final TextView title = (TextView) itemView.findViewById(android.R.id.title);
+        final TextView summary = (TextView) itemView.findViewById(android.R.id.summary);
+        final TextView date = (TextView) itemView.findViewById(R.id.date);
+        final TextView size = (TextView) itemView.findViewById(R.id.size);
+
+        mIconHelper.stopLoading(mIconThumb);
+
+        mIconMime.animate().cancel();
+        mIconThumb.animate().cancel();
+
+        final Uri uri = DocumentsContract.buildDocumentUri(docAuthority, docId);
+        mIconHelper.loadThumbnail(uri, docMimeType, docFlags, docIcon, mIconThumb, mIconMime);
+
+        title.setText(docDisplayName);
+        title.setVisibility(View.VISIBLE);
+
+        if (docSummary != null) {
+            summary.setText(docSummary);
+            summary.setVisibility(View.VISIBLE);
+        } else {
+            summary.setVisibility(View.INVISIBLE);
+        }
+
+        if (docLastModified == -1) {
+            date.setText(null);
+        } else {
+            date.setText(Shared.formatTime(mContext, docLastModified));
+        }
+
+        if (!state.showSize || Document.MIME_TYPE_DIR.equals(docMimeType) || docSize == -1) {
+            size.setVisibility(View.GONE);
+        } else {
+            size.setVisibility(View.VISIBLE);
+            size.setText(Formatter.formatFileSize(mContext, docSize));
+        }
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+        final float iconAlpha = enabled ? 1f : 0.5f;
+        mIconMime.setAlpha(iconAlpha);
+        mIconThumb.setAlpha(iconAlpha);
+        mIcon1.setAlpha(iconAlpha);
+    }
+}
diff --git a/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp b/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp
index 9267f4c..f592a1f 100644
--- a/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp
+++ b/packages/MtpDocumentsProvider/jni/com_android_mtp_AppFuse.cpp
@@ -62,6 +62,19 @@
     }
 };
 
+class ScopedFd {
+    int mFd;
+
+public:
+    explicit ScopedFd(int fd) : mFd(fd) {}
+    ~ScopedFd() {
+        close(mFd);
+    }
+    operator int() {
+        return mFd;
+    }
+};
+
 /**
  * The class is used to access AppFuse class in Java from fuse handlers.
  */
@@ -70,24 +83,26 @@
     AppFuse(JNIEnv* /*env*/, jobject /*self*/) {
     }
 
-    void handle_fuse_request(int fd, const FuseRequest& req) {
+    bool handle_fuse_request(int fd, const FuseRequest& req) {
         ALOGV("Request op=%d", req.header().opcode);
         switch (req.header().opcode) {
             // TODO: Handle more operations that are enough to provide seekable
             // FD.
             case FUSE_INIT:
                 invoke_handler(fd, req, &AppFuse::handle_fuse_init);
-                break;
+                return true;
             case FUSE_GETATTR:
                 invoke_handler(fd, req, &AppFuse::handle_fuse_getattr);
-                break;
+                return true;
+            case FUSE_FORGET:
+                return false;
             default: {
                 ALOGV("NOTIMPL op=%d uniq=%" PRIx64 " nid=%" PRIx64 "\n",
                       req.header().opcode,
                       req.header().unique,
                       req.header().nodeid);
                 fuse_reply(fd, req.header().unique, -ENOSYS, NULL, 0);
-                break;
+                return true;
             }
         }
     }
@@ -198,7 +213,7 @@
 
 jboolean com_android_mtp_AppFuse_start_app_fuse_loop(
         JNIEnv* env, jobject self, jint jfd) {
-    const int fd = static_cast<int>(jfd);
+    ScopedFd fd(dup(static_cast<int>(jfd)));
     AppFuse appfuse(env, self);
 
     ALOGD("Start fuse loop.");
@@ -209,7 +224,7 @@
         if (result < 0) {
             if (errno == ENODEV) {
                 ALOGE("Someone stole our marbles!\n");
-                return false;
+                return JNI_FALSE;
             }
             ALOGE("Failed to read bytes from FD: errno=%d\n", errno);
             continue;
@@ -227,7 +242,9 @@
             continue;
         }
 
-        appfuse.handle_fuse_request(fd, request);
+        if (!appfuse.handle_fuse_request(fd, request)) {
+            return JNI_TRUE;
+        }
     }
 }
 
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java b/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java
index e9edeb9..2c09ad1 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/AppFuse.java
@@ -18,10 +18,13 @@
 
 import android.os.ParcelFileDescriptor;
 import android.os.storage.StorageManager;
+import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.File;
+import java.io.IOException;
+
 import android.os.Process;
 
 /**
@@ -55,6 +58,21 @@
     }
 
     @VisibleForTesting
+    void close() {
+        try {
+            // Remote side of ParcelFileDescriptor is tracking the close of mDeviceFd, and unmount
+            // the corresponding fuse file system. The mMessageThread will receive FUSE_FORGET, and
+            // then terminate itself.
+            mDeviceFd.close();
+            mMessageThread.join();
+        } catch (IOException exp) {
+            Log.e(MtpDocumentsProvider.TAG, "Failed to close device FD.", exp);
+        } catch (InterruptedException exp) {
+            Log.e(MtpDocumentsProvider.TAG, "Failed to terminate message thread.", exp);
+        }
+    }
+
+    @VisibleForTesting
     File getMountPoint() {
         return new File("/mnt/appfuse/" + Process.myUid() + "_" + mName);
     }
diff --git a/packages/MtpDocumentsProvider/src/com/android/mtp/RootScanner.java b/packages/MtpDocumentsProvider/src/com/android/mtp/RootScanner.java
index 619ef54..c216c77 100644
--- a/packages/MtpDocumentsProvider/src/com/android/mtp/RootScanner.java
+++ b/packages/MtpDocumentsProvider/src/com/android/mtp/RootScanner.java
@@ -105,7 +105,7 @@
         public void run() {
             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
             int pollingCount = 0;
-            while (!Thread.interrupted()) {
+            while (true) {
                 boolean changed = false;
 
                 // Update devices.
@@ -147,8 +147,7 @@
                     Thread.sleep(pollingCount > SHORT_POLLING_TIMES ?
                         LONG_POLLING_INTERVAL : SHORT_POLLING_INTERVAL);
                 } catch (InterruptedException exp) {
-                    // The while condition handles the interrupted flag.
-                    continue;
+                    break;
                 }
             }
         }
diff --git a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java
index a145756..b66d8eb 100644
--- a/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java
+++ b/packages/MtpDocumentsProvider/tests/src/com/android/mtp/AppFuseTest.java
@@ -17,6 +17,8 @@
 package com.android.mtp;
 
 import android.os.storage.StorageManager;
+import android.system.ErrnoException;
+import android.system.Os;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
@@ -26,12 +28,17 @@
 public class AppFuseTest extends AndroidTestCase {
     /**
      * TODO: Enable this test after adding SELinux policies for appfuse.
+     * @throws ErrnoException
+     * @throws InterruptedException
      */
-    public void testBasic() {
+    public void disabled_testBasic() throws ErrnoException, InterruptedException {
         final StorageManager storageManager = getContext().getSystemService(StorageManager.class);
         final AppFuse appFuse = new AppFuse("test");
         appFuse.mount(storageManager);
         final File file = appFuse.getMountPoint();
         assertTrue(file.isDirectory());
+        assertEquals(1, Os.stat(file.getPath()).st_ino);
+        appFuse.close();
+        assertTrue(1 != Os.stat(file.getPath()).st_ino);
     }
 }
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 1c4c012..c6d9e98 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -108,6 +108,7 @@
     <uses-permission android:name="android.permission.REGISTER_SIM_SUBSCRIPTION" />
     <uses-permission android:name="android.permission.GET_APP_OPS_STATS" />
     <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.MANAGE_ACTIVITY_STACKS" />
 
     <application android:label="@string/app_label"
                  android:forceDeviceEncrypted="true"
diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
index a995ec7..2377684 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml
@@ -96,14 +96,14 @@
     <LinearLayout
         android:id="@+id/date_time_group"
         android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
+        android:layout_height="28dp"
         android:layout_alignParentStart="true"
         android:layout_alignParentTop="true"
         android:orientation="horizontal">
 
         <include layout="@layout/split_clock_view"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
+            android:layout_height="match_parent"
             android:layout_marginStart="16dp"
             android:layout_marginTop="2dp"
             android:id="@+id/clock" />
@@ -111,28 +111,28 @@
         <com.android.systemui.statusbar.policy.DateView
             android:id="@+id/date"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
+            android:layout_height="match_parent"
             android:layout_marginStart="6dp"
             android:layout_marginTop="8dp"
-            android:layout_alignParentTop="true"
             android:drawableStart="@drawable/header_dot"
             android:drawablePadding="6dp"
             android:singleLine="true"
             android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Clock"
             android:textSize="@dimen/qs_time_collapsed_size"
+            android:gravity="top"
             systemui:datePattern="@string/abbrev_wday_month_day_no_year_alarm" />
 
         <com.android.systemui.statusbar.AlphaOptimizedButton
             android:id="@+id/alarm_status"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_alignParentTop="true"
+            android:layout_height="match_parent"
+            android:layout_marginTop="8dp"
             android:drawablePadding="6dp"
             android:drawableStart="@drawable/ic_access_alarms_small"
             android:textColor="#64ffffff"
             android:textAppearance="@style/TextAppearance.StatusBar.Expanded.Date"
-            android:minHeight="36dp"
             android:paddingStart="6dp"
+            android:gravity="top"
             android:background="?android:attr/selectableItemBackground"
             android:visibility="gone" />
     </LinearLayout>
diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml
index 456391d..c75a89f 100644
--- a/packages/SystemUI/res/values-land/dimens.xml
+++ b/packages/SystemUI/res/values-land/dimens.xml
@@ -24,5 +24,5 @@
     <integer name="notification_panel_layout_gravity">@integer/standard_notification_panel_layout_gravity</integer>
 
     <dimen name="docked_divider_handle_width">2dp</dimen>
-    <dimen name="docked_divider_handle_height">24dp</dimen>
+    <dimen name="docked_divider_handle_height">16dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values-land/styles.xml b/packages/SystemUI/res/values-land/styles.xml
index 96d8fb8..b711faa 100644
--- a/packages/SystemUI/res/values-land/styles.xml
+++ b/packages/SystemUI/res/values-land/styles.xml
@@ -20,7 +20,7 @@
     </style>
 
     <style name="DockedDividerBackground">
-        <item name="android:layout_width">12dp</item>
+        <item name="android:layout_width">10dp</item>
         <item name="android:layout_height">match_parent</item>
         <item name="android:layout_gravity">center_horizontal</item>
     </style>
@@ -28,7 +28,7 @@
     <style name="DockedDividerHandle">
         <item name="android:layout_gravity">center_vertical</item>
         <item name="android:layout_width">48dp</item>
-        <item name="android:layout_height">64dp</item>
+        <item name="android:layout_height">96dp</item>
     </style>
 
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 24cc6bf..035f564 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -583,9 +583,9 @@
     <dimen name="qs_header_neg_padding">-8dp</dimen>
 
     <!-- How high we lift the divider when touching -->
-    <dimen name="docked_stack_divider_lift_elevation">6dp</dimen>
+    <dimen name="docked_stack_divider_lift_elevation">4dp</dimen>
 
-    <dimen name="docked_divider_handle_width">24dp</dimen>
+    <dimen name="docked_divider_handle_width">16dp</dimen>
     <dimen name="docked_divider_handle_height">2dp</dimen>
 
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index a9176e0..3b0ab791 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -299,13 +299,13 @@
 
     <style name="DockedDividerBackground">
         <item name="android:layout_width">match_parent</item>
-        <item name="android:layout_height">12dp</item>
+        <item name="android:layout_height">10dp</item>
         <item name="android:layout_gravity">center_vertical</item>
     </style>
 
     <style name="DockedDividerHandle">
         <item name="android:layout_gravity">center_horizontal</item>
-        <item name="android:layout_width">64dp</item>
+        <item name="android:layout_width">96dp</item>
         <item name="android:layout_height">48dp</item>
     </style>
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
index d931856..481b9180 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardUI.java
@@ -70,17 +70,19 @@
     // time for us to receive the signal that it's starting.
     private static final long BLUETOOTH_START_DELAY_MILLIS = 10 * 1000;
 
+    // We will be scanning up to 30 seconds, after which we'll stop.
+    private static final long BLUETOOTH_SCAN_TIMEOUT_MILLIS = 30 * 1000;
+
     private static final int STATE_NOT_ENABLED = -1;
     private static final int STATE_UNKNOWN = 0;
     private static final int STATE_WAITING_FOR_BOOT_COMPLETED = 1;
     private static final int STATE_WAITING_FOR_TABLET_MODE_EXIT = 2;
     private static final int STATE_WAITING_FOR_DEVICE_DISCOVERY = 3;
     private static final int STATE_WAITING_FOR_BLUETOOTH = 4;
-    private static final int STATE_WAITING_FOR_STATE_PAIRED = 5;
-    private static final int STATE_PAIRING = 6;
-    private static final int STATE_PAIRED = 7;
-    private static final int STATE_USER_CANCELLED = 8;
-    private static final int STATE_DEVICE_NOT_FOUND = 9;
+    private static final int STATE_PAIRING = 5;
+    private static final int STATE_PAIRED = 6;
+    private static final int STATE_USER_CANCELLED = 7;
+    private static final int STATE_DEVICE_NOT_FOUND = 8;
 
     private static final int MSG_INIT = 0;
     private static final int MSG_ON_BOOT_COMPLETED = 1;
@@ -92,6 +94,7 @@
     private static final int MSG_ON_BLE_SCAN_FAILED = 7;
     private static final int MSG_SHOW_BLUETOOTH_DIALOG = 8;
     private static final int MSG_DISMISS_BLUETOOTH_DIALOG = 9;
+    private static final int MSG_BLE_ABORT_SCAN = 10;
 
     private volatile KeyboardHandler mHandler;
     private volatile KeyboardUIHandler mUIHandler;
@@ -107,6 +110,7 @@
     private long mBootCompletedTime;
 
     private int mInTabletMode = InputManager.SWITCH_STATE_UNKNOWN;
+    private int mScanAttempt = 0;
     private ScanCallback mScanCallback;
     private BluetoothDialog mDialog;
 
@@ -328,6 +332,9 @@
             .build();
         mScanCallback = new KeyboardScanCallback();
         scanner.startScan(Arrays.asList(filter), settings, mScanCallback);
+
+        Message abortMsg = mHandler.obtainMessage(MSG_BLE_ABORT_SCAN, ++mScanAttempt, 0);
+        mHandler.sendMessageDelayed(abortMsg, BLUETOOTH_SCAN_TIMEOUT_MILLIS);
     }
 
     private void stopScanning() {
@@ -338,6 +345,19 @@
     }
 
     // Should only be called on the handler thread
+    private void bleAbortScanInternal(int scanAttempt) {
+        if (mState == STATE_WAITING_FOR_DEVICE_DISCOVERY && scanAttempt == mScanAttempt) {
+            if (DEBUG) {
+                Slog.d(TAG, "Bluetooth scan timed out");
+            }
+            stopScanning();
+            // FIXME: should we also try shutting off bluetooth if we enabled
+            // it in the first place?
+            mState = STATE_DEVICE_NOT_FOUND;
+        }
+    }
+
+    // Should only be called on the handler thread
     private void onDeviceAddedInternal(CachedBluetoothDevice d) {
         if (mState == STATE_WAITING_FOR_DEVICE_DISCOVERY && d.getName().equals(mKeyboardName)) {
             stopScanning();
@@ -425,6 +445,12 @@
                     } else {
                         mState = STATE_USER_CANCELLED;
                     }
+                    break;
+                }
+                case MSG_BLE_ABORT_SCAN: {
+                    int scanAttempt = msg.arg1;
+                    bleAbortScanInternal(scanAttempt);
+                    break;
                 }
                 case MSG_ON_BLUETOOTH_STATE_CHANGED: {
                     int bluetoothState = msg.arg1;
@@ -555,8 +581,6 @@
                 return "STATE_WAITING_FOR_DEVICE_DISCOVERY";
             case STATE_WAITING_FOR_BLUETOOTH:
                 return "STATE_WAITING_FOR_BLUETOOTH";
-            case STATE_WAITING_FOR_STATE_PAIRED:
-                return "STATE_WAITING_FOR_STATE_PAIRED";
             case STATE_PAIRING:
                 return "STATE_PAIRING";
             case STATE_PAIRED:
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerSnapAlgorithm.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerSnapAlgorithm.java
index 69e90cc..5af172c 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerSnapAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerSnapAlgorithm.java
@@ -98,10 +98,10 @@
 
         // TODO: Better calculation
         targets.add(new SnapTarget(-mDividerSize, SnapTarget.FLAG_DISMISS_START));
-        targets.add(new SnapTarget((int) (0.38f * dividerMax) - mDividerSize / 2,
+        targets.add(new SnapTarget((int) (0.3415f * dividerMax) - mDividerSize / 2,
                 SnapTarget.FLAG_NONE));
         targets.add(new SnapTarget(dividerMax / 2 - mDividerSize / 2, SnapTarget.FLAG_NONE));
-        targets.add(new SnapTarget((int) (0.62f * dividerMax) - mDividerSize / 2,
+        targets.add(new SnapTarget((int) (0.6585f * dividerMax) - mDividerSize / 2,
                 SnapTarget.FLAG_NONE));
         targets.add(new SnapTarget(dividerMax, SnapTarget.FLAG_DISMISS_END));
         return targets;
diff --git a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
index c01f170..13642eb 100644
--- a/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
+++ b/packages/SystemUI/src/com/android/systemui/stackdivider/DividerView.java
@@ -240,9 +240,9 @@
 
     private void liftBackground() {
         if (isHorizontalDivision()) {
-            mBackground.animate().scaleY(1.5f);
+            mBackground.animate().scaleY(1.4f);
         } else {
-            mBackground.animate().scaleX(1.5f);
+            mBackground.animate().scaleX(1.4f);
         }
         mBackground.animate()
                 .setInterpolator(mTouchResponseInterpolator)
diff --git a/services/core/java/com/android/server/MountService.java b/services/core/java/com/android/server/MountService.java
index 1d6be3b..6cccf38 100644
--- a/services/core/java/com/android/server/MountService.java
+++ b/services/core/java/com/android/server/MountService.java
@@ -2813,17 +2813,32 @@
     }
 
     @Override
-    public ParcelFileDescriptor mountAppFuse(String name) throws RemoteException {
+    public ParcelFileDescriptor mountAppFuse(final String name) throws RemoteException {
         try {
+            final int uid = Binder.getCallingUid();
             final NativeDaemonEvent event =
-                    mConnector.execute("appfuse", "mount", Binder.getCallingUid(), name);
+                    mConnector.execute("appfuse", "mount", uid, name);
             if (event.getFileDescriptors() == null) {
-                Log.e(TAG, "AppFuse FD from vold is null.");
-                return null;
+                throw new RemoteException("AppFuse FD from vold is null.");
             }
-            return new ParcelFileDescriptor(event.getFileDescriptors()[0]);
+            return ParcelFileDescriptor.fromFd(
+                    event.getFileDescriptors()[0],
+                    mHandler,
+                    new ParcelFileDescriptor.OnCloseListener() {
+                        @Override
+                        public void onClose(IOException e) {
+                            try {
+                                final NativeDaemonEvent event = mConnector.execute(
+                                        "appfuse", "unmount", uid, name);
+                            } catch (NativeDaemonConnectorException unmountException) {
+                                Log.e(TAG, "Failed to unmount appfuse.");
+                            }
+                        }
+                    });
         } catch (NativeDaemonConnectorException e) {
             throw e.rethrowAsParcelableException();
+        } catch (IOException e) {
+            throw new RemoteException(e.getMessage());
         }
     }
 
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index ca5212a..aa99442 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -37,7 +37,9 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
+import android.app.admin.DeviceAdminInfo;
 import android.app.admin.DevicePolicyManager;
+import android.app.admin.DevicePolicyManagerInternal;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentValues;
@@ -82,6 +84,7 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.FgThread;
+import com.android.server.LocalServices;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Sets;
 
@@ -830,7 +833,8 @@
             throw new SecurityException(msg);
         }
 
-        if (!canUserModifyAccounts(userId) || !canUserModifyAccountsForType(userId, account.type)) {
+        if (!canUserModifyAccounts(userId, callingUid) ||
+                !canUserModifyAccountsForType(userId, account.type, callingUid)) {
             return false;
         }
 
@@ -1259,7 +1263,7 @@
                     account.type);
             throw new SecurityException(msg);
         }
-        if (!canUserModifyAccounts(userId)) {
+        if (!canUserModifyAccounts(userId, callingUid)) {
             try {
                 response.onError(AccountManager.ERROR_CODE_USER_RESTRICTED,
                         "User cannot modify accounts");
@@ -1267,7 +1271,7 @@
             }
             return;
         }
-        if (!canUserModifyAccountsForType(userId, account.type)) {
+        if (!canUserModifyAccountsForType(userId, account.type, callingUid)) {
             try {
                 response.onError(AccountManager.ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE,
                         "User cannot modify accounts of this type (policy).");
@@ -1321,9 +1325,6 @@
             throw new SecurityException(msg);
         }
         UserAccounts accounts = getUserAccountsForCaller();
-        if (!canUserModifyAccounts(userId) || !canUserModifyAccountsForType(userId, account.type)) {
-            return false;
-        }
         logRecord(accounts, DebugDbHelper.ACTION_CALLED_ACCOUNT_REMOVE, TABLE_ACCOUNTS);
         long identityToken = clearCallingIdentity();
         try {
@@ -2146,8 +2147,9 @@
         if (accountType == null) throw new IllegalArgumentException("accountType is null");
 
         // Is user disallowed from modifying accounts?
-        int userId = Binder.getCallingUserHandle().getIdentifier();
-        if (!canUserModifyAccounts(userId)) {
+        final int uid = Binder.getCallingUid();
+        final int userId = UserHandle.getUserId(uid);
+        if (!canUserModifyAccounts(userId, uid)) {
             try {
                 response.onError(AccountManager.ERROR_CODE_USER_RESTRICTED,
                         "User is not allowed to add an account!");
@@ -2156,7 +2158,7 @@
             showCantAddAccount(AccountManager.ERROR_CODE_USER_RESTRICTED, userId);
             return;
         }
-        if (!canUserModifyAccountsForType(userId, accountType)) {
+        if (!canUserModifyAccountsForType(userId, accountType, uid)) {
             try {
                 response.onError(AccountManager.ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE,
                         "User cannot modify accounts of this type (policy).");
@@ -2168,7 +2170,6 @@
         }
 
         final int pid = Binder.getCallingPid();
-        final int uid = Binder.getCallingUid();
         final Bundle options = (optionsIn == null) ? new Bundle() : optionsIn;
         options.putInt(AccountManager.KEY_CALLER_UID, uid);
         options.putInt(AccountManager.KEY_CALLER_PID, pid);
@@ -2230,7 +2231,7 @@
         }
 
         // Is user disallowed from modifying accounts?
-        if (!canUserModifyAccounts(userId)) {
+        if (!canUserModifyAccounts(userId, callingUid)) {
             try {
                 response.onError(AccountManager.ERROR_CODE_USER_RESTRICTED,
                         "User is not allowed to add an account!");
@@ -2239,7 +2240,7 @@
             showCantAddAccount(AccountManager.ERROR_CODE_USER_RESTRICTED, userId);
             return;
         }
-        if (!canUserModifyAccountsForType(userId, accountType)) {
+        if (!canUserModifyAccountsForType(userId, accountType, callingUid)) {
             try {
                 response.onError(AccountManager.ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE,
                         "User cannot modify accounts of this type (policy).");
@@ -2310,8 +2311,9 @@
             throw new IllegalArgumentException("accountType is null");
         }
 
-        int userId = Binder.getCallingUserHandle().getIdentifier();
-        if (!canUserModifyAccounts(userId)) {
+        final int uid = Binder.getCallingUid();
+        final int userId = UserHandle.getUserId(uid);
+        if (!canUserModifyAccounts(userId, uid)) {
             try {
                 response.onError(AccountManager.ERROR_CODE_USER_RESTRICTED,
                         "User is not allowed to add an account!");
@@ -2320,7 +2322,7 @@
             showCantAddAccount(AccountManager.ERROR_CODE_USER_RESTRICTED, userId);
             return;
         }
-        if (!canUserModifyAccountsForType(userId, accountType)) {
+        if (!canUserModifyAccountsForType(userId, accountType, uid)) {
             try {
                 response.onError(AccountManager.ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE,
                         "User cannot modify accounts of this type (policy).");
@@ -2332,7 +2334,6 @@
         }
 
         final int pid = Binder.getCallingPid();
-        final int uid = Binder.getCallingUid();
         final Bundle options = (optionsIn == null) ? new Bundle() : optionsIn;
         options.putInt(AccountManager.KEY_CALLER_UID, uid);
         options.putInt(AccountManager.KEY_CALLER_PID, pid);
@@ -2497,8 +2498,9 @@
             throw new IllegalArgumentException("sessionBundle is empty");
         }
 
-        int userId = Binder.getCallingUserHandle().getIdentifier();
-        if (!canUserModifyAccounts(userId)) {
+        final int uid = Binder.getCallingUid();
+        final int userId = UserHandle.getUserId(uid);
+        if (!canUserModifyAccounts(userId, uid)) {
             sendErrorResponse(response,
                     AccountManager.ERROR_CODE_USER_RESTRICTED,
                     "User is not allowed to add an account!");
@@ -2507,7 +2509,6 @@
         }
 
         final int pid = Binder.getCallingPid();
-        final int uid = Binder.getCallingUid();
         final Bundle decryptedBundle;
         final String accountType;
         // First decrypt session bundle to get account type for checking permission.
@@ -2554,7 +2555,7 @@
             return;
         }
 
-        if (!canUserModifyAccountsForType(userId, accountType)) {
+        if (!canUserModifyAccountsForType(userId, accountType, uid)) {
             sendErrorResponse(
                     response,
                     AccountManager.ERROR_CODE_MANAGEMENT_DISABLED_FOR_ACCOUNT_TYPE,
@@ -4319,7 +4320,11 @@
         }
     }
 
-    private boolean canUserModifyAccounts(int userId) {
+    private boolean canUserModifyAccounts(int userId, int callingUid) {
+        // the managing app can always modify accounts
+        if (isProfileOwner(callingUid)) {
+            return true;
+        }
         if (getUserManager().getUserRestrictions(new UserHandle(userId))
                 .getBoolean(UserManager.DISALLOW_MODIFY_ACCOUNTS)) {
             return false;
@@ -4327,7 +4332,11 @@
         return true;
     }
 
-    private boolean canUserModifyAccountsForType(int userId, String accountType) {
+    private boolean canUserModifyAccountsForType(int userId, String accountType, int callingUid) {
+        // the managing app can always modify accounts
+        if (isProfileOwner(callingUid)) {
+            return true;
+        }
         DevicePolicyManager dpm = (DevicePolicyManager) mContext
                 .getSystemService(Context.DEVICE_POLICY_SERVICE);
         String[] typesArray = dpm.getAccountTypesWithManagementDisabledAsUser(userId);
@@ -4342,6 +4351,13 @@
         return true;
     }
 
+    private boolean isProfileOwner(int uid) {
+        final DevicePolicyManagerInternal dpmi =
+                LocalServices.getService(DevicePolicyManagerInternal.class);
+        return (dpmi != null)
+                && dpmi.isActiveAdminWithPolicy(uid, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+    }
+
     @Override
     public void updateAppPermission(Account account, String authTokenType, int uid, boolean value)
             throws RemoteException {
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 43d4e77..20b1e60 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -432,6 +432,10 @@
             synchronized (mService) {
                 mService.mStackSupervisor.removeUserLocked(userId);
             }
+            // Remove the user if it is ephemeral.
+            if (getUserInfo(userId).isEphemeral()) {
+                mUserManager.removeUser(userId);
+            }
         }
     }
 
@@ -478,9 +482,9 @@
     }
 
     /**
-     * Stops the guest user if it has gone to the background.
+     * Stops the guest or ephemeral user if it has gone to the background.
      */
-    private void stopGuestUserIfBackground() {
+    private void stopGuestOrEphemeralUserIfBackground() {
         synchronized (mService) {
             final int num = mUserLru.size();
             for (int i = 0; i < num; i++) {
@@ -492,7 +496,7 @@
                     continue;
                 }
                 UserInfo userInfo = getUserInfo(oldUserId);
-                if (userInfo.isGuest()) {
+                if (userInfo.isGuest() || userInfo.isEphemeral()) {
                     // This is a user to be stopped.
                     stopUsersLocked(oldUserId, true, null);
                     break;
@@ -918,7 +922,7 @@
             mHandler.sendMessage(mHandler.obtainMessage(REPORT_USER_SWITCH_COMPLETE_MSG,
                     newUserId, 0));
         }
-        stopGuestUserIfBackground();
+        stopGuestOrEphemeralUserIfBackground();
         stopBackgroundUsersIfEnforced(oldUserId);
     }
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index c6df83a..49aa73e 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2697,8 +2697,8 @@
                 visibilities[i] = r.getPackageVisibilityOverride();
                 mRankingHelper.extractSignals(r);
             }
+            mRankingHelper.sort(mNotificationList);
             for (int i = 0; i < N; i++) {
-                mRankingHelper.sort(mNotificationList);
                 final NotificationRecord r = mNotificationList.get(i);
                 if (!orderBefore.get(i).equals(r.getKey())
                         || visibilities[i] != r.getPackageVisibilityOverride()) {
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 8a16850..6fcf1d6 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1594,10 +1594,8 @@
 
         mScreenshotChordEnabled = mContext.getResources().getBoolean(
                 com.android.internal.R.bool.config_enableScreenshotChord);
-        // TODO(b/26050571): This can be only reenabled, if there are measure to prevent the alert
-        // windows from being fullscreen. Please consult the bug before enabling.
-        mForceWindowDrawsStatusBarBackground = false; // mContext.getResources().getBoolean(
-                //R.bool.config_forceWindowDrawsStatusBarBackground);
+        mForceWindowDrawsStatusBarBackground = mContext.getResources().getBoolean(
+                R.bool.config_forceWindowDrawsStatusBarBackground);
 
         mGlobalKeyManager = new GlobalKeyManager(mContext);
 
@@ -2065,7 +2063,8 @@
                 attrs.subtreeSystemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
             }
             if ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
-                    || mForceWindowDrawsStatusBarBackground) {
+                    || (mForceWindowDrawsStatusBarBackground
+                            && attrs.height == MATCH_PARENT && attrs.width == MATCH_PARENT)) {
                 attrs.subtreeSystemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
             }
         }
diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
index 9bf7ae4..578428b 100644
--- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java
+++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
@@ -39,6 +39,7 @@
 import android.media.AudioPatch;
 import android.media.AudioPort;
 import android.media.AudioPortConfig;
+import android.media.AudioSystem;
 import android.media.tv.ITvInputHardware;
 import android.media.tv.ITvInputHardwareCallback;
 import android.media.tv.TvInputHardwareInfo;
@@ -703,7 +704,8 @@
             }
             int sinkDevice = mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC);
             for (AudioDevicePort port : devicePorts) {
-                if ((port.type() & sinkDevice) != 0) {
+                if ((port.type() & sinkDevice) != 0 &&
+                    (port.type() & AudioSystem.DEVICE_BIT_IN) == 0) {
                     sinks.add(port);
                 }
             }
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 7605af0..01e41f4 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -705,14 +705,12 @@
         }
 
         // Something is wrong and SurfaceFlinger will not like this, try to revert to sane values.
+        // This doesn't necessarily mean that there is an error in the system. The sizes might be
+        // incorrect, because it is before the first layout or draw.
         if (mTmpSize.width() < 1) {
-            if (!mWin.mLayoutNeeded) Slog.w(TAG,
-                    "Width of " + w + " is not positive " + mTmpSize.width());
             mTmpSize.right = mTmpSize.left + 1;
         }
         if (mTmpSize.height() < 1) {
-            if (!mWin.mLayoutNeeded) Slog.w(TAG,
-                    "Height of " + w + " is not positive " + mTmpSize.height());
             mTmpSize.bottom = mTmpSize.top + 1;
         }