Merge "Clean up sharing virtual files." into nyc-andromeda-dev
diff --git a/res/layout/fragment_directory.xml b/res/layout/fragment_directory.xml
index 22bcbe3..7652ed9 100644
--- a/res/layout/fragment_directory.xml
+++ b/res/layout/fragment_directory.xml
@@ -31,14 +31,6 @@
         style="@style/TrimmedHorizontalProgressBar"
         android:visibility="gone"/>
 
-    <FrameLayout
-        android:id="@+id/container_message_bar"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:elevation="1dp"
-        android:background="@color/material_grey_50"
-        android:visibility="gone" />
-
     <com.android.documentsui.dirlist.DocumentsSwipeRefreshLayout
         android:id="@+id/refresh_layout"
         android:layout_width="match_parent"
diff --git a/res/layout/fragment_message_bar.xml b/res/layout/item_doc_header_message.xml
similarity index 65%
rename from res/layout/fragment_message_bar.xml
rename to res/layout/item_doc_header_message.xml
index 47e4e77..ffb8110 100644
--- a/res/layout/fragment_message_bar.xml
+++ b/res/layout/item_doc_header_message.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2016 The Android Open Source Project
 
      Licensed under the Apache License, Version 2.0 (the "License");
      you may not use this file except in compliance with the License.
@@ -25,11 +25,10 @@
     android:paddingTop="@dimen/list_item_padding">
 
     <LinearLayout
-        android:id="@+id/container_info"
+        android:id="@+id/message_container"
         android:layout_height="wrap_content"
         android:layout_width="wrap_content"
-        android:orientation="horizontal"
-        android:visibility="gone">
+        android:orientation="horizontal">
 
         <FrameLayout
             android:layout_height="@dimen/icon_size"
@@ -37,7 +36,7 @@
 
             <ImageView
                 android:contentDescription="@null"
-                android:id="@+id/icon_info"
+                android:id="@+id/message_icon"
                 android:layout_height="match_parent"
                 android:layout_width="wrap_content"
                 android:scaleType="centerInside"/>
@@ -45,36 +44,7 @@
         </FrameLayout>
 
         <TextView
-            android:id="@+id/textview_info"
-            android:layout_gravity="center_vertical"
-            android:layout_height="wrap_content"
-            android:layout_width="match_parent"
-            android:selectAllOnFocus="true"/>
-
-    </LinearLayout>
-
-    <LinearLayout
-        android:id="@+id/container_error"
-        android:layout_height="wrap_content"
-        android:layout_width="wrap_content"
-        android:orientation="horizontal"
-        android:visibility="gone">
-
-        <FrameLayout
-            android:layout_height="@dimen/icon_size"
-            android:layout_width="@dimen/icon_size">
-
-            <ImageView
-                android:contentDescription="@null"
-                android:id="@+id/icon_error"
-                android:layout_height="match_parent"
-                android:layout_width="wrap_content"
-                android:scaleType="centerInside"/>
-
-        </FrameLayout>
-
-        <TextView
-            android:id="@+id/textview_error"
+            android:id="@+id/message_textview"
             android:layout_gravity="center_vertical"
             android:layout_height="wrap_content"
             android:layout_width="match_parent"
diff --git a/res/layout/item_doc_inflated_message.xml b/res/layout/item_doc_inflated_message.xml
new file mode 100644
index 0000000..71354e6
--- /dev/null
+++ b/res/layout/item_doc_inflated_message.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/empty"
+    android:gravity="center"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:background="@color/directory_background"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:clickable="true">
+
+    <LinearLayout
+        android:id="@+id/content"
+        android:gravity="center"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/artwork"
+            android:adjustViewBounds="true"
+            android:layout_height="250dp"
+            android:layout_width="fill_parent"
+            android:alpha="1"
+            android:layout_centerVertical="true"
+            android:layout_marginBottom="25dp"
+            android:scaleType="fitCenter"
+            android:contentDescription="@null"/>
+
+        <TextView
+            android:id="@+id/message"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            style="@android:style/TextAppearance.Material.Subhead"/>
+
+    </LinearLayout>
+</FrameLayout>
diff --git a/res/layout/single_pane_layout.xml b/res/layout/single_pane_layout.xml
deleted file mode 100644
index 7b7e229..0000000
--- a/res/layout/single_pane_layout.xml
+++ /dev/null
@@ -1,57 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2013 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-
-<!-- CoordinatorLayout is necessary for various components (e.g. Snackbars, and
-     floating action buttons) to operate correctly. -->
-<!-- focusableInTouchMode is set in order to force key events to go to the activity's global key 
-     callback, which is necessary for proper event routing. See BaseActivity.onKeyDown. -->
-<android.support.design.widget.CoordinatorLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:id="@+id/coordinator_layout"
-    android:focusableInTouchMode="true">
-
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="vertical">
-
-        <com.android.documentsui.DocumentsToolbar
-            android:id="@+id/toolbar"
-            android:layout_width="match_parent"
-            android:layout_height="?android:attr/actionBarSize"
-            android:background="?android:attr/colorPrimary"
-            android:elevation="8dp"
-            android:theme="?actionBarTheme"
-            android:popupTheme="?actionBarPopupTheme">
-
-            <Spinner
-                android:id="@+id/stack"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:popupTheme="?actionBarPopupTheme"
-                android:background="@android:color/transparent"
-                android:layout_marginStart="4dp"
-                android:overlapAnchor="true" />
-
-        </com.android.documentsui.DocumentsToolbar>
-
-        <include layout="@layout/directory_cluster"/>
-
-    </LinearLayout>
-
-</android.support.design.widget.CoordinatorLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 109363b..6daab3d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -265,4 +265,7 @@
     <string name="documents_shortcut_label">Documents</string>
     <!-- Shortcut label of Downloads root folder -->
     <string name="downloads_shortcut_label">Downloads</string>
+
+    <!-- Error message shown when an archive fails to load -->
+    <string name="archive_loading_failed">Unable to open archive for browsing. File is either corrupt, or an unsupported format.</string>
 </resources>
diff --git a/src/com/android/documentsui/MessageBar.java b/src/com/android/documentsui/MessageBar.java
deleted file mode 100644
index 5c6213f..0000000
--- a/src/com/android/documentsui/MessageBar.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- * 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;
-
-import android.annotation.Nullable;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-/**
- * A message bar displaying some info/error messages and a Dismiss button.
- */
-public class MessageBar extends Fragment {
-    private View mView;
-    private ViewGroup mContainer;
-
-    /**
-     * Creates an instance of a MessageBar. Note that the new MessagBar is not visible by default,
-     * and has to be shown by calling MessageBar.show.
-     */
-    public static MessageBar create(FragmentManager fm) {
-        final MessageBar fragment = new MessageBar();
-
-        final FragmentTransaction ft = fm.beginTransaction();
-        ft.replace(R.id.container_message_bar, fragment);
-        ft.commitAllowingStateLoss();
-
-        return fragment;
-    }
-
-    /**
-     * Sets the info message. Can be null, in which case no info message will be displayed. The
-     * message bar layout will be adjusted accordingly.
-     */
-    public void setInfo(@Nullable String info) {
-        View infoContainer = mView.findViewById(R.id.container_info);
-        if (info != null) {
-            TextView infoText = (TextView) mView.findViewById(R.id.textview_info);
-            infoText.setText(info);
-            infoContainer.setVisibility(View.VISIBLE);
-        } else {
-            infoContainer.setVisibility(View.GONE);
-        }
-    }
-
-    /**
-     * Sets the error message. Can be null, in which case no error message will be displayed. The
-     * message bar layout will be adjusted accordingly.
-     */
-    public void setError(@Nullable String error) {
-        View errorView = mView.findViewById(R.id.container_error);
-        if (error != null) {
-            TextView errorText = (TextView) mView.findViewById(R.id.textview_error);
-            errorText.setText(error);
-            errorView.setVisibility(View.VISIBLE);
-        } else {
-            errorView.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    public View onCreateView(
-            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-
-        mView = inflater.inflate(R.layout.fragment_message_bar, container, false);
-
-        ImageView infoIcon = (ImageView) mView.findViewById(R.id.icon_info);
-        infoIcon.setImageResource(R.drawable.ic_dialog_info);
-
-        ImageView errorIcon = (ImageView) mView.findViewById(R.id.icon_error);
-        errorIcon.setImageResource(R.drawable.ic_dialog_alert);
-
-        Button dismiss = (Button) mView.findViewById(R.id.button_dismiss);
-        dismiss.setOnClickListener(
-                new View.OnClickListener() {
-                    @Override
-                    public void onClick(View v) {
-                        hide();
-                    }
-                });
-
-        mContainer = container;
-
-        return mView;
-    }
-
-    public void hide() {
-        // The container view is used to show/hide the error bar. If a container is not provided,
-        // fall back to showing/hiding the error bar View, which also works, but does not provide
-        // the same animated transition.
-        if (mContainer != null) {
-            mContainer.setVisibility(View.GONE);
-        } else {
-            mView.setVisibility(View.GONE);
-        }
-    }
-
-    public void show() {
-        // The container view is used to show/hide the error bar. If a container is not provided,
-        // fall back to showing/hiding the error bar View, which also works, but does not provide
-        // the same animated transition.
-        if (mContainer != null) {
-            mContainer.setVisibility(View.VISIBLE);
-        } else {
-            mView.setVisibility(View.VISIBLE);
-        }
-    }
-}
diff --git a/src/com/android/documentsui/archives/ArchivesProvider.java b/src/com/android/documentsui/archives/ArchivesProvider.java
index fb96337..8d5552b 100644
--- a/src/com/android/documentsui/archives/ArchivesProvider.java
+++ b/src/com/android/documentsui/archives/ArchivesProvider.java
@@ -35,6 +35,7 @@
 import android.util.Log;
 import android.util.LruCache;
 
+import com.android.documentsui.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
 
@@ -70,8 +71,6 @@
                     oldValue.getWriteLock().lock();
                     try {
                         oldValue.get().close();
-                    } catch (FileNotFoundException e) {
-                        Log.e(TAG, "Failed to close an archive as it no longer exists.");
                     } finally {
                         oldValue.getWriteLock().unlock();
                     }
@@ -96,20 +95,34 @@
         Loader loader = null;
         try {
             loader = obtainInstance(documentId);
-            if (loader.mArchive == null) {
-                final MatrixCursor cursor = new MatrixCursor(
-                        projection != null ? projection : Archive.DEFAULT_PROJECTION);
-                // Return an empty cursor with EXTRA_LOADING, which shows spinner
-                // in DocumentsUI. Once the archive is loaded, the notification will
-                // be sent, and the directory reloaded.
-                final Bundle bundle = new Bundle();
-                bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true);
-                cursor.setExtras(bundle);
-                cursor.setNotificationUri(getContext().getContentResolver(),
-                        buildUriForArchive(archiveId.mArchiveUri));
-                return cursor;
+            final int status = loader.getStatus();
+            // If already loaded, then forward the request to the archive.
+            if (status == Loader.STATUS_OPENED) {
+                return loader.get().queryChildDocuments(documentId, projection, sortOrder);
             }
-            return loader.get().queryChildDocuments(documentId, projection, sortOrder);
+
+            final MatrixCursor cursor = new MatrixCursor(
+                    projection != null ? projection : Archive.DEFAULT_PROJECTION);
+            final Bundle bundle = new Bundle();
+
+            switch (status) {
+                case Loader.STATUS_OPENING:
+                    bundle.putBoolean(DocumentsContract.EXTRA_LOADING, true);
+                    break;
+
+                case Loader.STATUS_FAILED:
+                    // Return an empty cursor with EXTRA_LOADING, which shows spinner
+                    // in DocumentsUI. Once the archive is loaded, the notification will
+                    // be sent, and the directory reloaded.
+                    bundle.putString(DocumentsContract.EXTRA_ERROR,
+                            getContext().getString(R.string.archive_loading_failed));
+                    break;
+            }
+
+            cursor.setExtras(bundle);
+            cursor.setNotificationUri(getContext().getContentResolver(),
+                    buildUriForArchive(archiveId.mArchiveUri));
+            return cursor;
         } finally {
             releaseInstance(loader);
         }
@@ -259,6 +272,10 @@
 
         final Cursor cursor = getContext().getContentResolver().query(
                 id.mArchiveUri, new String[] { Document.COLUMN_MIME_TYPE }, null, null, null);
+        if (cursor == null || cursor.getCount() == 0) {
+            throw new FileNotFoundException("File not found." + id.mArchiveUri);
+        }
+
         cursor.moveToFirst();
         final String mimeType = cursor.getString(cursor.getColumnIndex(
                 Document.COLUMN_MIME_TYPE));
diff --git a/src/com/android/documentsui/archives/Loader.java b/src/com/android/documentsui/archives/Loader.java
index 1acdc15..2e03c39 100644
--- a/src/com/android/documentsui/archives/Loader.java
+++ b/src/com/android/documentsui/archives/Loader.java
@@ -16,8 +16,11 @@
 
 package com.android.documentsui.archives;
 
+import com.android.internal.annotations.GuardedBy;
+
 import android.content.Context;
 import android.net.Uri;
+import android.util.Log;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -31,13 +34,21 @@
  * Loads an instance of Archive lazily.
  */
 public class Loader {
+    private static final String TAG = "Loader";
+
+    public static final int STATUS_OPENING = 0;
+    public static final int STATUS_OPENED = 1;
+    public static final int STATUS_FAILED = 2;
+
     private final Context mContext;
     private final Uri mArchiveUri;
     private final Uri mNotificationUri;
     private final ReentrantReadWriteLock mLock = new ReentrantReadWriteLock();
     private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
-    private Exception mFailureException = null;
-    public Archive mArchive = null;
+    private final Object mStatusLock = new Object();
+    @GuardedBy("mStatusLock")
+    private int mStatus = STATUS_OPENING;
+    private Archive mArchive = null;
 
     Loader(Context context, Uri archiveUri, Uri notificationUri) {
         this.mContext = context;
@@ -48,18 +59,21 @@
         mExecutor.submit(this::get);
     }
 
-    synchronized Archive get() throws FileNotFoundException {
-        if (mArchive != null) {
-            return mArchive;
+    synchronized Archive get() {
+        synchronized (mStatusLock) {
+            if (mStatus == STATUS_OPENED) {
+                return mArchive;
+            }
         }
 
         // Once loading the archive failed, do not to retry opening it until the
         // archive file has changed (the loader is deleted once we receive
         // a notification about the archive file being changed).
-        if (mFailureException != null) {
-            throw new IllegalStateException(
-                    "Trying to perform an operation on an archive which failed to load.",
-                    mFailureException);
+        synchronized (mStatusLock) {
+            if (mStatus == STATUS_FAILED) {
+                throw new IllegalStateException(
+                        "Trying to perform an operation on an archive which failed to load.");
+            }
         }
 
         try {
@@ -68,12 +82,15 @@
                     mContext.getContentResolver().openFileDescriptor(
                             mArchiveUri, "r", null /* signal */),
                     mArchiveUri, mNotificationUri);
-        } catch (IOException e) {
-            mFailureException = e;
-            throw new IllegalStateException(e);
-        } catch (RuntimeException e) {
-            mFailureException = e;
-            throw e;
+            synchronized (mStatusLock) {
+                mStatus = STATUS_OPENED;
+            }
+        } catch (IOException | RuntimeException e) {
+            Log.e(TAG, "Failed to open the archive.", e);
+            synchronized (mStatusLock) {
+                mStatus = STATUS_FAILED;
+            }
+            throw new IllegalStateException("Failed to open the archive.", e);
         } finally {
             // Notify observers that the root directory is loaded (or failed)
             // so clients reload it.
@@ -81,9 +98,16 @@
                     ArchivesProvider.buildUriForArchive(mArchiveUri),
                     null /* observer */, false /* syncToNetwork */);
         }
+
         return mArchive;
     }
 
+    int getStatus() {
+        synchronized (mStatusLock) {
+            return mStatus;
+        }
+    }
+
     Lock getReadLock() {
         return mLock.readLock();
     }
diff --git a/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java b/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java
new file mode 100644
index 0000000..d99e168
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/DirectoryAddonsAdapter.java
@@ -0,0 +1,300 @@
+/*
+ * 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.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView.AdapterDataObserver;
+import android.view.ViewGroup;
+
+import com.android.documentsui.base.EventListener;
+import com.android.documentsui.dirlist.Message.HeaderMessage;
+import com.android.documentsui.dirlist.Message.InflateMessage;
+import com.android.documentsui.dirlist.Model.Update;
+
+import java.util.List;
+
+/**
+ * Adapter wrapper that embellishes the directory list by inserting Holder views inbetween
+ * items.
+ */
+final class DirectoryAddonsAdapter extends DocumentsAdapter {
+
+    private static final String TAG = "SectioningDocumentsAdapterWrapper";
+
+    private final Environment mEnv;
+    private final DocumentsAdapter mDelegate;
+    private final EventListener<Update> mModelUpdateListener;
+
+    private int mBreakPosition = -1;
+    // TODO: There should be two header messages (or more here). Defaulting to showing only one for
+    // now.
+    private final Message mHeaderMessage;
+    private final Message mInflateMessage;
+
+    DirectoryAddonsAdapter(Environment environment, DocumentsAdapter delegate) {
+        mEnv = environment;
+        mDelegate = delegate;
+        mHeaderMessage = new HeaderMessage(environment);
+        mInflateMessage = new InflateMessage(environment);
+
+        // Relay events published by our delegate to our listeners (presumably RecyclerView)
+        // with adjusted positions.
+        mDelegate.registerAdapterDataObserver(new EventRelay());
+
+        mModelUpdateListener = this::onModelUpdate;
+    }
+
+    @Override
+    EventListener<Update> getModelUpdateListener() {
+        return mModelUpdateListener;
+    }
+
+    @Override
+    public GridLayoutManager.SpanSizeLookup createSpanSizeLookup() {
+        return new GridLayoutManager.SpanSizeLookup() {
+            @Override
+            public int getSpanSize(int position) {
+                // Make layout whitespace span the grid. This has the effect of breaking
+                // grid rows whenever layout whitespace is encountered.
+                if (getItemViewType(position) == ITEM_TYPE_SECTION_BREAK
+                        || getItemViewType(position) == ITEM_TYPE_HEADER_MESSAGE
+                        || getItemViewType(position) == ITEM_TYPE_INFLATED_MESSAGE) {
+                    return mEnv.getColumnCount();
+                } else {
+                    return 1;
+                }
+            }
+        };
+    }
+
+    @Override
+    public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        switch (viewType) {
+            case ITEM_TYPE_SECTION_BREAK:
+                return new TransparentDividerDocumentHolder(mEnv.getContext());
+            case ITEM_TYPE_HEADER_MESSAGE:
+                return new HeaderMessageDocumentHolder(mEnv.getContext(), parent,
+                        this::onDismissHeaderMessage);
+            case ITEM_TYPE_INFLATED_MESSAGE:
+                return new InflateMessageDocumentHolder(mEnv.getContext(), parent);
+            default:
+                return mDelegate.createViewHolder(parent, viewType);
+        }
+    }
+
+    private void onDismissHeaderMessage() {
+        mHeaderMessage.reset();
+        if (mBreakPosition > 0) {
+            mBreakPosition--;
+        }
+        notifyItemRemoved(0);
+    }
+
+    @Override
+    public void onBindViewHolder(DocumentHolder holder, int p, List<Object> payload) {
+        switch (holder.getItemViewType()) {
+            case ITEM_TYPE_SECTION_BREAK:
+                ((TransparentDividerDocumentHolder) holder).bind(mEnv.getDisplayState());
+                break;
+            case ITEM_TYPE_HEADER_MESSAGE:
+                ((HeaderMessageDocumentHolder) holder).bind(mHeaderMessage);
+                break;
+            case ITEM_TYPE_INFLATED_MESSAGE:
+                ((InflateMessageDocumentHolder) holder).bind(mInflateMessage);
+                break;
+            default:
+                mDelegate.onBindViewHolder(holder, toDelegatePosition(p), payload);
+                break;
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(DocumentHolder holder, int p) {
+        switch (holder.getItemViewType()) {
+            case ITEM_TYPE_SECTION_BREAK:
+                ((TransparentDividerDocumentHolder) holder).bind(mEnv.getDisplayState());
+                break;
+            case ITEM_TYPE_HEADER_MESSAGE:
+                ((HeaderMessageDocumentHolder) holder).bind(mHeaderMessage);
+                break;
+            case ITEM_TYPE_INFLATED_MESSAGE:
+                ((InflateMessageDocumentHolder) holder).bind(mInflateMessage);
+                break;
+            default:
+                mDelegate.onBindViewHolder(holder, toDelegatePosition(p));
+                break;
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        int addons = mHeaderMessage.shouldShow() ? 1 : 0;
+        addons += mInflateMessage.shouldShow() ? 1 : 0;
+        return mBreakPosition == -1
+                ? mDelegate.getItemCount() + addons
+                : mDelegate.getItemCount() + addons + 1;
+    }
+
+    private void onModelUpdate(Update event) {
+        // make sure the delegate handles the update before we do.
+        // This isn't ideal since the delegate might be listening
+        // the updates itself. But this is the safe thing to do
+        // since we read model ids from the delegate
+        // in our update handler.
+        mDelegate.getModelUpdateListener().accept(event);
+
+        mBreakPosition = -1;
+        mInflateMessage.update(event);
+        mHeaderMessage.update(event);
+        // If there's any fatal error (exceptions), then no need to update the rest.
+        if (event.hasError()) {
+            return;
+        }
+
+
+        // Walk down the list of IDs till we encounter something that's not a directory, and
+        // insert a whitespace element - this introduces a visual break in the grid between
+        // folders and documents.
+        // TODO: This code makes assumptions about the model, namely, that it performs a
+        // bucketed sort where directories will always be ordered before other files. CBB.
+        Model model = mEnv.getModel();
+        for (int i = 0; i < model.getModelIds().length; i++) {
+            if (!isDirectory(model, i)) {
+                // If the break is the first thing in the list, then there are actually no
+                // directories. In that case, don't insert a break at all.
+                if (i > 0) {
+                    mBreakPosition = i + (mHeaderMessage.shouldShow() ? 1 : 0);
+                }
+                break;
+            }
+        }
+    }
+
+    @Override
+    public int getItemViewType(int p) {
+        if (p == 0 && mHeaderMessage.shouldShow()) {
+            return ITEM_TYPE_HEADER_MESSAGE;
+        }
+
+        if (p == mBreakPosition) {
+            return ITEM_TYPE_SECTION_BREAK;
+        }
+
+        if (p == getItemCount() - 1 && mInflateMessage.shouldShow()) {
+            return ITEM_TYPE_INFLATED_MESSAGE;
+        }
+
+        return mDelegate.getItemViewType(toDelegatePosition(p));
+    }
+
+    /**
+     * Returns the position of an item in the delegate, adjusting
+     * values that are greater than the break position.
+     *
+     * @param p Position within the view
+     * @return Position within the delegate
+     */
+    private int toDelegatePosition(int p) {
+        int topOffset = mHeaderMessage.shouldShow() ? 1 : 0;
+        return (mBreakPosition != -1 && p > mBreakPosition) ? p - 1 - topOffset : p - topOffset;
+    }
+
+    /**
+     * Returns the position of an item in the view, adjusting
+     * values that are greater than the break position.
+     *
+     * @param p Position within the delegate
+     * @return Position within the view
+     */
+    private int toViewPosition(int p) {
+        int topOffset = mHeaderMessage.shouldShow() ? 1 : 0;
+        // Offset it first so we can compare break position correctly
+        p += topOffset;
+        // If position is greater than or equal to the break, increase by one.
+        return (mBreakPosition != -1 && p >= mBreakPosition) ? p + 1 : p;
+    }
+
+    @Override
+    public List<String> getModelIds() {
+        return mDelegate.getModelIds();
+    }
+
+    @Override
+    public String getModelId(int p) {
+        if (p == mBreakPosition) {
+            return null;
+        }
+
+        if (p == 0 && mHeaderMessage.shouldShow()) {
+            return null;
+        }
+
+        if (p == getItemCount() - 1 && mInflateMessage.shouldShow()) {
+            return null;
+        }
+
+        return mDelegate.getModelId(toDelegatePosition(p));
+    }
+
+    @Override
+    public void onItemSelectionChanged(String id) {
+        mDelegate.onItemSelectionChanged(id);
+    }
+
+    // Listener we add to our delegate. This allows us to relay events published
+    // by the delegate to our listeners (presumably RecyclerView) with adjusted positions.
+    private final class EventRelay extends AdapterDataObserver {
+        @Override
+        public void onChanged() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
+            assert(itemCount == 1);
+            notifyItemRangeChanged(toViewPosition(positionStart), itemCount, payload);
+        }
+
+        @Override
+        public void onItemRangeInserted(int positionStart, int itemCount) {
+            assert(itemCount == 1);
+            if (positionStart < mBreakPosition) {
+                mBreakPosition++;
+            }
+            notifyItemRangeInserted(toViewPosition(positionStart), itemCount);
+        }
+
+        @Override
+        public void onItemRangeRemoved(int positionStart, int itemCount) {
+            assert(itemCount == 1);
+            if (positionStart < mBreakPosition) {
+                mBreakPosition--;
+            }
+            notifyItemRangeRemoved(toViewPosition(positionStart), itemCount);
+        }
+
+        @Override
+        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
+            throw new UnsupportedOperationException();
+        }
+    }
+}
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 0efb19c..62382cd 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -80,7 +80,6 @@
 import com.android.documentsui.Injector.ContentScoped;
 import com.android.documentsui.Injector.Injected;
 import com.android.documentsui.ItemDragListener;
-import com.android.documentsui.MessageBar;
 import com.android.documentsui.Metrics;
 import com.android.documentsui.R;
 import com.android.documentsui.RecentsLoader;
@@ -184,7 +183,6 @@
     private @Nullable DragHoverListener mDragHoverListener;
     private IconHelper mIconHelper;
     private SwipeRefreshLayout mRefreshLayout;
-    private View mEmptyView;
     private RecyclerView mRecView;
     private View mFileList;
 
@@ -196,7 +194,6 @@
     private float mLiveScale = 1.0f;
     private @ViewMode int mMode;
 
-    private MessageBar mMessageBar;
     private View mProgressBar;
 
     private DirectoryState mLocalState;
@@ -220,14 +217,12 @@
         BaseActivity activity = (BaseActivity) getActivity();
         final View view = inflater.inflate(R.layout.fragment_directory, container, false);
 
-        mMessageBar = MessageBar.create(getChildFragmentManager());
         mProgressBar = view.findViewById(R.id.progressbar);
         assert(mProgressBar != null);
 
         mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
         mRefreshLayout.setOnRefreshListener(this);
 
-        mEmptyView = view.findViewById(android.R.id.empty);
         mRecView = (RecyclerView) view.findViewById(R.id.dir_list);
         mRecView.setRecyclerListener(
                 new RecyclerListener() {
@@ -246,7 +241,6 @@
 
         // Make the recycler and the empty views responsive to drop events when allowed.
         mRecView.setOnDragListener(mDragHoverListener);
-        mEmptyView.setOnDragListener(mDragHoverListener);
 
         return view;
     }
@@ -291,7 +285,7 @@
         mIconHelper = new IconHelper(mActivity, MODE_GRID);
         mClipper = DocumentsApplication.getDocumentClipper(getContext());
 
-        mAdapter = new SectionBreakDocumentsAdapterWrapper(
+        mAdapter = new DirectoryAddonsAdapter(
                 mAdapterEnv, new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper));
 
         mRecView.setAdapter(mAdapter);
@@ -367,7 +361,6 @@
         new ListeningGestureDetector(
                 this.getContext(),
                 mRecView,
-                mEmptyView,
                 mDragStartListener::onMouseDragEvent,
                 gestureSel,
                 mInputHandler,
@@ -483,9 +476,7 @@
             x = e.getX() - v.getLeft();
             y = e.getY() - v.getTop();
         } else {
-            v = (mEmptyView.getVisibility() == View.VISIBLE)
-                    ? mEmptyView
-                    : mRecView;
+            v = mRecView;
             x = e.getX();
             y = e.getY();
         }
@@ -801,41 +792,6 @@
         return mInjector.config.isDocumentEnabled(mimeType, flags, mState);
     }
 
-    private void showEmptyDirectory() {
-        showEmptyView(R.string.empty, R.drawable.cabinet);
-    }
-
-    private void showNoResults(RootInfo root) {
-        CharSequence msg = getContext().getResources().getText(R.string.no_results);
-        showEmptyView(String.format(String.valueOf(msg), root.title), R.drawable.cabinet);
-    }
-
-    private void showQueryError() {
-        showEmptyView(R.string.query_error, R.drawable.hourglass);
-    }
-
-    private void showEmptyView(@StringRes int id, int drawable) {
-        showEmptyView(getContext().getResources().getText(id), drawable);
-    }
-
-    private void showEmptyView(CharSequence msg, int drawable) {
-        View content = mEmptyView.findViewById(R.id.content);
-        TextView msgView = (TextView) mEmptyView.findViewById(R.id.message);
-        ImageView imageView = (ImageView) mEmptyView.findViewById(R.id.artwork);
-        msgView.setText(msg);
-        imageView.setImageResource(drawable);
-
-        mEmptyView.setVisibility(View.VISIBLE);
-        mEmptyView.requestFocus();
-        mFileList.setVisibility(View.GONE);
-    }
-
-    private void showDirectory() {
-        mEmptyView.setVisibility(View.GONE);
-        mFileList.setVisibility(View.VISIBLE);
-        mRecView.requestFocus();
-    }
-
     public void pasteFromClipboard() {
         Metrics.logUserAction(getContext(), Metrics.USER_ACTION_PASTE_CLIPBOARD);
 
@@ -988,7 +944,7 @@
             return DocumentInfo.fromDirectoryCursor(dstCursor);
         }
 
-        if (v == mRecView || v == mEmptyView) {
+        if (v == mRecView) {
             return mState.stack.peek();
         }
 
@@ -1160,31 +1116,11 @@
 
         @Override
         public void accept(Model.Update update) {
-            if (update.hasError()) {
-                showQueryError();
-                return;
-            }
-
             if (DEBUG) Log.d(TAG, "Received model update. Loading=" + mModel.isLoading());
 
-            if (mModel.info != null || mModel.error != null) {
-                mMessageBar.setInfo(mModel.info);
-                mMessageBar.setError(mModel.error);
-                mMessageBar.show();
-            }
-
             mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE);
 
-            if (mModel.isEmpty()) {
-                if (mLocalState.mSearchMode) {
-                    showNoResults(mState.stack.getRoot());
-                } else {
-                    showEmptyDirectory();
-                }
-            } else {
-                showDirectory();
-                mAdapter.notifyDataSetChanged();
-            }
+            mAdapter.notifyDataSetChanged();
 
             if (!mModel.isLoading()) {
                 mActivity.notifyDirectoryLoaded(
@@ -1206,6 +1142,11 @@
         }
 
         @Override
+        public boolean isInSearchMode() {
+            return mLocalState.mSearchMode;
+        }
+
+        @Override
         public Model getModel() {
             return mModel;
         }
diff --git a/src/com/android/documentsui/dirlist/DocumentsAdapter.java b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
index 04baa50..8d7c63c 100644
--- a/src/com/android/documentsui/dirlist/DocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/DocumentsAdapter.java
@@ -36,17 +36,23 @@
  * dummy layout objects was error prone when interspersed with the core mode / adapter code.
  *
  * @see ModelBackedDocumentsAdapter
- * @see SectionBreakDocumentsAdapterWrapper
+ * @see DirectoryAddonsAdapter
  */
 public abstract class DocumentsAdapter
         extends RecyclerView.Adapter<DocumentHolder> {
+    // Item types used by ModelBackedDocumentsAdapter
+    public static final int ITEM_TYPE_DOCUMENT = 1;
+    public static final int ITEM_TYPE_DIRECTORY = 2;
+    // Item types used by SectionBreakDocumentsAdapterWrapper
+    public static final int ITEM_TYPE_SECTION_BREAK = Integer.MAX_VALUE;
+    public static final int ITEM_TYPE_HEADER_MESSAGE = Integer.MAX_VALUE - 1;
+    public static final int ITEM_TYPE_INFLATED_MESSAGE = Integer.MAX_VALUE - 2;
 
     // Payloads for notifyItemChange to distinguish between selection and other events.
     static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
 
     /**
-     * Returns a list of model IDs of items currently in the adapter. Excludes items that are
-     * currently hidden (see {@link #hide(String...)}).
+     * Returns a list of model IDs of items currently in the adapter.
      *
      * @return A list of Model IDs.
      */
@@ -67,13 +73,17 @@
 
     /**
      * Returns a class that yields the span size for a particular element. This is
-     * primarily useful in {@link SectionBreakDocumentsAdapterWrapper} where
+     * primarily useful in {@link DirectoryAddonsAdapter} where
      * we adjust sizes.
      */
     GridLayoutManager.SpanSizeLookup createSpanSizeLookup() {
         throw new UnsupportedOperationException();
     }
 
+    public boolean hasModelIds() {
+        return !getModelIds().isEmpty();
+    }
+
     static boolean isDirectory(Cursor cursor) {
         final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
         return Document.MIME_TYPE_DIR.equals(mimeType);
@@ -92,6 +102,7 @@
         Context getContext();
         int getColumnCount();
         State getDisplayState();
+        boolean isInSearchMode();
         boolean isSelected(String id);
         Model getModel();
         boolean isDocumentEnabled(String mimeType, int flags);
diff --git a/src/com/android/documentsui/dirlist/HeaderMessageDocumentHolder.java b/src/com/android/documentsui/dirlist/HeaderMessageDocumentHolder.java
new file mode 100644
index 0000000..be2ce4c
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/HeaderMessageDocumentHolder.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.documentsui.R;
+
+/**
+ * RecyclerView.ViewHolder class that displays at the top of the directory list when there
+ * are more information from the Provider.
+ * Used by {@link DirectoryAddonsAdapter}.
+ */
+final class HeaderMessageDocumentHolder extends DocumentHolder {
+    private Message mMessage;
+    private ImageView mIcon;
+    private TextView mTextView;
+
+    public HeaderMessageDocumentHolder(Context context, ViewGroup parent, Runnable callback) {
+        super(context, parent, R.layout.item_doc_header_message);
+
+        mIcon = (ImageView) itemView.findViewById(R.id.message_icon);
+        mTextView = (TextView) itemView.findViewById(R.id.message_textview);
+        Button dismiss = (Button) itemView.findViewById(R.id.button_dismiss);
+        dismiss.setOnClickListener(
+                new View.OnClickListener() {
+                    @Override
+                    public void onClick(View v) {
+                        callback.run();
+                    }
+                });
+    }
+
+    public void bind(Message message) {
+        mMessage = message;
+        bind(null, null);
+    }
+
+    @Override
+    public void bind(Cursor cursor, String modelId) {
+        mTextView.setText(mMessage.getMessageString());
+        mIcon.setImageResource(mMessage.getIconId());
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/dirlist/InflateMessageDocumentHolder.java b/src/com/android/documentsui/dirlist/InflateMessageDocumentHolder.java
new file mode 100644
index 0000000..d0c4ba0
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/InflateMessageDocumentHolder.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.documentsui.R;
+
+/**
+ * RecyclerView.ViewHolder class that displays a message when there are no contents
+ * in the directory, whether due to no items, no search results or an error.
+ * Used by {@link DirectoryAddonsAdapter}.
+ */
+final class InflateMessageDocumentHolder extends DocumentHolder {
+    private Message mMessage;
+    private TextView mMsgView;
+    private ImageView mImageView;
+
+    public InflateMessageDocumentHolder(Context context, ViewGroup parent) {
+        super(context, parent, R.layout.item_doc_inflated_message);
+        mMsgView = (TextView) itemView.findViewById(R.id.message);
+        mImageView = (ImageView) itemView.findViewById(R.id.artwork);
+    }
+
+    public void bind(Message message) {
+        mMessage = message;
+        bind(null, null);
+    }
+
+    @Override
+    public void bind(Cursor cursor, String modelId) {
+        mMsgView.setText(mMessage.getMessageString());
+        mImageView.setImageResource(mMessage.getIconId());
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
index d2f6972..06ad73b 100644
--- a/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
+++ b/src/com/android/documentsui/dirlist/ListeningGestureDetector.java
@@ -43,8 +43,7 @@
 //Receives event meant for both directory and empty view, and either pass them to
 //{@link UserInputHandler} for simple gestures (Single Tap, Long-Press), or intercept them for
 //other types of gestures (drag n' drop)
-final class ListeningGestureDetector extends GestureDetector
-        implements OnItemTouchListener, OnTouchListener {
+final class ListeningGestureDetector extends GestureDetector implements OnItemTouchListener {
 
     private static final String TAG = "ListeningGestureDetector";
 
@@ -60,7 +59,6 @@
     public ListeningGestureDetector(
             Context context,
             RecyclerView recView,
-            View emptyView,
             EventHandler<InputEvent> mouseDragListener,
             GestureSelector gestureSelector,
             UserInputHandler<? extends InputEvent> handler,
@@ -73,7 +71,6 @@
         mGestureSelector = gestureSelector;
         mBandController = bandController;
         recView.addOnItemTouchListener(this);
-        emptyView.setOnTouchListener(this);
 
         mScaleDetector = !Build.IS_DEBUGGABLE
                 ? null
@@ -169,11 +166,4 @@
 
     @Override
     public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
-
-    // For mEmptyView events
-    @Override
-    public boolean onTouch(View v, MotionEvent event) {
-        // Pass events to UserInputHandler.
-        return onTouchEvent(event);
-    }
 }
diff --git a/src/com/android/documentsui/dirlist/Message.java b/src/com/android/documentsui/dirlist/Message.java
new file mode 100644
index 0000000..3969ab8
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/Message.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import android.annotation.Nullable;
+
+import com.android.documentsui.R;
+import com.android.documentsui.dirlist.DocumentsAdapter.Environment;
+import com.android.documentsui.dirlist.Model.Update;
+
+/**
+ * Data object used by {@link InflateMessageDocumentHolder} and {@link HeaderMessageDocumentHolder}.
+ */
+abstract class Message {
+    protected final Environment mEnv;
+    private @Nullable CharSequence mMessageString;
+    private int mIconId = -1;
+    private boolean mShouldShow = false;
+
+    Message(Environment env) {
+        mEnv = env;
+    }
+
+    abstract void update(Update Event);
+
+    protected void update(CharSequence messageString, int iconId) {
+        if (messageString == null) {
+            return;
+        }
+        mMessageString = messageString;
+        mIconId = iconId;
+        mShouldShow = true;
+    }
+
+    void reset() {
+        mMessageString = null;
+        mShouldShow = false;
+        mIconId = -1;
+    }
+
+    int getIconId() {
+        return mIconId;
+    }
+
+    boolean shouldShow() {
+        return mShouldShow;
+    }
+
+    CharSequence getMessageString() {
+        return mMessageString;
+    }
+
+    final static class HeaderMessage extends Message {
+
+        HeaderMessage(Environment env) {
+            super(env);
+        }
+
+        @Override
+        void update(Update event) {
+            reset();
+            // Error gets first dibs ... for now
+            // TODO: These should be different Message objects getting updated instead of
+            // overwriting.
+            if (mEnv.getModel().error != null) {
+                update(mEnv.getModel().error, R.drawable.ic_dialog_alert);
+            } else if (mEnv.getModel().info != null) {
+                update(mEnv.getModel().info, R.drawable.ic_dialog_info);
+            }
+        }
+    }
+
+    final static class InflateMessage extends Message {
+
+        InflateMessage(Environment env) {
+            super(env);
+        }
+
+        @Override
+        void update(Update event) {
+            reset();
+            if (event.hasError()) {
+                updateToInflatedErrorMesage();
+            } else if (mEnv.getModel().getModelIds().length == 0) {
+                updateToInflatedEmptyMessage();
+            }
+        }
+
+        private void updateToInflatedErrorMesage() {
+            update(mEnv.getContext().getResources().getText(R.string.query_error),
+                    R.drawable.hourglass);
+        }
+
+        private void updateToInflatedEmptyMessage() {
+            final CharSequence message;
+            if (mEnv.isInSearchMode()) {
+                message = String.format(
+                        String.valueOf(
+                                mEnv.getContext().getResources().getText(R.string.no_results)),
+                        mEnv.getDisplayState().stack.getRoot().title);
+            } else {
+                message = mEnv.getContext().getResources().getText(R.string.empty);
+            }
+            update(message, R.drawable.cabinet);
+        }
+    }
+}
diff --git a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
index 99f9371..6afbd1b 100644
--- a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
+++ b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java
@@ -42,8 +42,6 @@
 final class ModelBackedDocumentsAdapter extends DocumentsAdapter {
 
     private static final String TAG = "ModelBackedDocuments";
-    public static final int ITEM_TYPE_DOCUMENT = 1;
-    public static final int ITEM_TYPE_DIRECTORY = 2;
 
     // Provides access to information needed when creating and view holders. This
     // isn't an ideal pattern (more transitive dependency stuff) but good enough for now.
@@ -57,15 +55,6 @@
     private List<String> mModelIds = new ArrayList<>();
     private EventListener<Model.Update> mModelUpdateListener;
 
-    // List of files that have been deleted. Some transient directory updates
-    // may happen while files are being deleted. During this time we don't
-    // want once-hidden files to be re-shown. We only remove
-    // items from this list when we get a model update where the model
-    // does not contain a corresponding id. This ensures hidden entries
-    // don't momentarily re-appear if we get intermediate updates from
-    // the file system.
-    private Set<String> mHiddenIds = new HashSet<>();
-
     public ModelBackedDocumentsAdapter(Environment env, IconHelper iconHelper) {
         mEnv = env;
         mIconHelper = iconHelper;
@@ -151,23 +140,11 @@
     }
 
     private void onModelUpdate(Model model) {
-        if (DEBUG && mHiddenIds.size() > 0) {
-            Log.d(TAG, "Updating model with hidden ids: " + mHiddenIds);
-        }
-
         String[] modelIds = model.getModelIds();
         mModelIds = new ArrayList<>(modelIds.length);
         for (String id : modelIds) {
-            if (!mHiddenIds.contains(id)) {
-                mModelIds.add(id);
-            } else {
-                if (DEBUG) Log.d(TAG, "Omitting hidden id from model during update: " + id);
-            }
+            mModelIds.add(id);
         }
-
-        // Finally remove any hidden ids that aren't present in the model.
-        // This assumes that model updates represent a complete set of files.
-        mHiddenIds.retainAll(mModelIds);
     }
 
     private void onModelUpdateFailed(Exception e) {
diff --git a/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java b/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java
deleted file mode 100644
index 4cb55b3..0000000
--- a/src/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapper.java
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- * 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.support.v7.widget.GridLayoutManager;
-import android.support.v7.widget.RecyclerView.AdapterDataObserver;
-import android.view.ViewGroup;
-import android.widget.Space;
-
-import com.android.documentsui.R;
-import com.android.documentsui.base.EventListener;
-import com.android.documentsui.base.State;
-import com.android.documentsui.dirlist.Model.Update;
-
-import java.util.List;
-
-/**
- * Adapter wrapper that inserts a sort of line break item between directories and regular files.
- * Only needs to be used in GRID mode...at this time.
- */
-final class SectionBreakDocumentsAdapterWrapper extends DocumentsAdapter {
-
-    private static final String TAG = "SectionBreakDocumentsAdapterWrapper";
-    private static final int ITEM_TYPE_SECTION_BREAK = Integer.MAX_VALUE;
-
-    private final Environment mEnv;
-    private final DocumentsAdapter mDelegate;
-    private final EventListener<Update> mModelUpdateListener;
-
-    private int mBreakPosition = -1;
-
-    SectionBreakDocumentsAdapterWrapper(Environment environment, DocumentsAdapter delegate) {
-        mEnv = environment;
-        mDelegate = delegate;
-
-        // Relay events published by our delegate to our listeners (presumably RecyclerView)
-        // with adjusted positions.
-        mDelegate.registerAdapterDataObserver(new EventRelay());
-
-        mModelUpdateListener = new EventListener<Model.Update>() {
-            @Override
-            public void accept(Update event) {
-                // make sure the delegate handles the update before we do.
-                // This isn't ideal since the delegate might be listening
-                // the updates itself. But this is the safe thing to do
-                // since we read model ids from the delegate
-                // in our update handler.
-                mDelegate.getModelUpdateListener().accept(event);
-                if (!event.hasError()) {
-                    onModelUpdate(mEnv.getModel());
-                }
-            }
-        };
-    }
-
-    @Override
-    EventListener<Update> getModelUpdateListener() {
-        return mModelUpdateListener;
-    }
-
-    @Override
-    public GridLayoutManager.SpanSizeLookup createSpanSizeLookup() {
-        return new GridLayoutManager.SpanSizeLookup() {
-            @Override
-            public int getSpanSize(int position) {
-                // Make layout whitespace span the grid. This has the effect of breaking
-                // grid rows whenever layout whitespace is encountered.
-                if (getItemViewType(position) == ITEM_TYPE_SECTION_BREAK) {
-                    return mEnv.getColumnCount();
-                } else {
-                    return 1;
-                }
-            }
-        };
-    }
-
-    @Override
-    public DocumentHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        if (viewType == ITEM_TYPE_SECTION_BREAK) {
-            return new EmptyDocumentHolder(mEnv.getContext());
-        } else {
-            return mDelegate.createViewHolder(parent, viewType);
-        }
-    }
-
-    @Override
-    public void onBindViewHolder(DocumentHolder holder, int p, List<Object> payload) {
-        if (holder.getItemViewType() != ITEM_TYPE_SECTION_BREAK) {
-            mDelegate.onBindViewHolder(holder, toDelegatePosition(p), payload);
-        } else {
-            ((EmptyDocumentHolder)holder).bind(mEnv.getDisplayState());
-        }
-    }
-
-    @Override
-    public void onBindViewHolder(DocumentHolder holder, int p) {
-        if (holder.getItemViewType() != ITEM_TYPE_SECTION_BREAK) {
-            mDelegate.onBindViewHolder(holder, toDelegatePosition(p));
-        } else {
-            ((EmptyDocumentHolder)holder).bind(mEnv.getDisplayState());
-        }
-    }
-
-    @Override
-    public int getItemCount() {
-        return mBreakPosition == -1
-                ? mDelegate.getItemCount()
-                : mDelegate.getItemCount() + 1;
-    }
-
-    private void onModelUpdate(Model model) {
-        mBreakPosition = -1;
-
-        // Walk down the list of IDs till we encounter something that's not a directory, and
-        // insert a whitespace element - this introduces a visual break in the grid between
-        // folders and documents.
-        // TODO: This code makes assumptions about the model, namely, that it performs a
-        // bucketed sort where directories will always be ordered before other files. CBB.
-        List<String> modelIds = mDelegate.getModelIds();
-        for (int i = 0; i < modelIds.size(); i++) {
-            if (!isDirectory(model, i)) {
-                // If the break is the first thing in the list, then there are actually no
-                // directories. In that case, don't insert a break at all.
-                if (i > 0) {
-                    mBreakPosition = i;
-                }
-                break;
-            }
-        }
-    }
-
-    @Override
-    public int getItemViewType(int p) {
-        if (p == mBreakPosition) {
-            return ITEM_TYPE_SECTION_BREAK;
-        } else {
-            return mDelegate.getItemViewType(toDelegatePosition(p));
-        }
-    }
-
-    /**
-     * Returns the position of an item in the delegate, adjusting
-     * values that are greater than the break position.
-     *
-     * @param p Position within the view
-     * @return Position within the delegate
-     */
-    private int toDelegatePosition(int p) {
-        return (mBreakPosition != -1 && p > mBreakPosition) ? p - 1 : p;
-    }
-
-    /**
-     * Returns the position of an item in the view, adjusting
-     * values that are greater than the break position.
-     *
-     * @param p Position within the delegate
-     * @return Position within the view
-     */
-    private int toViewPosition(int p) {
-        // If position is greater than or equal to the break, increase by one.
-        return (mBreakPosition != -1 && p >= mBreakPosition) ? p + 1 : p;
-    }
-
-    @Override
-    public List<String> getModelIds() {
-        return mDelegate.getModelIds();
-    }
-
-    @Override
-    public String getModelId(int p) {
-        return (p == mBreakPosition) ? null : mDelegate.getModelId(toDelegatePosition(p));
-    }
-
-    @Override
-    public void onItemSelectionChanged(String id) {
-        mDelegate.onItemSelectionChanged(id);
-    }
-
-    // Listener we add to our delegate. This allows us to relay events published
-    // by the delegate to our listeners (presumably RecyclerView) with adjusted positions.
-    private final class EventRelay extends AdapterDataObserver {
-        @Override
-        public void onChanged() {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onItemRangeChanged(int positionStart, int itemCount) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
-            assert(itemCount == 1);
-            notifyItemRangeChanged(toViewPosition(positionStart), itemCount, payload);
-        }
-
-        @Override
-        public void onItemRangeInserted(int positionStart, int itemCount) {
-            assert(itemCount == 1);
-            if (positionStart < mBreakPosition) {
-                mBreakPosition++;
-            }
-            notifyItemRangeInserted(toViewPosition(positionStart), itemCount);
-        }
-
-        @Override
-        public void onItemRangeRemoved(int positionStart, int itemCount) {
-            assert(itemCount == 1);
-            if (positionStart < mBreakPosition) {
-                mBreakPosition--;
-            }
-            notifyItemRangeRemoved(toViewPosition(positionStart), itemCount);
-        }
-
-        @Override
-        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
-            throw new UnsupportedOperationException();
-        }
-    }
-
-    /**
-     * The most elegant transparent blank box that spans N rows ever conceived.
-     */
-    private static final class EmptyDocumentHolder extends DocumentHolder {
-        final int mVisibleHeight;
-        private State mState;
-
-        public EmptyDocumentHolder(Context context) {
-            super(context, new Space(context));
-
-            mVisibleHeight = context.getResources().getDimensionPixelSize(
-                    R.dimen.grid_section_separator_height);
-        }
-
-        public void bind(State state) {
-            mState = state;
-            bind(null, null);
-        }
-
-        @Override
-        public void bind(Cursor cursor, String modelId) {
-            if (mState.derivedMode == State.MODE_GRID) {
-                itemView.setMinimumHeight(mVisibleHeight);
-            } else {
-                itemView.setMinimumHeight(0);
-            }
-            return;
-        }
-    }
-}
diff --git a/src/com/android/documentsui/dirlist/TransparentDividerDocumentHolder.java b/src/com/android/documentsui/dirlist/TransparentDividerDocumentHolder.java
new file mode 100644
index 0000000..2cf8b3e
--- /dev/null
+++ b/src/com/android/documentsui/dirlist/TransparentDividerDocumentHolder.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.widget.Space;
+
+import com.android.documentsui.R;
+import com.android.documentsui.base.State;
+
+/**
+ * The most elegant transparent blank box that spans N rows ever conceived.
+ * Used by {@link DirectoryAddonsAdapter}.
+ */
+final class TransparentDividerDocumentHolder extends DocumentHolder {
+    private final int mVisibleHeight;
+    private State mState;
+
+    public TransparentDividerDocumentHolder(Context context) {
+        super(context, new Space(context));
+
+        mVisibleHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.grid_section_separator_height);
+    }
+
+    public void bind(State state) {
+        mState = state;
+        bind(null, null);
+    }
+
+    @Override
+    public void bind(Cursor cursor, String modelId) {
+        if (mState.derivedMode == State.MODE_GRID) {
+            itemView.setMinimumHeight(mVisibleHeight);
+        } else {
+            itemView.setMinimumHeight(0);
+        }
+        return;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/selection/BandController.java b/src/com/android/documentsui/selection/BandController.java
index b6bca11..5415faf 100644
--- a/src/com/android/documentsui/selection/BandController.java
+++ b/src/com/android/documentsui/selection/BandController.java
@@ -227,9 +227,11 @@
         // mouse moves, or else starting band selection on mouse down can cause problems as events
         // don't get routed correctly to onTouchEvent.
         return !isActive()
-                && e.isActionMove()  // the initial button move via mouse-touch (ie. down press)
-                && mAdapter.getItemCount() > 0
+                && e.isActionMove() // the initial button move via mouse-touch (ie. down press)
+                && mAdapter.hasModelIds() // we want to check against actual modelIds count to
+                                          // avoid dummy view count from the AdapterWrapper
                 && !e.isOverDragHotspot();
+
     }
 
     public boolean shouldStop(InputEvent input) {
diff --git a/tests/Android.mk b/tests/Android.mk
index 9d3b64e..3074500 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -7,8 +7,10 @@
     $(call all-java-files-under, unit) \
     $(call all-java-files-under, functional)
 
-# For testing ZIP files.
+# For testing ZIP files. Include testing ZIP files as uncompresseed raw
+# resources.
 LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_AAPT_FLAGS += -0 .zip
 
 LOCAL_JAVA_LIBRARIES := android.test.runner
 LOCAL_STATIC_JAVA_LIBRARIES := mockito-target ub-uiautomator espresso-core guava
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 0df0ffc..6f4e78b 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -29,6 +29,18 @@
                 <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
             </intent-filter>
        </provider>
+       <!-- Provider for testing archives. -->
+       <provider
+            android:name="com.android.documentsui.archives.ResourcesProvider"
+            android:authorities="com.android.documentsui.archives.resourcesprovider"
+            android:exported="true"
+            android:grantUriPermissions="true"
+            android:permission="android.permission.MANAGE_DOCUMENTS"
+            android:enabled="true">
+            <intent-filter>
+                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
+            </intent-filter>
+      </provider>
     </application>
 
     <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
diff --git a/tests/common/com/android/documentsui/DemoProvider.java b/tests/common/com/android/documentsui/DemoProvider.java
index 67703e7..9ebff5f 100644
--- a/tests/common/com/android/documentsui/DemoProvider.java
+++ b/tests/common/com/android/documentsui/DemoProvider.java
@@ -92,20 +92,24 @@
         c.setExtras(extras);
 
         switch (parentDocumentId) {
-            case "aaa":
+            case "show info":
                 extras.putString(
                         DocumentsContract.EXTRA_INFO,
                         "I'm a synthetic INFO. Don't judge me.");
+                addFolder(c, "folder");
                 addFile(c, "zzz");
+                for (int i = 0; i < 100; i++) {
+                    addFile(c, "" + i);
+                }
                 break;
 
-            case "bbb":
+            case "show error":
                 extras.putString(
-                        DocumentsContract.EXTRA_INFO,
-                        "I'm a synthetic INFO. Don't judge me.");
+                        DocumentsContract.EXTRA_ERROR,
+                        "I'm a synthetic ERROR. Don't judge me.");
                 break;
 
-            case "ccc":
+            case "show both error and info":
                 extras.putString(
                         DocumentsContract.EXTRA_INFO,
                         "INFO: I'm confused. I've show both ERROR and INFO.");
@@ -114,10 +118,14 @@
                         "ERROR: I'm confused. I've show both ERROR and INFO.");
                 break;
 
+            case "throw a nice exception":
+                throw new RuntimeException();
+
             default:
-                addFolder(c, "aaa");
-                addFolder(c, "bbb");
-                addFolder(c, "ccc");
+                addFolder(c, "show info");
+                addFolder(c, "show error");
+                addFolder(c, "show both error and info");
+                addFolder(c, "throw a nice exception");
                 break;
         }
 
diff --git a/tests/common/com/android/documentsui/dirlist/TestModel.java b/tests/common/com/android/documentsui/dirlist/TestModel.java
index 472114b..4c45102 100644
--- a/tests/common/com/android/documentsui/dirlist/TestModel.java
+++ b/tests/common/com/android/documentsui/dirlist/TestModel.java
@@ -17,6 +17,7 @@
 package com.android.documentsui.dirlist;
 
 import android.database.MatrixCursor;
+import android.os.Bundle;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 
@@ -61,6 +62,10 @@
         super.update(r);
     }
 
+    public void setCursorExtras(Bundle bundle) {
+        mCursor.setExtras(bundle);
+    }
+
     public DocumentInfo createFile(String name) {
         return createFile(
                 name,
diff --git a/tests/res/raw/broken.zip b/tests/res/raw/broken.zip
new file mode 100644
index 0000000..0d7096b
--- /dev/null
+++ b/tests/res/raw/broken.zip
@@ -0,0 +1 @@
+I love vanilla ice cream!
diff --git a/tests/unit/com/android/documentsui/archives/ArchiveTest.java b/tests/unit/com/android/documentsui/archives/ArchiveTest.java
index e69096c..e4356e5 100644
--- a/tests/unit/com/android/documentsui/archives/ArchiveTest.java
+++ b/tests/unit/com/android/documentsui/archives/ArchiveTest.java
@@ -42,14 +42,15 @@
     private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
     private static final String NOTIFICATION_URI = "content://notification-uri";
     private ExecutorService mExecutor = null;
-    private Context mContext = null;
     private Archive mArchive = null;
+    private TestUtils mTestUtils = null;
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        mContext = InstrumentationRegistry.getTargetContext();
         mExecutor = Executors.newSingleThreadExecutor();
+        mTestUtils = new TestUtils(InstrumentationRegistry.getTargetContext(),
+                InstrumentationRegistry.getContext(), mExecutor);
     }
 
     @Override
@@ -66,93 +67,16 @@
         return new ArchiveId(ARCHIVE_URI, path);
     }
 
-    /**
-     * Opens a resource and returns the contents via file descriptor to a local
-     * snapshot file.
-     */
-    public ParcelFileDescriptor getSeekableDescriptor(int resource) {
-        // Extract the file from resources.
-        File file = null;
-        final Context testContext = InstrumentationRegistry.getContext();
-        try {
-            file = File.createTempFile("com.android.documentsui.archives.tests{",
-                    "}.zip", mContext.getCacheDir());
-            try (
-                final FileOutputStream outputStream =
-                        new ParcelFileDescriptor.AutoCloseOutputStream(
-                                ParcelFileDescriptor.open(
-                                        file, ParcelFileDescriptor.MODE_WRITE_ONLY));
-                final InputStream inputStream =
-                        testContext.getResources().openRawResource(resource);
-            ) {
-                final byte[] buffer = new byte[32 * 1024];
-                int bytes;
-                while ((bytes = inputStream.read(buffer)) != -1) {
-                    outputStream.write(buffer, 0, bytes);
-                }
-                outputStream.flush();
-
-            }
-            return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
-        } catch (IOException e) {
-            fail(String.valueOf(e));
-            return null;
-        } finally {
-            // On UNIX the file will be still available for processes which opened it, even
-            // after deleting it. Remove it ASAP, as it won't be used by anyone else.
-            if (file != null) {
-                file.delete();
-            }
-        }
-    }
-
-    /**
-     * Opens a resource and returns the contents via a pipe.
-     */
-    public ParcelFileDescriptor getNonSeekableDescriptor(int resource) {
-        ParcelFileDescriptor[] pipe = null;
-        final Context testContext = InstrumentationRegistry.getContext();
-        try {
-            pipe = ParcelFileDescriptor.createPipe();
-            final ParcelFileDescriptor finalOutputPipe = pipe[1];
-            mExecutor.execute(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            try (
-                                final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
-                                        new ParcelFileDescriptor.
-                                                AutoCloseOutputStream(finalOutputPipe);
-                                final InputStream inputStream =
-                                        testContext.getResources().openRawResource(resource);
-                            ) {
-                                final byte[] buffer = new byte[32 * 1024];
-                                int bytes;
-                                while ((bytes = inputStream.read(buffer)) != -1) {
-                                    outputStream.write(buffer, 0, bytes);
-                                }
-                            } catch (IOException e) {
-                              fail(String.valueOf(e));
-                            }
-                        }
-                    });
-            return pipe[0];
-        } catch (IOException e) {
-            fail(String.valueOf(e));
-            return null;
-        }
-    }
-
     public void loadArchive(ParcelFileDescriptor descriptor) throws IOException {
         mArchive = Archive.createForParcelFileDescriptor(
-                mContext,
+                InstrumentationRegistry.getTargetContext(),
                 descriptor,
                 ARCHIVE_URI,
                 Uri.parse(NOTIFICATION_URI));
     }
 
     public void testQueryChildDocument() throws IOException {
-        loadArchive(getNonSeekableDescriptor(R.raw.archive));
+        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         final Cursor cursor = mArchive.queryChildDocuments(
                 new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
 
@@ -210,7 +134,7 @@
     }
 
     public void testQueryChildDocument_NoDirs() throws IOException {
-        loadArchive(getNonSeekableDescriptor(R.raw.no_dirs));
+        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.no_dirs));
         final Cursor cursor = mArchive.queryChildDocuments(
             new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
 
@@ -257,7 +181,7 @@
     }
 
     public void testQueryChildDocument_EmptyDirs() throws IOException {
-        loadArchive(getNonSeekableDescriptor(R.raw.empty_dirs));
+        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.empty_dirs));
         final Cursor cursor = mArchive.queryChildDocuments(
                 new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
 
@@ -317,7 +241,7 @@
     }
 
     public void testGetDocumentType() throws IOException {
-        loadArchive(getNonSeekableDescriptor(R.raw.archive));
+        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         assertEquals(Document.MIME_TYPE_DIR, mArchive.getDocumentType(
                 new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId()));
         assertEquals("text/plain", mArchive.getDocumentType(
@@ -325,7 +249,7 @@
     }
 
     public void testIsChildDocument() throws IOException {
-        loadArchive(getNonSeekableDescriptor(R.raw.archive));
+        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         final String documentId = new ArchiveId(ARCHIVE_URI, "/").toDocumentId();
         assertTrue(mArchive.isChildDocument(documentId,
                 new ArchiveId(ARCHIVE_URI, "/dir1/").toDocumentId()));
@@ -339,7 +263,7 @@
     }
 
     public void testQueryDocument() throws IOException {
-        loadArchive(getNonSeekableDescriptor(R.raw.archive));
+        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         final Cursor cursor = mArchive.queryDocument(
                 new ArchiveId(ARCHIVE_URI, "/dir2/strawberries.txt").toDocumentId(),
                 null);
@@ -357,12 +281,12 @@
     }
 
     public void testOpenDocument() throws IOException {
-        loadArchive(getSeekableDescriptor(R.raw.archive));
+        loadArchive(mTestUtils.getSeekableDescriptor(R.raw.archive));
         commonTestOpenDocument();
     }
 
     public void testOpenDocument_NonSeekable() throws IOException {
-        loadArchive(getNonSeekableDescriptor(R.raw.archive));
+        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
         commonTestOpenDocument();
     }
 
@@ -378,7 +302,13 @@
     }
 
     public void testCanSeek() throws IOException {
-        assertTrue(Archive.canSeek(getSeekableDescriptor(R.raw.archive)));
-        assertFalse(Archive.canSeek(getNonSeekableDescriptor(R.raw.archive)));
+        assertTrue(Archive.canSeek(mTestUtils.getSeekableDescriptor(R.raw.archive)));
+        assertFalse(Archive.canSeek(mTestUtils.getNonSeekableDescriptor(R.raw.archive)));
+    }
+
+    public void testBrokenArchive() throws IOException {
+        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
+        final Cursor cursor = mArchive.queryChildDocuments(
+                new ArchiveId(ARCHIVE_URI, "/").toDocumentId(), null, null);
     }
 }
diff --git a/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java b/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
new file mode 100644
index 0000000..c196c0c
--- /dev/null
+++ b/tests/unit/com/android/documentsui/archives/ArchivesProviderTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.archives;
+
+import com.android.documentsui.archives.ArchivesProvider;
+import com.android.documentsui.archives.Archive;
+import com.android.documentsui.tests.R;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract;
+import android.support.test.InstrumentationRegistry;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.text.TextUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Scanner;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.CountDownLatch;
+
+@MediumTest
+public class ArchivesProviderTest extends AndroidTestCase {
+    private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
+    private static final String NOTIFICATION_URI = "content://notification-uri";
+    private ExecutorService mExecutor = null;
+    private Archive mArchive = null;
+    private TestUtils mTestUtils = null;
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        mExecutor = Executors.newSingleThreadExecutor();
+        mTestUtils = new TestUtils(InstrumentationRegistry.getTargetContext(),
+                InstrumentationRegistry.getContext(), mExecutor);
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mExecutor.shutdown();
+        assertTrue(mExecutor.awaitTermination(3 /* timeout */, TimeUnit.SECONDS));
+        super.tearDown();
+    }
+
+    public void testOpen_Success() throws InterruptedException {
+        final Uri sourceUri = DocumentsContract.buildDocumentUri(
+                ResourcesProvider.AUTHORITY, "archive.zip");
+        final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri);
+
+        final Uri childrenUri = DocumentsContract.buildChildDocumentsUri(
+                ArchivesProvider.AUTHORITY, DocumentsContract.getDocumentId(archiveUri));
+
+        final ContentResolver resolver = getContext().getContentResolver();
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        {
+            final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
+            assertNotNull("Cursor must not be null. File not found?", cursor);
+
+            assertEquals(0, cursor.getCount());
+            final Bundle extras = cursor.getExtras();
+            assertEquals(true, extras.getBoolean(DocumentsContract.EXTRA_LOADING, false));
+            assertNull(extras.getString(DocumentsContract.EXTRA_ERROR));
+
+            final Uri notificationUri = cursor.getNotificationUri();
+            assertNotNull(notificationUri);
+
+            resolver.registerContentObserver(notificationUri, false, new ContentObserver(null) {
+                @Override
+                public void onChange(boolean selfChange, Uri uri) {
+                    latch.countDown();
+                }
+            });
+        }
+
+        latch.await(3, TimeUnit.SECONDS);
+        {
+            final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
+            assertNotNull("Cursor must not be null. File not found?", cursor);
+
+            assertEquals(3, cursor.getCount());
+            final Bundle extras = cursor.getExtras();
+            assertEquals(false, extras.getBoolean(DocumentsContract.EXTRA_LOADING, false));
+            assertNull(extras.getString(DocumentsContract.EXTRA_ERROR));
+        }
+    }
+
+    public void testOpen_Failure() throws InterruptedException {
+        final Uri sourceUri = DocumentsContract.buildDocumentUri(
+                ResourcesProvider.AUTHORITY, "broken.zip");
+        final Uri archiveUri = ArchivesProvider.buildUriForArchive(sourceUri);
+
+        final Uri childrenUri = DocumentsContract.buildChildDocumentsUri(
+                ArchivesProvider.AUTHORITY, DocumentsContract.getDocumentId(archiveUri));
+
+        final ContentResolver resolver = getContext().getContentResolver();
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        {
+            final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
+            assertNotNull("Cursor must not be null. File not found?", cursor);
+
+            assertEquals(0, cursor.getCount());
+            final Bundle extras = cursor.getExtras();
+            assertEquals(true, extras.getBoolean(DocumentsContract.EXTRA_LOADING, false));
+            assertNull(extras.getString(DocumentsContract.EXTRA_ERROR));
+
+            final Uri notificationUri = cursor.getNotificationUri();
+            assertNotNull(notificationUri);
+
+            resolver.registerContentObserver(notificationUri, false, new ContentObserver(null) {
+                @Override
+                public void onChange(boolean selfChange, Uri uri) {
+                    latch.countDown();
+                }
+            });
+        }
+
+        latch.await(3, TimeUnit.SECONDS);
+        {
+            final Cursor cursor = resolver.query(childrenUri, null, null, null, null, null);
+            assertNotNull("Cursor must not be null. File not found?", cursor);
+
+            assertEquals(0, cursor.getCount());
+            final Bundle extras = cursor.getExtras();
+            assertEquals(false, extras.getBoolean(DocumentsContract.EXTRA_LOADING, false));
+            assertFalse(TextUtils.isEmpty(extras.getString(DocumentsContract.EXTRA_ERROR)));
+        }
+    }}
diff --git a/tests/unit/com/android/documentsui/archives/ResourcesProvider.java b/tests/unit/com/android/documentsui/archives/ResourcesProvider.java
new file mode 100644
index 0000000..988b495
--- /dev/null
+++ b/tests/unit/com/android/documentsui/archives/ResourcesProvider.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.archives;
+
+import com.android.documentsui.tests.R;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MatrixCursor.RowBuilder;
+import android.net.Uri;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsProvider;
+import android.webkit.MimeTypeMap;
+
+import libcore.io.IoUtils;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+
+public class ResourcesProvider extends DocumentsProvider {
+    public static final String AUTHORITY = "com.android.documentsui.archives.resourcesprovider";
+
+    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
+            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID
+    };
+    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
+            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME,
+            Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
+    };
+
+    private static final Map<String, Integer> RESOURCES = new HashMap<>();
+    static {
+        RESOURCES.put("archive.zip", R.raw.archive);
+        RESOURCES.put("empty_dirs.zip", R.raw.empty_dirs);
+        RESOURCES.put("no_dirs.zip", R.raw.no_dirs);
+        RESOURCES.put("broken.zip", R.raw.broken);
+    }
+
+    private ExecutorService mExecutor = null;
+    private TestUtils mTestUtils = null;
+
+    @Override
+    public boolean onCreate() {
+        mExecutor = Executors.newSingleThreadExecutor();
+        mTestUtils = new TestUtils(getContext(), getContext(), mExecutor);
+        return true;
+    }
+
+    @Override
+    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection
+                : DEFAULT_ROOT_PROJECTION);
+        final RowBuilder row = result.newRow();
+        row.add(Root.COLUMN_ROOT_ID, "root-id");
+        row.add(Root.COLUMN_FLAGS, 0);
+        row.add(Root.COLUMN_TITLE, "ResourcesProvider");
+        row.add(Root.COLUMN_DOCUMENT_ID, "root-document-id");
+        return result;
+    }
+
+    @Override
+    public Cursor queryDocument(String documentId, String[] projection)
+            throws FileNotFoundException {
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection
+                : DEFAULT_DOCUMENT_PROJECTION);
+        if ("root-document-id".equals(documentId)) {
+            final RowBuilder row = result.newRow();
+            row.add(Document.COLUMN_DOCUMENT_ID, "root-document-id");
+            row.add(Document.COLUMN_FLAGS, 0);
+            row.add(Document.COLUMN_DISPLAY_NAME, "ResourcesProvider");
+            row.add(Document.COLUMN_SIZE, 0);
+            row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
+            return result;
+        }
+
+        includeDocument(result, documentId);
+        return result;
+    }
+
+    @Override
+    public Cursor queryChildDocuments(
+            String parentDocumentId, String[] projection, String sortOrder)
+            throws FileNotFoundException {
+        if (!"root-document-id".equals(parentDocumentId)) {
+            throw new FileNotFoundException();
+        }
+
+        final MatrixCursor result = new MatrixCursor(projection != null ? projection
+                : DEFAULT_DOCUMENT_PROJECTION);
+        for (String documentId : RESOURCES.keySet()) {
+            includeDocument(result, documentId);
+        }
+        return result;
+    }
+
+    @Override
+    public ParcelFileDescriptor openDocument(String docId, String mode, CancellationSignal signal)
+            throws FileNotFoundException {
+        final Integer resourceId = RESOURCES.get(docId);
+        if (resourceId == null) {
+            throw new FileNotFoundException();
+        }
+        return mTestUtils.getSeekableDescriptor(resourceId);
+    }
+
+    void includeDocument(MatrixCursor result, String documentId) throws FileNotFoundException {
+        final Integer resourceId = RESOURCES.get(documentId);
+        if (resourceId == null) {
+            throw new FileNotFoundException();
+        }
+
+        AssetFileDescriptor fd = null;
+        try {
+            fd = getContext().getResources().openRawResourceFd(resourceId);
+            final RowBuilder row = result.newRow();
+            row.add(Document.COLUMN_DOCUMENT_ID, documentId);
+            row.add(Document.COLUMN_FLAGS, 0);
+            row.add(Document.COLUMN_DISPLAY_NAME, documentId);
+
+            final int lastDot = documentId.lastIndexOf('.');
+            assert(lastDot > 0);
+            final String extension = documentId.substring(lastDot + 1).toLowerCase();
+            final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+
+            row.add(Document.COLUMN_MIME_TYPE, mimeType);
+            row.add(Document.COLUMN_SIZE, fd.getLength());
+        }
+        finally {
+            IoUtils.closeQuietly(fd);
+        }
+    }
+}
diff --git a/tests/unit/com/android/documentsui/archives/TestUtils.java b/tests/unit/com/android/documentsui/archives/TestUtils.java
new file mode 100644
index 0000000..c3a4b2a
--- /dev/null
+++ b/tests/unit/com/android/documentsui/archives/TestUtils.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.archives;
+
+import com.android.documentsui.archives.Archive;
+import com.android.documentsui.tests.R;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.ExecutorService;
+
+import android.util.Log;
+
+public class TestUtils {
+    public static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
+    public static final String NOTIFICATION_URI = "content://notification-uri";
+
+    public final Context mTargetContext;
+    public final Context mTestContext;
+    public final ExecutorService mExecutor;
+
+    public TestUtils(Context targetContext, Context testContext, ExecutorService executor) {
+        mTargetContext = targetContext;
+        mTestContext = testContext;
+        mExecutor = executor;
+    }
+
+    /**
+     * Opens a resource and returns the contents via file descriptor to a local
+     * snapshot file.
+     */
+    public ParcelFileDescriptor getSeekableDescriptor(int resource) {
+        // Extract the file from resources.
+        File file = null;
+        try {
+            file = File.createTempFile("com.android.documentsui.archives.tests{",
+                    "}.zip", mTargetContext.getCacheDir());
+            try (
+                final FileOutputStream outputStream =
+                        new ParcelFileDescriptor.AutoCloseOutputStream(
+                                ParcelFileDescriptor.open(
+                                        file, ParcelFileDescriptor.MODE_WRITE_ONLY));
+                final InputStream inputStream =
+                        mTestContext.getResources().openRawResource(resource);
+            ) {
+                final byte[] buffer = new byte[32 * 1024];
+                int bytes;
+                while ((bytes = inputStream.read(buffer)) != -1) {
+                    outputStream.write(buffer, 0, bytes);
+                }
+                outputStream.flush();
+            }
+            return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
+        } catch (IOException e) {
+            throw new IllegalStateException("Creating a snapshot failed. ", e);
+        } finally {
+            // On UNIX the file will be still available for processes which opened it, even
+            // after deleting it. Remove it ASAP, as it won't be used by anyone else.
+            if (file != null) {
+                file.delete();
+            }
+        }
+    }
+
+    /**
+     * Opens a resource and returns the contents via a pipe.
+     */
+    public ParcelFileDescriptor getNonSeekableDescriptor(int resource) {
+        ParcelFileDescriptor[] pipe = null;
+        try {
+            pipe = ParcelFileDescriptor.createPipe();
+            final ParcelFileDescriptor finalOutputPipe = pipe[1];
+            mExecutor.execute(
+                    new Runnable() {
+                        @Override
+                        public void run() {
+                            try (
+                                final ParcelFileDescriptor.AutoCloseOutputStream outputStream =
+                                        new ParcelFileDescriptor.
+                                                AutoCloseOutputStream(finalOutputPipe);
+                                final InputStream inputStream =
+                                        mTestContext.getResources().openRawResource(resource);
+                            ) {
+                                final byte[] buffer = new byte[32 * 1024];
+                                int bytes;
+                                while ((bytes = inputStream.read(buffer)) != -1) {
+                                    outputStream.write(buffer, 0, bytes);
+                                }
+                            } catch (IOException e) {
+                              throw new IllegalStateException("Piping resource failed.", e);
+                            }
+                        }
+                    });
+            return pipe[0];
+        } catch (IOException e) {
+            throw new IllegalStateException("Failed to create a pipe.", e);
+        }
+    }
+}
diff --git a/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java b/tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java
similarity index 67%
rename from tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
rename to tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java
index 39ab24a..ddf2485 100644
--- a/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/DirectoryAddonsAdapterTest.java
@@ -18,6 +18,8 @@
 
 import android.content.Context;
 import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.DocumentsContract;
 import android.support.test.filters.MediumTest;
 import android.support.v7.widget.RecyclerView;
 import android.test.AndroidTestCase;
@@ -27,12 +29,12 @@
 import com.android.documentsui.testing.TestEnv;
 
 @MediumTest
-public class SectionBreakDocumentsAdapterWrapperTest extends AndroidTestCase {
+public class DirectoryAddonsAdapterTest extends AndroidTestCase {
 
     private static final String AUTHORITY = "test_authority";
 
     private TestEnv mEnv;
-    private SectionBreakDocumentsAdapterWrapper mAdapter;
+    private DirectoryAddonsAdapter mAdapter;
 
     public void setUp() {
 
@@ -42,7 +44,7 @@
         final Context testContext = TestContext.createStorageTestContext(getContext(), AUTHORITY);
         DocumentsAdapter.Environment env = new TestEnvironment(testContext);
 
-        mAdapter = new SectionBreakDocumentsAdapterWrapper(
+        mAdapter = new DirectoryAddonsAdapter(
             env,
             new ModelBackedDocumentsAdapter(
                     env, new IconHelper(testContext, State.MODE_GRID)));
@@ -77,6 +79,49 @@
         assertEquals(mEnv.model.getItemCount(), mAdapter.getItemCount());
     }
 
+    public void testAddsInfoMessage_WithDirectoryChildren() {
+        String[] names = {"123.txt", "234.jpg", "abc.pdf"};
+        for (String name : names) {
+            mEnv.model.createFile(name);
+        }
+        Bundle bundle = new Bundle();
+        bundle.putString(DocumentsContract.EXTRA_INFO, "some info");
+        mEnv.model.setCursorExtras(bundle);
+        mEnv.model.update();
+        assertEquals(mEnv.model.getItemCount() + 1, mAdapter.getItemCount());
+        assertHolderType(0, DocumentsAdapter.ITEM_TYPE_HEADER_MESSAGE);
+    }
+
+    public void testItemCount_none() {
+        mEnv.model.update();
+        assertEquals(1, mAdapter.getItemCount());
+        assertHolderType(0, DocumentsAdapter.ITEM_TYPE_INFLATED_MESSAGE);
+    }
+
+    public void testAddsInfoMessage_WithNoItem() {
+        Bundle bundle = new Bundle();
+        bundle.putString(DocumentsContract.EXTRA_INFO, "some info");
+        mEnv.model.setCursorExtras(bundle);
+
+        mEnv.model.update();
+        assertEquals(2, mAdapter.getItemCount());
+        assertHolderType(0, DocumentsAdapter.ITEM_TYPE_HEADER_MESSAGE);
+    }
+
+    public void testAddsErrorMessage_WithNoItem() {
+        Bundle bundle = new Bundle();
+        bundle.putString(DocumentsContract.EXTRA_ERROR, "some error");
+        mEnv.model.setCursorExtras(bundle);
+
+        mEnv.model.update();
+        assertEquals(2, mAdapter.getItemCount());
+        assertHolderType(0, DocumentsAdapter.ITEM_TYPE_HEADER_MESSAGE);
+    }
+
+    private void assertHolderType(int index, int type) {
+        assertTrue(mAdapter.getItemViewType(index) == type);
+    }
+
     private final class TestEnvironment implements DocumentsAdapter.Environment {
         private final Context testContext;
 
@@ -108,6 +153,11 @@
         }
 
         @Override
+        public boolean isInSearchMode() {
+            return false;
+        }
+
+        @Override
         public Context getContext() {
             return testContext;
         }
diff --git a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
index eb1ee6f..ed0bc85 100644
--- a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
@@ -80,6 +80,11 @@
         }
 
         @Override
+        public boolean isInSearchMode() {
+            return false;
+        }
+
+        @Override
         public Context getContext() {
             return testContext;
         }