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;
}