Merge "[multi part] Moving Empty/Error/No Results view and Info/Error bar into DocsHolders." 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/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/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/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/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;
         }