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