Merge "Always pass DragEvents to the underlying DirFragment's ItemDragListener." into nyc-andromeda-dev
diff --git a/res/values-uz-rUZ/strings.xml b/res/values-uz-rUZ/strings.xml
index c865aff..3198be3 100644
--- a/res/values-uz-rUZ/strings.xml
+++ b/res/values-uz-rUZ/strings.xml
@@ -122,7 +122,7 @@
<string name="open_external_dialog_root_request" msgid="6776729293982633">"<xliff:g id="APPNAME"><b>^1</b></xliff:g> ilovasiga <xliff:g id="STORAGE"><i>^2</i></xliff:g> xotirasidagi ma’lumotlardan, jumladan, rasmlar va videolardan foydalanishiga ruxsat berilsinmi?"</string>
<string name="never_ask_again" msgid="525908236522201138">"Boshqa so‘ralmasin"</string>
<string name="allow" msgid="1275746941353040309">"Ruxsat berish"</string>
- <string name="deny" msgid="5127201668078153379">"Rad qilish"</string>
+ <string name="deny" msgid="5127201668078153379">"Rad etish"</string>
<plurals name="elements_selected" formatted="false" msgid="4448165978637163692">
<item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> ta tanlandi</item>
<item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g> ta tanlandi</item>
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 81463d8..f867a02 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -37,6 +37,7 @@
import com.android.documentsui.dirlist.Model;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.files.LauncherActivity;
+import com.android.documentsui.files.OpenUriForViewTask;
import com.android.documentsui.roots.LoadRootTask;
import com.android.documentsui.roots.RootsAccess;
import com.android.documentsui.sidebar.EjectRootTask;
@@ -53,21 +54,25 @@
protected final T mActivity;
protected final State mState;
protected final RootsAccess mRoots;
+ protected final DocumentsAccess mDocs;
protected final Lookup<String, Executor> mExecutors;
public AbstractActionHandler(
T activity,
State state,
RootsAccess roots,
+ DocumentsAccess docs,
Lookup<String, Executor> executors) {
assert(activity != null);
assert(state != null);
assert(roots != null);
+ assert(docs != null);
mActivity = activity;
mState = state;
mRoots = roots;
+ mDocs = docs;
mExecutors = executors;
}
@@ -138,9 +143,15 @@
}
@Override
+ public final void loadDocument(Uri uri) {
+ new OpenUriForViewTask<>(mActivity, mState, mRoots, mDocs, uri)
+ .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
+ }
+
+ @Override
public final void loadRoot(Uri uri) {
- new LoadRootTask<>(mActivity, mRoots, mState, uri).executeOnExecutor(
- mExecutors.lookup(uri.getAuthority()));
+ new LoadRootTask<>(mActivity, mRoots, mState, uri)
+ .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
}
protected final void loadHomeDir() {
@@ -152,10 +163,13 @@
* from our concrete activity implementations.
*/
public interface CommonAddons {
- void onRootPicked(RootInfo root);
- // TODO: Move this to PickAddons.
- void onDocumentsPicked(List<DocumentInfo> docs);
- void onDocumentPicked(DocumentInfo doc, Model model);
void refreshCurrentRootAndDirectory(@AnimationType int anim);
+ void onRootPicked(RootInfo root);
+ // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity.
+ void onDocumentsPicked(List<DocumentInfo> docs);
+ void onDocumentPicked(DocumentInfo doc);
+ void openContainerDocument(DocumentInfo doc);
+ RootInfo getCurrentRoot();
+ DocumentInfo getCurrentDirectory();
}
}
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index f4aa0ca..0c33f6c 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -52,6 +52,8 @@
void loadRoot(Uri uri);
+ void loadDocument(Uri uri);
+
void openInNewWindow(DocumentStack path);
void pasteIntoFolder(RootInfo root);
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 9f5e66c..5d1c40c 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -47,7 +47,6 @@
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.KeyEvent;
-import android.view.KeyboardShortcutGroup;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@@ -69,6 +68,7 @@
import com.android.documentsui.dirlist.Model;
import com.android.documentsui.dirlist.MultiSelectManager;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.roots.GetRootDocumentTask;
import com.android.documentsui.roots.RootsCache;
import com.android.documentsui.sidebar.RootsFragment;
import com.android.documentsui.sorting.SortController;
@@ -210,7 +210,6 @@
@Override
public void onSearchViewChanged(boolean opened) {
- mState.sortModel.setSortEnabled(!opened);
mNavigator.update();
}
};
@@ -382,7 +381,9 @@
&& !root.isDownloads();
}
- protected void openContainerDocument(DocumentInfo doc) {
+ // TODO: Move to ActionHandler...currently blocked by the notifyDirectory....business.
+ @Override
+ public void openContainerDocument(DocumentInfo doc) {
assert(doc.isContainer());
notifyDirectoryNavigated(doc.derivedUri);
@@ -548,6 +549,7 @@
}
}
+ @Override
public DocumentInfo getCurrentDirectory() {
return mState.stack.peek();
}
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index ab79d01..d2c7d10 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -25,7 +25,6 @@
import android.os.CancellationSignal;
import android.os.OperationCanceledException;
import android.os.RemoteException;
-import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.util.Log;
@@ -35,8 +34,6 @@
import com.android.documentsui.roots.RootCursorWrapper;
import com.android.documentsui.sorting.SortModel;
-import java.io.FileNotFoundException;
-
import libcore.io.IoUtils;
public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> {
@@ -86,7 +83,6 @@
final DirectoryResult result = new DirectoryResult();
result.doc = mDoc;
- result.sortModel = mModel;
ContentProviderClient client = null;
Cursor cursor;
@@ -107,6 +103,8 @@
cursor = new FilteringCursorWrapper(cursor, null, SEARCH_REJECT_MIMES);
}
+ cursor = mModel.sortCursor(cursor);
+
result.client = client;
result.cursor = cursor;
} catch (Exception e) {
diff --git a/src/com/android/documentsui/DirectoryResult.java b/src/com/android/documentsui/DirectoryResult.java
index 32910ce..9e4d9f3 100644
--- a/src/com/android/documentsui/DirectoryResult.java
+++ b/src/com/android/documentsui/DirectoryResult.java
@@ -20,7 +20,6 @@
import android.database.Cursor;
import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.sorting.SortModel;
import libcore.io.IoUtils;
@@ -29,7 +28,6 @@
public Cursor cursor;
public Exception exception;
public DocumentInfo doc;
- public SortModel sortModel;
@Override
public void close() {
@@ -37,6 +35,6 @@
ContentProviderClient.releaseQuietly(client);
cursor = null;
client = null;
- sortModel = null;
+ doc = null;
}
}
diff --git a/src/com/android/documentsui/DocumentsAccess.java b/src/com/android/documentsui/DocumentsAccess.java
new file mode 100644
index 0000000..cd9c4c9
--- /dev/null
+++ b/src/com/android/documentsui/DocumentsAccess.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package com.android.documentsui;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.util.Log;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.RootInfo;
+
+import java.io.FileNotFoundException;
+
+/**
+ * Provides synchronous access to {@link DocumentInfo} instances given some identifying information.
+ */
+public interface DocumentsAccess {
+
+ @Nullable DocumentInfo getRootDocument(RootInfo root);
+ @Nullable DocumentInfo getRootDocument(Uri uri);
+ @Nullable DocumentInfo getDocument(Uri uri);
+
+ public static DocumentsAccess create(Context context) {
+ return new RuntimeDocumentAccess(context);
+ }
+
+ public final class RuntimeDocumentAccess implements DocumentsAccess {
+
+ private static final String TAG = "DocumentAccess";
+
+ private final Context mContext;
+
+ private RuntimeDocumentAccess(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public @Nullable DocumentInfo getRootDocument(RootInfo root) {
+ return getRootDocument(
+ DocumentsContract.buildDocumentUri(root.authority, root.documentId));
+ }
+
+ @Override
+ public @Nullable DocumentInfo getRootDocument(Uri uri) {
+ try {
+ return DocumentInfo.fromUri(mContext.getContentResolver(), uri);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Failed to find root", e);
+ return null;
+ }
+ }
+
+ @Override
+ public DocumentInfo getDocument(Uri uri) {
+ try {
+ return DocumentInfo.fromUri(mContext.getContentResolver(), uri);
+ } catch (FileNotFoundException e) {
+ Log.w(TAG, "Couldn't create DocumentInfo for uri: " + uri);
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java
index a95bf72..4f97111 100644
--- a/src/com/android/documentsui/NavigationViewManager.java
+++ b/src/com/android/documentsui/NavigationViewManager.java
@@ -130,6 +130,7 @@
}
interface Environment {
+ @Deprecated // Use CommonAddones#getCurrentRoot
RootInfo getCurrentRoot();
String getDrawerTitle();
@Deprecated // Use CommonAddones#refreshCurrentRootAndDirectory
diff --git a/src/com/android/documentsui/RecentsLoader.java b/src/com/android/documentsui/RecentsLoader.java
index dc4d9f2..6ce1896 100644
--- a/src/com/android/documentsui/RecentsLoader.java
+++ b/src/com/android/documentsui/RecentsLoader.java
@@ -40,10 +40,10 @@
import com.android.documentsui.roots.RootsAccess;
import com.android.internal.annotations.GuardedBy;
-import com.google.common.util.concurrent.AbstractFuture;
-
import libcore.io.IoUtils;
+import com.google.common.util.concurrent.AbstractFuture;
+
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
@@ -180,7 +180,6 @@
}
final DirectoryResult result = new DirectoryResult();
- result.sortModel = mState.sortModel;
final Cursor merged;
if (cursors.size() > 0) {
@@ -190,13 +189,16 @@
merged = new MatrixCursor(new String[0]);
}
+
+ final Cursor sorted = mState.sortModel.sortCursor(merged);
+
// Tell the UI if this is an in-progress result. When loading is complete, another update is
// sent with EXTRA_LOADING set to false.
Bundle extras = new Bundle();
extras.putBoolean(DocumentsContract.EXTRA_LOADING, !allDone);
- merged.setExtras(extras);
+ sorted.setExtras(extras);
- result.cursor = merged;
+ result.cursor = sorted;
return result;
}
diff --git a/src/com/android/documentsui/base/RootInfo.java b/src/com/android/documentsui/base/RootInfo.java
index 1b4656a..2c32b9e 100644
--- a/src/com/android/documentsui/base/RootInfo.java
+++ b/src/com/android/documentsui/base/RootInfo.java
@@ -37,6 +37,7 @@
import com.android.documentsui.IconUtils;
import com.android.documentsui.R;
+import com.android.documentsui.roots.RootsAccess;
import java.io.DataInputStream;
import java.io.DataOutputStream;
@@ -349,8 +350,9 @@
}
/**
- * Gets the {@link DocumentInfo} of the root folder of this root.
+ * @deprecate use {@link RootsAccess#getRootDocumentBlocking}.
*/
+ @Deprecated
public @Nullable DocumentInfo getRootDocumentBlocking(Context context) {
try {
final Uri uri = DocumentsContract.buildDocumentUri(authority, documentId);
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 32ea333..a5c2564 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -659,7 +659,7 @@
if (docs.size() > 1) {
activity.onDocumentsPicked(docs);
} else {
- activity.onDocumentPicked(docs.get(0), mModel);
+ activity.onDocumentPicked(docs.get(0));
}
}
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 7973101..6c08859 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -16,7 +16,6 @@
package com.android.documentsui.dirlist;
-import static com.android.documentsui.base.DocumentInfo.getCursorLong;
import static com.android.documentsui.base.DocumentInfo.getCursorString;
import static com.android.documentsui.base.Shared.DEBUG;
@@ -34,11 +33,8 @@
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.EventListener;
-import com.android.documentsui.base.Shared;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.roots.RootCursorWrapper;
-import com.android.documentsui.sorting.SortDimension;
-import com.android.documentsui.sorting.SortModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -60,12 +56,7 @@
private int mCursorCount;
/** Maps Model ID to cursor positions, for looking up items by Model ID. */
private Map<String, Integer> mPositions = new HashMap<>();
- /**
- * A sorted array of model IDs for the files currently in the Model. Sort order is determined
- * by {@link #mSortModel}
- */
private String mIds[] = new String[0];
- private SortModel mSortModel;
@Nullable String info;
@Nullable String error;
@@ -126,7 +117,6 @@
mCursor = result.cursor;
mCursorCount = mCursor.getCount();
- mSortModel = result.sortModel;
doc = result.doc;
updateModelData();
@@ -151,24 +141,7 @@
* according to the current sort order.
*/
private void updateModelData() {
- int[] positions = new int[mCursorCount];
mIds = new String[mCursorCount];
- boolean[] isDirs = new boolean[mCursorCount];
- String[] displayNames = null;
- long[] longValues = null;
-
- final int id = mSortModel.getSortedDimensionId();
- switch (id) {
- case SortModel.SORT_DIMENSION_ID_TITLE:
- displayNames = new String[mCursorCount];
- break;
- case SortModel.SORT_DIMENSION_ID_DATE:
- case SortModel.SORT_DIMENSION_ID_SIZE:
- longValues = new long[mCursorCount];
- break;
- }
-
- String mimeType;
mCursor.moveToPosition(-1);
for (int pos = 0; pos < mCursorCount; ++pos) {
@@ -176,246 +149,25 @@
Log.e(TAG, "Fail to move cursor to next pos: " + pos);
return;
}
- positions[pos] = pos;
-
// Generates a Model ID for a cursor entry that refers to a document. The Model ID is a
// unique string that can be used to identify the document referred to by the cursor.
// If the cursor is a merged cursor over multiple authorities, then prefix the ids
// with the authority to avoid collisions.
if (mCursor instanceof MergeCursor) {
- mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY) + "|" +
- getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
+ mIds[pos] = getCursorString(mCursor, RootCursorWrapper.COLUMN_AUTHORITY)
+ + "|" + getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
} else {
mIds[pos] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
}
-
- mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
- isDirs[pos] = Document.MIME_TYPE_DIR.equals(mimeType);
-
- switch(id) {
- case SortModel.SORT_DIMENSION_ID_TITLE:
- final String displayName = getCursorString(
- mCursor, Document.COLUMN_DISPLAY_NAME);
- displayNames[pos] = displayName;
- break;
- case SortModel.SORT_DIMENSION_ID_DATE:
- longValues[pos] = getLastModified(mCursor);
- break;
- case SortModel.SORT_DIMENSION_ID_SIZE:
- longValues[pos] = getCursorLong(mCursor, Document.COLUMN_SIZE);
- break;
- }
- }
-
- final SortDimension dimension = mSortModel.getDimensionById(id);
- switch (id) {
- case SortModel.SORT_DIMENSION_ID_TITLE:
- binarySort(displayNames, isDirs, positions, mIds, dimension.getSortDirection());
- break;
- case SortModel.SORT_DIMENSION_ID_DATE:
- case SortModel.SORT_DIMENSION_ID_SIZE:
- binarySort(longValues, isDirs, positions, mIds, dimension.getSortDirection());
- break;
}
// Populate the positions.
mPositions.clear();
for (int i = 0; i < mCursorCount; ++i) {
- mPositions.put(mIds[i], positions[i]);
+ mPositions.put(mIds[i], i);
}
}
- /**
- * Sorts model data. Takes three columns of index-corresponded data. The first column is the
- * sort key. Rows are sorted in ascending alphabetical order on the sort key.
- * Directories are always shown first. This code is based on TimSort.binarySort().
- *
- * @param sortKey Data is sorted in ascending alphabetical order.
- * @param isDirs Array saying whether an item is a directory or not.
- * @param positions Cursor positions to be sorted.
- * @param ids Model IDs to be sorted.
- */
- private static void binarySort(
- String[] sortKey,
- boolean[] isDirs,
- int[] positions,
- String[] ids,
- @SortDimension.SortDirection int direction) {
- final int count = positions.length;
- for (int start = 1; start < count; start++) {
- final int pivotPosition = positions[start];
- final String pivotValue = sortKey[start];
- final boolean pivotIsDir = isDirs[start];
- final String pivotId = ids[start];
-
- int left = 0;
- int right = start;
-
- while (left < right) {
- int mid = (left + right) >>> 1;
-
- // Directories always go in front.
- int compare = 0;
- final boolean rhsIsDir = isDirs[mid];
- if (pivotIsDir && !rhsIsDir) {
- compare = -1;
- } else if (!pivotIsDir && rhsIsDir) {
- compare = 1;
- } else {
- final String lhs = pivotValue;
- final String rhs = sortKey[mid];
- switch (direction) {
- case SortDimension.SORT_DIRECTION_ASCENDING:
- compare = Shared.compareToIgnoreCaseNullable(lhs, rhs);
- break;
- case SortDimension.SORT_DIRECTION_DESCENDING:
- compare = -Shared.compareToIgnoreCaseNullable(lhs, rhs);
- break;
- default:
- throw new IllegalArgumentException(
- "Unknown sorting direction: " + direction);
- }
- }
-
- if (compare < 0) {
- right = mid;
- } else {
- left = mid + 1;
- }
- }
-
- int n = start - left;
- switch (n) {
- case 2:
- positions[left + 2] = positions[left + 1];
- sortKey[left + 2] = sortKey[left + 1];
- isDirs[left + 2] = isDirs[left + 1];
- ids[left + 2] = ids[left + 1];
- case 1:
- positions[left + 1] = positions[left];
- sortKey[left + 1] = sortKey[left];
- isDirs[left + 1] = isDirs[left];
- ids[left + 1] = ids[left];
- break;
- default:
- System.arraycopy(positions, left, positions, left + 1, n);
- System.arraycopy(sortKey, left, sortKey, left + 1, n);
- System.arraycopy(isDirs, left, isDirs, left + 1, n);
- System.arraycopy(ids, left, ids, left + 1, n);
- }
-
- positions[left] = pivotPosition;
- sortKey[left] = pivotValue;
- isDirs[left] = pivotIsDir;
- ids[left] = pivotId;
- }
- }
-
- /**
- * Sorts model data. Takes four columns of index-corresponded data. The first column is the sort
- * key, and the second is an array of mime types. The rows are first bucketed by mime type
- * (directories vs documents) and then each bucket is sorted independently in descending
- * numerical order on the sort key. This code is based on TimSort.binarySort().
- *
- * @param sortKey Data is sorted in descending numerical order.
- * @param isDirs Array saying whether an item is a directory or not.
- * @param positions Cursor positions to be sorted.
- * @param ids Model IDs to be sorted.
- */
- private static void binarySort(
- long[] sortKey,
- boolean[] isDirs,
- int[] positions,
- String[] ids,
- @SortDimension.SortDirection int direction) {
- final int count = positions.length;
- for (int start = 1; start < count; start++) {
- final int pivotPosition = positions[start];
- final long pivotValue = sortKey[start];
- final boolean pivotIsDir = isDirs[start];
- final String pivotId = ids[start];
-
- int left = 0;
- int right = start;
-
- while (left < right) {
- int mid = ((left + right) >>> 1);
-
- // Directories always go in front.
- int compare = 0;
- final boolean rhsIsDir = isDirs[mid];
- if (pivotIsDir && !rhsIsDir) {
- compare = -1;
- } else if (!pivotIsDir && rhsIsDir) {
- compare = 1;
- } else {
- final long lhs = pivotValue;
- final long rhs = sortKey[mid];
- switch (direction) {
- case SortDimension.SORT_DIRECTION_ASCENDING:
- compare = Long.compare(lhs, rhs);
- break;
- case SortDimension.SORT_DIRECTION_DESCENDING:
- compare = -Long.compare(lhs, rhs);
- break;
- default:
- throw new IllegalArgumentException(
- "Unknown sorting direction: " + direction);
- }
- }
-
- // If numerical comparison yields a tie, use document ID as a tie breaker. This
- // will yield stable results even if incoming items are continually shuffling and
- // have identical numerical sort keys. One common example of this scenario is seen
- // when sorting a set of active downloads by mod time.
- if (compare == 0) {
- compare = pivotId.compareTo(ids[mid]);
- }
-
- if (compare < 0) {
- right = mid;
- } else {
- left = mid + 1;
- }
- }
-
- int n = start - left;
- switch (n) {
- case 2:
- positions[left + 2] = positions[left + 1];
- sortKey[left + 2] = sortKey[left + 1];
- isDirs[left + 2] = isDirs[left + 1];
- ids[left + 2] = ids[left + 1];
- case 1:
- positions[left + 1] = positions[left];
- sortKey[left + 1] = sortKey[left];
- isDirs[left + 1] = isDirs[left];
- ids[left + 1] = ids[left];
- break;
- default:
- System.arraycopy(positions, left, positions, left + 1, n);
- System.arraycopy(sortKey, left, sortKey, left + 1, n);
- System.arraycopy(isDirs, left, isDirs, left + 1, n);
- System.arraycopy(ids, left, ids, left + 1, n);
- }
-
- positions[left] = pivotPosition;
- sortKey[left] = pivotValue;
- isDirs[left] = pivotIsDir;
- ids[left] = pivotId;
- }
- }
-
- /**
- * @return Timestamp for the given document. Some docs (e.g. active downloads) have a null
- * timestamp - these will be replaced with MAX_LONG so that such files get sorted to the top
- * when sorting descending by date.
- */
- long getLastModified(Cursor cursor) {
- long l = getCursorLong(mCursor, Document.COLUMN_LAST_MODIFIED);
- return (l == -1) ? Long.MAX_VALUE : l;
- }
-
public @Nullable Cursor getItem(String modelId) {
Integer pos = mPositions.get(modelId);
if (pos == null) {
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index d6c4f42..feb70d0 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -19,6 +19,7 @@
import static com.android.documentsui.base.Shared.DEBUG;
import android.app.Activity;
+import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
@@ -26,10 +27,9 @@
import android.util.Log;
import com.android.documentsui.AbstractActionHandler;
+import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.DocumentsApplication;
-import com.android.documentsui.GetRootDocumentTask;
import com.android.documentsui.Metrics;
-import com.android.documentsui.ProviderExecutor;
import com.android.documentsui.base.ConfirmationCallback;
import com.android.documentsui.base.ConfirmationCallback.Result;
import com.android.documentsui.base.DocumentInfo;
@@ -47,6 +47,7 @@
import com.android.documentsui.dirlist.MultiSelectManager;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.files.ActionHandler.Addons;
+import com.android.documentsui.roots.GetRootDocumentTask;
import com.android.documentsui.roots.RootsAccess;
import com.android.documentsui.services.FileOperation;
import com.android.documentsui.services.FileOperationService;
@@ -77,13 +78,14 @@
T activity,
State state,
RootsAccess roots,
+ DocumentsAccess docs,
Lookup<String, Executor> executors,
DialogController dialogs,
FragmentTuner tuner,
DocumentClipper clipper,
ClipStore clipStore) {
- super(activity, state, roots, executors);
+ super(activity, state, roots, docs, executors);
mDialogs = dialogs;
mTuner = tuner;
@@ -101,7 +103,7 @@
mActivity::isDestroyed,
(DocumentInfo doc) -> mClipper.copyFromClipData(
root, doc, data, mDialogs::showFileOperationFailures)
- ).executeOnExecutor(ProviderExecutor.forAuthority(root.authority));
+ ).executeOnExecutor(mExecutors.lookup(root.authority));
return true;
}
@@ -120,7 +122,7 @@
mActivity,
mActivity::isDestroyed,
(DocumentInfo doc) -> pasteIntoFolder(root, doc)
- ).executeOnExecutor(ProviderExecutor.forAuthority(root.authority));
+ ).executeOnExecutor(mExecutors.lookup(root.authority));
}
private void pasteIntoFolder(RootInfo root, DocumentInfo doc) {
@@ -145,7 +147,7 @@
}
if (mTuner.isDocumentEnabled(doc.mimeType, doc.flags)) {
- mActivity.onDocumentPicked(doc, mConfig.model);
+ onDocumentPicked(doc);
mConfig.selectionMgr.clearSelection();
return true;
}
@@ -155,7 +157,7 @@
@Override
public boolean viewDocument(DocumentDetails details) {
DocumentInfo doc = mConfig.model.getDocument(details.getModelId());
- return mActivity.viewDocument(doc);
+ return viewDocument(doc);
}
@Override
@@ -164,7 +166,7 @@
if (doc.isContainer()) {
return false;
}
- return mActivity.previewDocument(doc, mConfig.model);
+ return previewDocument(doc);
}
@Override
@@ -174,6 +176,7 @@
assert(!selected.isEmpty());
final DocumentInfo srcParent = mState.stack.peek();
+ assert(srcParent != null);
// Model must be accessed in UI thread, since underlying cursor is not threadsafe.
List<DocumentInfo> docs = model.getDocuments(selected);
@@ -268,8 +271,7 @@
if (Intent.ACTION_VIEW.equals(intent.getAction())) {
Uri uri = intent.getData();
assert(uri != null);
- new OpenUriForViewTask<>(mActivity, mState).executeOnExecutor(
- ProviderExecutor.forAuthority(uri.getAuthority()), uri);
+ loadDocument(uri);
return true;
}
@@ -290,6 +292,163 @@
return false;
}
+ public void showChooserForDoc(DocumentInfo doc) {
+ assert(!doc.isContainer());
+
+ if (manageDocument(doc)) {
+ Log.w(TAG, "Open with is not yet supported for managed doc.");
+ return;
+ }
+
+ Intent intent = Intent.createChooser(buildViewIntent(doc), null);
+ try {
+ mActivity.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ mDialogs.showNoApplicationFound();
+ }
+ }
+
+ public void onDocumentPicked(DocumentInfo doc) {
+ if (doc.isContainer()) {
+ mActivity.openContainerDocument(doc);
+ return;
+ }
+
+ if (manageDocument(doc)) {
+ return;
+ }
+
+ if (previewDocument(doc)) {
+ return;
+ }
+
+ viewDocument(doc);
+ }
+
+ public boolean viewDocument(DocumentInfo doc) {
+ if (doc.isPartial()) {
+ Log.w(TAG, "Can't view partial file.");
+ return false;
+ }
+
+ if (doc.isContainer()) {
+ mActivity.openContainerDocument(doc);
+ return true;
+ }
+
+ // this is a redundant check.
+ if (manageDocument(doc)) {
+ return true;
+ }
+
+ // Fall back to traditional VIEW action...
+ Intent intent = buildViewIntent(doc);
+ if (DEBUG && intent.getClipData() != null) {
+ Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
+ }
+
+ try {
+ mActivity.startActivity(intent);
+ return true;
+ } catch (ActivityNotFoundException e) {
+ mDialogs.showNoApplicationFound();
+ }
+ return false;
+ }
+
+ public boolean previewDocument(DocumentInfo doc) {
+ if (doc.isPartial()) {
+ Log.w(TAG, "Can't view partial file.");
+ return false;
+ }
+
+ Intent intent = new QuickViewIntentBuilder(
+ mActivity.getPackageManager(),
+ mActivity.getResources(),
+ doc,
+ mConfig.model).build();
+
+ if (intent != null) {
+ // TODO: un-work around issue b/24963914. Should be fixed soon.
+ try {
+ mActivity.startActivity(intent);
+ return true;
+ } catch (SecurityException e) {
+ // Carry on to regular view mode.
+ Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
+ }
+ }
+
+ return false;
+ }
+
+ private boolean manageDocument(DocumentInfo doc) {
+ if (isManagedDownload(doc)) {
+ // First try managing the document; we expect manager to filter
+ // based on authority, so we don't grant.
+ Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
+ manage.setData(doc.derivedUri);
+ try {
+ mActivity.startActivity(manage);
+ return true;
+ } catch (ActivityNotFoundException ex) {
+ // Fall back to regular handling.
+ }
+ }
+
+ return false;
+ }
+
+ private boolean isManagedDownload(DocumentInfo doc) {
+ // Anything on downloads goes through the back through downloads manager
+ // (that's the MANAGE_DOCUMENT bit).
+ // This is done for two reasons:
+ // 1) The file in question might be a failed/queued or otherwise have some
+ // specialized download handling.
+ // 2) For APKs, the download manager will add on some important security stuff
+ // like origin URL.
+ // 3) For partial files, the download manager will offer to restart/retry downloads.
+
+ // All other files not on downloads, event APKs, would get no benefit from this
+ // treatment, thusly the "isDownloads" check.
+
+ // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
+ // files in archives. Also, if the activity is already browsing a ZIP from downloads,
+ // then skip MANAGE_DOCUMENTS.
+ if (Intent.ACTION_VIEW.equals(mActivity.getIntent().getAction())
+ && mState.stack.size() > 1) {
+ // viewing the contents of an archive.
+ return false;
+ }
+
+ // managament is only supported in downloads.
+ if (mActivity.getCurrentRoot().isDownloads()) {
+ // and only and only on APKs or partial files.
+ return "application/vnd.android.package-archive".equals(doc.mimeType)
+ || doc.isPartial();
+ }
+
+ return false;
+ }
+
+ private Intent buildViewIntent(DocumentInfo doc) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(doc.derivedUri, doc.mimeType);
+
+ // Downloads has traditionally added the WRITE permission
+ // in the TrampolineActivity. Since this behavior is long
+ // established, we set the same permission for non-managed files
+ // This ensures consistent behavior between the Downloads root
+ // and other roots.
+ int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
+ if (doc.isWriteSupported()) {
+ flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
+ }
+ intent.setFlags(flags);
+
+ return intent;
+ }
+
ActionHandler<T> reset(Model model, MultiSelectManager selectionMgr) {
mConfig.reset(model, selectionMgr);
return this;
@@ -302,7 +461,8 @@
public void reset(Model model, MultiSelectManager selectionMgr) {
assert(model != null);
- assert(selectionMgr != null);
+ // Allowed to be null in testing, for now.
+ // assert(selectionMgr != null);
this.model = model;
this.selectionMgr = selectionMgr;
@@ -310,7 +470,5 @@
}
public interface Addons extends CommonAddons {
- boolean viewDocument(DocumentInfo doc);
- boolean previewDocument(DocumentInfo doc, Model model);
}
}
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 2d1ed94..f3dfd77 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -21,13 +21,10 @@
import android.app.Activity;
import android.app.FragmentManager;
-import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
-import android.provider.DocumentsContract;
-import android.support.design.widget.Snackbar;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.KeyEvent;
@@ -36,6 +33,7 @@
import android.view.MenuItem;
import com.android.documentsui.BaseActivity;
+import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.FocusManager;
import com.android.documentsui.MenuManager.DirectoryDetails;
@@ -57,7 +55,6 @@
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.sidebar.RootsFragment;
import com.android.documentsui.ui.DialogController;
-import com.android.documentsui.ui.Snackbars;
import java.util.ArrayList;
import java.util.Arrays;
@@ -104,6 +101,7 @@
this,
mState,
mRoots,
+ DocumentsAccess.create(this),
ProviderExecutor::forAuthority,
mDialogs,
mTuner,
@@ -252,22 +250,13 @@
throw new UnsupportedOperationException();
}
+ /**
+ * @deprecated use {@link ActionHandler#onDocumentPicked(DocumentInfo)}
+ * @param doc
+ */
@Override
- public void onDocumentPicked(DocumentInfo doc, Model model) {
- if (doc.isContainer()) {
- openContainerDocument(doc);
- return;
- }
-
- if (manageDocument(doc)) {
- return;
- }
-
- if (previewDocument(doc, model)) {
- return;
- }
-
- viewDocument(doc);
+ public void onDocumentPicked(DocumentInfo doc) {
+ mActions.onDocumentPicked(doc);
}
@Override
@@ -283,138 +272,13 @@
openContainerDocument(doc);
}
+ /**
+ * @deprecated use {@link ActionHandler#showChooserForDoc(DocumentInfo)}
+ * @param doc
+ */
+ @Deprecated
public void showChooserForDoc(DocumentInfo doc) {
- assert(!doc.isContainer());
-
- if (manageDocument(doc)) {
- Log.w(TAG, "Open with is not yet supported for managed doc.");
- return;
- }
-
- Intent intent = Intent.createChooser(buildViewIntent(doc), null);
- try {
- startActivity(intent);
- } catch (ActivityNotFoundException e) {
- Snackbars.makeSnackbar(
- this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
- }
- }
-
- // TODO: Move to ActionHandler.
- @Override
- public boolean viewDocument(DocumentInfo doc) {
- if (doc.isPartial()) {
- Log.w(TAG, "Can't view partial file.");
- return false;
- }
-
- if (doc.isContainer()) {
- openContainerDocument(doc);
- return true;
- }
-
- // this is a redundant check.
- if (manageDocument(doc)) {
- return true;
- }
-
- // Fall back to traditional VIEW action...
- Intent intent = buildViewIntent(doc);
- if (DEBUG && intent.getClipData() != null) {
- Log.d(TAG, "Starting intent w/ clip data: " + intent.getClipData());
- }
-
- try {
- startActivity(intent);
- return true;
- } catch (ActivityNotFoundException e) {
- Snackbars.makeSnackbar(
- this, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
- }
- return false;
- }
-
- // TODO: Move to ActionHandler.
- @Override
- public boolean previewDocument(DocumentInfo doc, Model model) {
- if (doc.isPartial()) {
- Log.w(TAG, "Can't view partial file.");
- return false;
- }
-
- Intent intent = new QuickViewIntentBuilder(
- getPackageManager(), getResources(), doc, model).build();
-
- if (intent != null) {
- // TODO: un-work around issue b/24963914. Should be fixed soon.
- try {
- startActivity(intent);
- return true;
- } catch (SecurityException e) {
- // Carry on to regular view mode.
- Log.e(TAG, "Caught security error: " + e.getLocalizedMessage());
- }
- }
-
- return false;
- }
-
- private boolean manageDocument(DocumentInfo doc) {
- if (isManagedDocument(doc)) {
- // First try managing the document; we expect manager to filter
- // based on authority, so we don't grant.
- Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT);
- manage.setData(doc.derivedUri);
- try {
- startActivity(manage);
- return true;
- } catch (ActivityNotFoundException ex) {
- // Fall back to regular handling.
- }
- }
-
- return false;
- }
-
- private boolean isManagedDocument(DocumentInfo doc) {
- // Anything on downloads goes through the back through downloads manager
- // (that's the MANAGE_DOCUMENT bit).
- // This is done for two reasons:
- // 1) The file in question might be a failed/queued or otherwise have some
- // specialized download handling.
- // 2) For APKs, the download manager will add on some important security stuff
- // like origin URL.
- // All other files not on downloads, event APKs, would get no benefit from this
- // treatment, thusly the "isDownloads" check.
-
- // Launch MANAGE_DOCUMENTS only for the root level files, so it's not called for
- // files in archives. Also, if the activity is already browsing a ZIP from downloads,
- // then skip MANAGE_DOCUMENTS.
- // Oh, and only launch for APKs.
- final boolean isViewing = Intent.ACTION_VIEW.equals(getIntent().getAction());
- final boolean isInArchive = mState.stack.size() > 1;
- return getCurrentRoot().isDownloads()
- && "application/vnd.android.package-archive".equals(doc.mimeType)
- && !isInArchive
- && !isViewing;
- }
-
- private Intent buildViewIntent(DocumentInfo doc) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setDataAndType(doc.derivedUri, doc.mimeType);
-
- // Downloads has traditionally added the WRITE permission
- // in the TrampolineActivity. Since this behavior is long
- // established, we set the same permission for non-managed files
- // This ensures consistent behavior between the Downloads root
- // and other roots.
- int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
- if (doc.isWriteSupported()) {
- flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
- }
- intent.setFlags(flags);
-
- return intent;
+ mActions.showChooserForDoc(doc);
}
@Override
diff --git a/src/com/android/documentsui/files/OpenUriForViewTask.java b/src/com/android/documentsui/files/OpenUriForViewTask.java
index f8424c1..2a830c5 100644
--- a/src/com/android/documentsui/files/OpenUriForViewTask.java
+++ b/src/com/android/documentsui/files/OpenUriForViewTask.java
@@ -20,7 +20,7 @@
import android.util.Log;
import com.android.documentsui.AbstractActionHandler.CommonAddons;
-import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.PairedTask;
import com.android.documentsui.base.RootInfo;
@@ -28,44 +28,58 @@
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.roots.RootsAccess;
-import java.io.FileNotFoundException;
import java.util.Collection;
+import javax.annotation.Nullable;
+
/**
* Builds a stack for the specific Uris. Multi roots are not supported, as it's impossible
* to know which root to select. Also, the stack doesn't contain intermediate directories.
* It's primarly used for opening ZIP archives from Downloads app.
*/
-final class OpenUriForViewTask<T extends Activity & CommonAddons>
- extends PairedTask<T, Uri, Void> {
+public final class OpenUriForViewTask<T extends Activity & CommonAddons>
+ extends PairedTask<T, Void, Void> {
+ private static final String TAG = "OpenUriForViewTask";
+
+ private final T mActivity;
private final State mState;
- public OpenUriForViewTask(T activity, State state) {
+ private final RootsAccess mRoots;
+ private final DocumentsAccess mDocs;
+ private final Uri mUri;
+
+ public OpenUriForViewTask(
+ T activity, State state, RootsAccess roots, DocumentsAccess docs, Uri uri) {
super(activity);
+ mActivity = activity;
mState = state;
+ mRoots = roots;
+ mDocs = docs;
+ mUri = uri;
}
@Override
- public Void run(Uri... params) {
- final Uri uri = params[0];
+ public Void run(Void... params) {
- final RootsAccess rootsCache = DocumentsApplication.getRootsCache(mOwner);
- final String authority = uri.getAuthority();
+ final String authority = mUri.getAuthority();
+ final Collection<RootInfo> roots = mRoots.getRootsForAuthorityBlocking(authority);
- final Collection<RootInfo> roots =
- rootsCache.getRootsForAuthorityBlocking(authority);
if (roots.isEmpty()) {
- Log.e(FilesActivity.TAG, "Failed to find root for the requested Uri: " + uri);
+ Log.e(TAG, "Failed to find root for the requested Uri: " + mUri);
return null;
}
+ assert(mState.stack.isEmpty());
+
+ // NOTE: There's no guarantee that this root will be the correct root for the doc.
final RootInfo root = roots.iterator().next();
mState.stack.root = root;
- mState.stack.add(root.getRootDocumentBlocking(mOwner));
- try {
- mState.stack.add(DocumentInfo.fromUri(mOwner.getContentResolver(), uri));
- } catch (FileNotFoundException e) {
- Log.e(FilesActivity.TAG, "Failed to resolve DocumentInfo from Uri: " + uri);
+ mState.stack.add(mDocs.getRootDocument(root));
+ @Nullable DocumentInfo doc = mDocs.getDocument(mUri);
+ if (doc == null) {
+ Log.e(TAG, "Failed to resolve DocumentInfo from Uri: " + mUri);
+ } else {
+ mState.stack.add(doc);
}
return null;
@@ -73,6 +87,6 @@
@Override
public void finish(Void result) {
- mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
+ mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/documentsui/files/QuickViewIntentBuilder.java b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
index 75bb2c4..995d280 100644
--- a/src/com/android/documentsui/files/QuickViewIntentBuilder.java
+++ b/src/com/android/documentsui/files/QuickViewIntentBuilder.java
@@ -46,23 +46,34 @@
/**
* Provides support for gather a list of quick-viewable files into a quick view intent.
*/
-final class QuickViewIntentBuilder {
+public final class QuickViewIntentBuilder {
+
+ // trusted quick view package can be set via system property on debug builds.
+ // Unfortunately when the value is set, it interferes with testing.
+ // For that reason when trusted quick view package is set to this magic value
+ // we won't the system property. It's a gross hack, but stuff's gotta get done.
+ public static final String IGNORE_DEBUG_PROP = "*disabled*";
private static final String TAG = "QuickViewIntentBuilder";
private final DocumentInfo mDocument;
private final Model mModel;
- private final PackageManager mPkgManager;
+ private final PackageManager mPackageMgr;
private final Resources mResources;
public QuickViewIntentBuilder(
- PackageManager pkgManager,
+ PackageManager packageMgr,
Resources resources,
DocumentInfo doc,
Model model) {
- mPkgManager = pkgManager;
+ assert(packageMgr != null);
+ assert(resources != null);
+ assert(doc != null);
+ assert(model != null);
+
+ mPackageMgr = packageMgr;
mResources = resources;
mDocument = doc;
mModel = model;
@@ -121,9 +132,15 @@
private String getQuickViewPackage() {
String resValue = mResources.getString(R.string.trusted_quick_viewer_package);
- if (Build.IS_DEBUGGABLE ) {
- // Allow users of debug devices to override default quick viewer
- // for the purposes of testing.
+
+ // Allow automated tests to hard-disable quick viewing.
+ if (IGNORE_DEBUG_PROP.equals(resValue)) {
+ return "";
+ }
+
+ // Allow users of debug devices to override default quick viewer
+ // for the purposes of testing.
+ if (Build.IS_DEBUGGABLE) {
return android.os.SystemProperties.get("debug.quick_viewer", resValue);
}
return resValue;
@@ -196,6 +213,6 @@
private boolean hasRegisteredHandler(Intent intent) {
// Try to resolve the intent. If a matching app isn't installed, it won't resolve.
- return intent.resolveActivity(mPkgManager) != null;
+ return intent.resolveActivity(mPackageMgr) != null;
}
}
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index 59d0b48..369b489 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -27,6 +27,7 @@
import android.util.Log;
import com.android.documentsui.AbstractActionHandler;
+import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.Metrics;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
@@ -58,10 +59,11 @@
T activity,
State state,
RootsAccess roots,
+ DocumentsAccess docs,
Lookup<String, Executor> executors,
FragmentTuner tuner) {
- super(activity, state, roots, executors);
+ super(activity, state, roots, docs, executors);
mTuner = tuner;
mConfig = new Config();
@@ -134,7 +136,7 @@
}
if (mTuner.isDocumentEnabled(doc.mimeType, doc.flags)) {
- mActivity.onDocumentPicked(doc, mConfig.model);
+ mActivity.onDocumentPicked(doc);
mConfig.selectionMgr.clearSelection();
return true;
}
@@ -162,5 +164,6 @@
public interface Addons extends CommonAddons {
void onAppPicked(ResolveInfo info);
+ void onDocumentPicked(DocumentInfo doc);
}
}
diff --git a/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java b/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java
index 3dadca4..bb22e08 100644
--- a/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java
+++ b/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java
@@ -106,4 +106,4 @@
mState.external = mExternal;
mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
}
-}
\ No newline at end of file
+}
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 1f2f69f..962d3a9 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -43,6 +43,7 @@
import android.view.Menu;
import com.android.documentsui.BaseActivity;
+import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.FocusManager;
import com.android.documentsui.MenuManager.DirectoryDetails;
@@ -91,6 +92,7 @@
this,
mState,
DocumentsApplication.getRootsCache(this),
+ DocumentsAccess.create(this),
ProviderExecutor::forAuthority,
mTuner);
@@ -303,7 +305,7 @@
}
@Override
- public void onDocumentPicked(DocumentInfo doc, Model model) {
+ public void onDocumentPicked(DocumentInfo doc) {
final FragmentManager fm = getFragmentManager();
if (doc.isContainer()) {
openContainerDocument(doc);
diff --git a/src/com/android/documentsui/picker/PickFragment.java b/src/com/android/documentsui/picker/PickFragment.java
index a389942..8eb3b4c 100644
--- a/src/com/android/documentsui/picker/PickFragment.java
+++ b/src/com/android/documentsui/picker/PickFragment.java
@@ -32,9 +32,6 @@
import com.android.documentsui.BaseActivity;
import com.android.documentsui.R;
-import com.android.documentsui.R.id;
-import com.android.documentsui.R.layout;
-import com.android.documentsui.R.string;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.services.FileOperationService.OpType;
@@ -45,6 +42,27 @@
public class PickFragment extends Fragment {
public static final String TAG = "PickFragment";
+ private static final String ACTION_KEY = "action";
+ private static final String COPY_OPERATION_SUBTYPE_KEY = "copyOperationSubType";
+ private static final String PICK_TARGET_KEY = "pickTarget";
+
+ private final View.OnClickListener mPickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final PickActivity activity = PickActivity.get(PickFragment.this);
+ activity.onPickRequested(mPickTarget);
+ }
+ };
+
+ private final View.OnClickListener mCancelListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final BaseActivity activity = BaseActivity.get(PickFragment.this);
+ activity.setResult(Activity.RESULT_CANCELED);
+ activity.finish();
+ }
+ };
+
private int mAction;
// Only legal values are OPERATION_COPY, OPERATION_MOVE, and unset (OPERATION_UNKNOWN).
private @OpType int mCopyOperationSubType = OPERATION_UNKNOWN;
@@ -84,22 +102,26 @@
return mContainer;
}
- private View.OnClickListener mPickListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- final PickActivity activity = PickActivity.get(PickFragment.this);
- activity.onPickRequested(mPickTarget);
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ if (savedInstanceState != null) {
+ // Restore status
+ mAction = savedInstanceState.getInt(ACTION_KEY);
+ mCopyOperationSubType =
+ savedInstanceState.getInt(COPY_OPERATION_SUBTYPE_KEY);
+ mPickTarget = savedInstanceState.getParcelable(PICK_TARGET_KEY);
+ updateView();
}
- };
+ }
- private View.OnClickListener mCancelListener = new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- final BaseActivity activity = BaseActivity.get(PickFragment.this);
- activity.setResult(Activity.RESULT_CANCELED);
- activity.finish();
- }
- };
+ @Override
+ public void onSaveInstanceState(final Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(ACTION_KEY, mAction);
+ outState.putInt(COPY_OPERATION_SUBTYPE_KEY, mCopyOperationSubType);
+ outState.putParcelable(PICK_TARGET_KEY, mPickTarget);
+ }
/**
* @param action Which action defined in State is the picker shown for.
diff --git a/src/com/android/documentsui/GetRootDocumentTask.java b/src/com/android/documentsui/roots/GetRootDocumentTask.java
similarity index 96%
rename from src/com/android/documentsui/GetRootDocumentTask.java
rename to src/com/android/documentsui/roots/GetRootDocumentTask.java
index e56b110..1503ae1 100644
--- a/src/com/android/documentsui/GetRootDocumentTask.java
+++ b/src/com/android/documentsui/roots/GetRootDocumentTask.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.documentsui;
+package com.android.documentsui.roots;
import android.annotation.Nullable;
import android.app.Activity;
@@ -22,6 +22,7 @@
import android.content.Context;
import android.util.Log;
+import com.android.documentsui.TimeoutTask;
import com.android.documentsui.base.CheckedTask;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.RootInfo;
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 1174503..a250a8d 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -49,7 +49,6 @@
import com.android.documentsui.ActionHandler;
import com.android.documentsui.BaseActivity;
import com.android.documentsui.DocumentsApplication;
-import com.android.documentsui.GetRootDocumentTask;
import com.android.documentsui.ItemDragListener;
import com.android.documentsui.R;
import com.android.documentsui.base.BooleanConsumer;
@@ -59,6 +58,7 @@
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
+import com.android.documentsui.roots.GetRootDocumentTask;
import com.android.documentsui.roots.RootsCache;
import com.android.documentsui.roots.RootsLoader;
diff --git a/src/com/android/documentsui/sorting/DropdownSortWidgetController.java b/src/com/android/documentsui/sorting/DropdownSortWidgetController.java
index 7977591..21c182c 100644
--- a/src/com/android/documentsui/sorting/DropdownSortWidgetController.java
+++ b/src/com/android/documentsui/sorting/DropdownSortWidgetController.java
@@ -21,18 +21,14 @@
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
-import android.view.View.OnClickListener;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import com.android.documentsui.R;
import com.android.documentsui.sorting.SortController.WidgetController;
-import com.android.documentsui.sorting.SortDimension;
import com.android.documentsui.sorting.SortDimension.SortDirection;
-import com.android.documentsui.sorting.SortModel;
import com.android.documentsui.sorting.SortModel.SortDimensionId;
-import com.android.documentsui.sorting.SortModel.UpdateListener;
import com.android.documentsui.sorting.SortModel.UpdateType;
/**
@@ -49,24 +45,23 @@
private final PopupMenu mMenu;
private final ImageView mArrow;
- private final OnClickListener mDimensionButtonClickListener = this::showMenu;
- private final OnClickListener mArrowClickListener = this::onChangeDirection;
- private final UpdateListener mUpdateListener = this::onModelUpdate;
-
public DropdownSortWidgetController(SortModel model, View widget) {
mModel = model;
mWidget = widget;
mDimensionButton = (TextView) mWidget.findViewById(R.id.sort_dimen_dropdown);
+ mDimensionButton.setOnClickListener(this::showMenu);
+
mMenu = new PopupMenu(widget.getContext(), mDimensionButton, Gravity.END | Gravity.TOP);
mMenu.setOnMenuItemClickListener(this::onSelectDimension);
mArrow = (ImageView) mWidget.findViewById(R.id.sort_arrow);
+ mArrow.setOnClickListener(this::onChangeDirection);
populateMenuItems();
onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED);
- mModel.addListener(mUpdateListener);
+ mModel.addListener(this::onModelUpdate);
}
@Override
@@ -92,10 +87,6 @@
private void onModelUpdate(SortModel model, @UpdateType int updateType) {
final @SortDimensionId int sortedId = model.getSortedDimensionId();
- if ((updateType & SortModel.UPDATE_TYPE_STATUS) != 0) {
- setEnabled(mModel.isSortEnabled());
- }
-
if ((updateType & SortModel.UPDATE_TYPE_VISIBILITY) != 0) {
updateVisibility();
}
@@ -106,17 +97,6 @@
}
}
- private void setEnabled(boolean enabled) {
- if (enabled) {
- mDimensionButton.setOnClickListener(mDimensionButtonClickListener);
- mArrow.setOnClickListener(mArrowClickListener);
- } else {
- mMenu.dismiss();
- mDimensionButton.setOnClickListener(null);
- mArrow.setOnClickListener(null);
- }
- }
-
private void updateVisibility() {
Menu menu = mMenu.getMenu();
diff --git a/src/com/android/documentsui/sorting/SortModel.java b/src/com/android/documentsui/sorting/SortModel.java
index 793dd60..a5c8459 100644
--- a/src/com/android/documentsui/sorting/SortModel.java
+++ b/src/com/android/documentsui/sorting/SortModel.java
@@ -20,6 +20,7 @@
import android.annotation.IntDef;
import android.annotation.Nullable;
+import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsContract.Document;
@@ -59,7 +60,6 @@
@IntDef(flag = true, value = {
UPDATE_TYPE_NONE,
UPDATE_TYPE_UNSPECIFIED,
- UPDATE_TYPE_STATUS,
UPDATE_TYPE_VISIBILITY,
UPDATE_TYPE_SORTING
})
@@ -70,18 +70,14 @@
*/
public static final int UPDATE_TYPE_NONE = 0;
/**
- * Indicates the status of sorting has changed, i.e. whether soring is enabled.
- */
- public static final int UPDATE_TYPE_STATUS = 1;
- /**
* Indicates the visibility of at least one dimension has changed.
*/
- public static final int UPDATE_TYPE_VISIBILITY = 1 << 1;
+ public static final int UPDATE_TYPE_VISIBILITY = 1;
/**
* Indicates the sorting order has changed, either because the sorted dimension has changed or
* the sort direction has changed.
*/
- public static final int UPDATE_TYPE_SORTING = 1 << 2;
+ public static final int UPDATE_TYPE_SORTING = 1 << 1;
/**
* Anything can be changed if the type is unspecified.
*/
@@ -98,8 +94,6 @@
private boolean mIsUserSpecified = false;
private @Nullable SortDimension mSortedDimension;
- private boolean mIsSortEnabled = true;
-
public SortModel(Collection<SortDimension> columns) {
mDimensions = new SparseArray<>(columns.size());
@@ -145,16 +139,6 @@
: SortDimension.SORT_DIRECTION_NONE;
}
- public void setSortEnabled(boolean enabled) {
- mIsSortEnabled = enabled;
-
- notifyListeners(UPDATE_TYPE_STATUS);
- }
-
- public boolean isSortEnabled() {
- return mIsSortEnabled;
- }
-
/**
* Sort by the default direction of the given dimension if user has never specified any sort
* direction before.
@@ -181,10 +165,6 @@
* @param direction the direction to sort docs in
*/
public void sortByUser(int dimensionId, @SortDirection int direction) {
- if (!mIsSortEnabled) {
- throw new IllegalStateException("Sort is not enabled.");
- }
-
SortDimension dimension = mDimensions.get(dimensionId);
if (dimension == null) {
throw new IllegalArgumentException("Unknown column id: " + dimensionId);
@@ -239,6 +219,14 @@
notifyListeners(UPDATE_TYPE_VISIBILITY);
}
+ public Cursor sortCursor(Cursor cursor) {
+ if (mSortedDimension != null) {
+ return new SortingCursorWrapper(cursor, mSortedDimension);
+ } else {
+ return cursor;
+ }
+ }
+
public @Nullable String getDocumentSortQuery() {
final int id = getSortedDimensionId();
final String columnName;
@@ -290,17 +278,6 @@
mListeners.remove(listener);
}
- public void clearSortDirection() {
- if (mSortedDimension != null) {
- mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
- mSortedDimension = null;
- }
-
- mIsUserSpecified = false;
-
- sortOnDefault();
- }
-
/**
* Sort by default dimension and direction if there is no history of user specifying a sort
* order.
@@ -340,7 +317,6 @@
}
return mDefaultDimensionId == other.mDefaultDimensionId
- && mIsSortEnabled == other.mIsSortEnabled
&& (mSortedDimension == other.mSortedDimension
|| mSortedDimension.equals(other.mSortedDimension));
}
@@ -349,8 +325,7 @@
public String toString() {
return new StringBuilder()
.append("SortModel{")
- .append("enabled=").append(mIsSortEnabled)
- .append(", dimensions=").append(mDimensions)
+ .append("dimensions=").append(mDimensions)
.append(", defaultDimensionId=").append(mDefaultDimensionId)
.append(", sortedDimension=").append(mSortedDimension)
.append("}")
@@ -370,7 +345,6 @@
}
out.writeInt(mDefaultDimensionId);
- out.writeInt(mIsSortEnabled ? 1 : 0);
out.writeInt(getSortedDimensionId());
}
@@ -386,7 +360,6 @@
SortModel model = new SortModel(columns);
model.mDefaultDimensionId = in.readInt();
- model.mIsSortEnabled = (in.readInt() == 1);
model.mSortedDimension = model.getDimensionById(in.readInt());
return model;
diff --git a/src/com/android/documentsui/sorting/SortingCursorWrapper.java b/src/com/android/documentsui/sorting/SortingCursorWrapper.java
new file mode 100644
index 0000000..89869d9
--- /dev/null
+++ b/src/com/android/documentsui/sorting/SortingCursorWrapper.java
@@ -0,0 +1,330 @@
+/*
+ * 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.sorting;
+
+import static com.android.documentsui.base.DocumentInfo.getCursorLong;
+import static com.android.documentsui.base.DocumentInfo.getCursorString;
+
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.provider.DocumentsContract.Document;
+
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.sorting.SortModel.SortDimensionId;
+
+/**
+ * Cursor wrapper that presents a sorted view of the underlying cursor. Handles
+ * common {@link Document} sorting modes, such as ordering directories first.
+ */
+class SortingCursorWrapper extends AbstractCursor {
+ private final Cursor mCursor;
+
+ private final int[] mPosition;
+
+ public SortingCursorWrapper(Cursor cursor, SortDimension dimension) {
+ mCursor = cursor;
+
+ final int count = cursor.getCount();
+ mPosition = new int[count];
+ boolean[] isDirs = new boolean[count];
+ String[] displayNames = null;
+ long[] longValues = null;
+ String[] ids = null;
+
+ final @SortDimensionId int id = dimension.getId();
+ switch (id) {
+ case SortModel.SORT_DIMENSION_ID_TITLE:
+ displayNames = new String[count];
+ break;
+ case SortModel.SORT_DIMENSION_ID_DATE:
+ case SortModel.SORT_DIMENSION_ID_SIZE:
+ longValues = new long[count];
+ ids = new String[count];
+ break;
+ }
+
+ cursor.moveToPosition(-1);
+ for (int i = 0; i < count; i++) {
+ cursor.moveToNext();
+ mPosition[i] = i;
+
+ final String mimeType = getCursorString(mCursor, Document.COLUMN_MIME_TYPE);
+ isDirs[i] = Document.MIME_TYPE_DIR.equals(mimeType);
+
+ switch(id) {
+ case SortModel.SORT_DIMENSION_ID_TITLE:
+ final String displayName = getCursorString(
+ mCursor, Document.COLUMN_DISPLAY_NAME);
+ displayNames[i] = displayName;
+ break;
+ case SortModel.SORT_DIMENSION_ID_DATE:
+ longValues[i] = getLastModified(mCursor);
+ ids[i] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
+ break;
+ case SortModel.SORT_DIMENSION_ID_SIZE:
+ longValues[i] = getCursorLong(mCursor, Document.COLUMN_SIZE);
+ ids[i] = getCursorString(mCursor, Document.COLUMN_DOCUMENT_ID);
+ break;
+ }
+
+ }
+
+ switch (id) {
+ case SortModel.SORT_DIMENSION_ID_TITLE:
+ binarySort(displayNames, isDirs, mPosition, dimension.getSortDirection());
+ break;
+ case SortModel.SORT_DIMENSION_ID_DATE:
+ case SortModel.SORT_DIMENSION_ID_SIZE:
+ binarySort(longValues, isDirs, mPosition, ids, dimension.getSortDirection());
+ break;
+ }
+
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ mCursor.close();
+ }
+
+ @Override
+ public boolean onMove(int oldPosition, int newPosition) {
+ return mCursor.moveToPosition(mPosition[newPosition]);
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mCursor.getColumnNames();
+ }
+
+ @Override
+ public int getCount() {
+ return mCursor.getCount();
+ }
+
+ @Override
+ public double getDouble(int column) {
+ return mCursor.getDouble(column);
+ }
+
+ @Override
+ public float getFloat(int column) {
+ return mCursor.getFloat(column);
+ }
+
+ @Override
+ public int getInt(int column) {
+ return mCursor.getInt(column);
+ }
+
+ @Override
+ public long getLong(int column) {
+ return mCursor.getLong(column);
+ }
+
+ @Override
+ public short getShort(int column) {
+ return mCursor.getShort(column);
+ }
+
+ @Override
+ public String getString(int column) {
+ return mCursor.getString(column);
+ }
+
+ @Override
+ public int getType(int column) {
+ return mCursor.getType(column);
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ return mCursor.isNull(column);
+ }
+
+ /**
+ * @return Timestamp for the given document. Some docs (e.g. active downloads) have a null
+ * timestamp - these will be replaced with MAX_LONG so that such files get sorted to the top
+ * when sorting descending by date.
+ */
+ private static long getLastModified(Cursor cursor) {
+ long l = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
+ return (l == -1) ? Long.MAX_VALUE : l;
+ }
+
+ /**
+ * Borrowed from TimSort.binarySort(), but modified to sort two column
+ * dataset.
+ */
+ private static void binarySort(
+ String[] sortKey,
+ boolean[] isDirs,
+ int[] positions,
+ @SortDimension.SortDirection int direction) {
+ final int count = positions.length;
+ for (int start = 1; start < count; start++) {
+ final int pivotPosition = positions[start];
+ final String pivotValue = sortKey[start];
+ final boolean pivotIsDir = isDirs[start];
+
+ int left = 0;
+ int right = start;
+
+ while (left < right) {
+ int mid = (left + right) >>> 1;
+
+ // Directories always go in front.
+ int compare = 0;
+ final boolean rhsIsDir = isDirs[mid];
+ if (pivotIsDir && !rhsIsDir) {
+ compare = -1;
+ } else if (!pivotIsDir && rhsIsDir) {
+ compare = 1;
+ } else {
+ final String lhs = pivotValue;
+ final String rhs = sortKey[mid];
+ switch (direction) {
+ case SortDimension.SORT_DIRECTION_ASCENDING:
+ compare = Shared.compareToIgnoreCaseNullable(lhs, rhs);
+ break;
+ case SortDimension.SORT_DIRECTION_DESCENDING:
+ compare = -Shared.compareToIgnoreCaseNullable(lhs, rhs);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown sorting direction: " + direction);
+ }
+ }
+
+ if (compare < 0) {
+ right = mid;
+ } else {
+ left = mid + 1;
+ }
+ }
+
+ int n = start - left;
+ switch (n) {
+ case 2:
+ positions[left + 2] = positions[left + 1];
+ sortKey[left + 2] = sortKey[left + 1];
+ isDirs[left + 2] = isDirs[left + 1];
+ case 1:
+ positions[left + 1] = positions[left];
+ sortKey[left + 1] = sortKey[left];
+ isDirs[left + 1] = isDirs[left];
+ break;
+ default:
+ System.arraycopy(positions, left, positions, left + 1, n);
+ System.arraycopy(sortKey, left, sortKey, left + 1, n);
+ System.arraycopy(isDirs, left, isDirs, left + 1, n);
+ }
+
+ positions[left] = pivotPosition;
+ sortKey[left] = pivotValue;
+ isDirs[left] = pivotIsDir;
+ }
+ }
+
+ /**
+ * Borrowed from TimSort.binarySort(), but modified to sort two column
+ * dataset.
+ */
+ private static void binarySort(
+ long[] sortKey,
+ boolean[] isDirs,
+ int[] positions,
+ String[] ids,
+ @SortDimension.SortDirection int direction) {
+ final int count = positions.length;
+ for (int start = 1; start < count; start++) {
+ final int pivotPosition = positions[start];
+ final long pivotValue = sortKey[start];
+ final boolean pivotIsDir = isDirs[start];
+ final String pivotId = ids[start];
+
+ int left = 0;
+ int right = start;
+
+ while (left < right) {
+ int mid = ((left + right) >>> 1);
+
+ // Directories always go in front.
+ int compare = 0;
+ final boolean rhsIsDir = isDirs[mid];
+ if (pivotIsDir && !rhsIsDir) {
+ compare = -1;
+ } else if (!pivotIsDir && rhsIsDir) {
+ compare = 1;
+ } else {
+ final long lhs = pivotValue;
+ final long rhs = sortKey[mid];
+ switch (direction) {
+ case SortDimension.SORT_DIRECTION_ASCENDING:
+ compare = Long.compare(lhs, rhs);
+ break;
+ case SortDimension.SORT_DIRECTION_DESCENDING:
+ compare = -Long.compare(lhs, rhs);
+ break;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown sorting direction: " + direction);
+ }
+ }
+
+ // If numerical comparison yields a tie, use document ID as a tie breaker. This
+ // will yield stable results even if incoming items are continually shuffling and
+ // have identical numerical sort keys. One common example of this scenario is seen
+ // when sorting a set of active downloads by mod time.
+ if (compare == 0) {
+ compare = pivotId.compareTo(ids[mid]);
+ }
+
+ if (compare < 0) {
+ right = mid;
+ } else {
+ left = mid + 1;
+ }
+ }
+
+ int n = start - left;
+ switch (n) {
+ case 2:
+ positions[left + 2] = positions[left + 1];
+ sortKey[left + 2] = sortKey[left + 1];
+ isDirs[left + 2] = isDirs[left + 1];
+ ids[left + 2] = ids[left + 1];
+ case 1:
+ positions[left + 1] = positions[left];
+ sortKey[left + 1] = sortKey[left];
+ isDirs[left + 1] = isDirs[left];
+ ids[left + 1] = ids[left];
+ break;
+ default:
+ System.arraycopy(positions, left, positions, left + 1, n);
+ System.arraycopy(sortKey, left, sortKey, left + 1, n);
+ System.arraycopy(isDirs, left, isDirs, left + 1, n);
+ System.arraycopy(ids, left, ids, left + 1, n);
+ }
+
+ positions[left] = pivotPosition;
+ sortKey[left] = pivotValue;
+ isDirs[left] = pivotIsDir;
+ ids[left] = pivotId;
+ }
+ }
+}
diff --git a/src/com/android/documentsui/sorting/TableHeaderController.java b/src/com/android/documentsui/sorting/TableHeaderController.java
index c5957ce..da82a6e 100644
--- a/src/com/android/documentsui/sorting/TableHeaderController.java
+++ b/src/com/android/documentsui/sorting/TableHeaderController.java
@@ -76,8 +76,7 @@
cell.setTag(dimension);
cell.onBind(dimension);
- if (mModel.isSortEnabled()
- && dimension.getVisibility() == View.VISIBLE
+ if (dimension.getVisibility() == View.VISIBLE
&& dimension.getSortCapability() != SortDimension.SORT_CAPABILITY_NONE) {
cell.setOnClickListener(mOnCellClickListener);
} else {
diff --git a/src/com/android/documentsui/ui/DialogController.java b/src/com/android/documentsui/ui/DialogController.java
index e684618..90b7777 100644
--- a/src/com/android/documentsui/ui/DialogController.java
+++ b/src/com/android/documentsui/ui/DialogController.java
@@ -18,7 +18,7 @@
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
-import android.content.DialogInterface.OnShowListener;
+import android.support.design.widget.Snackbar;
import android.widget.Button;
import android.widget.TextView;
@@ -45,10 +45,16 @@
public void showFileOperationFailures(int status, int opType, int docCount) {
throw new UnsupportedOperationException();
}
+
+ @Override
+ public void showNoApplicationFound() {
+ throw new UnsupportedOperationException();
+ }
};
void confirmDelete(List<DocumentInfo> docs, ConfirmationCallback callback);
void showFileOperationFailures(int status, int opType, int docCount);
+ void showNoApplicationFound();
// Should be private, but Java doesn't like me treating an interface like a mini-package.
public static final class RuntimeDialogController implements DialogController {
@@ -99,7 +105,8 @@
}
@Override
- public void showFileOperationFailures(@Status int status, @OpType int opType, int docCount) {
+ public void showFileOperationFailures(
+ @Status int status, @OpType int opType, int docCount) {
if (status == FileOperations.Callback.STATUS_REJECTED) {
Snackbars.showPasteFailed(mActivity);
return;
@@ -123,7 +130,13 @@
default:
throw new UnsupportedOperationException("Unsupported Operation: " + opType);
}
- };
+ }
+
+ @Override
+ public void showNoApplicationFound() {
+ Snackbars.makeSnackbar(
+ mActivity, R.string.toast_no_application, Snackbar.LENGTH_SHORT).show();
+ }
}
static DialogController create(Activity activity) {
diff --git a/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java
index 4cca190..3987636 100644
--- a/tests/common/com/android/documentsui/TestActivity.java
+++ b/tests/common/com/android/documentsui/TestActivity.java
@@ -16,6 +16,8 @@
package com.android.documentsui;
+import static junit.framework.Assert.assertEquals;
+
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
@@ -23,6 +25,7 @@
import android.content.res.Resources;
import com.android.documentsui.AbstractActionHandler.CommonAddons;
+import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.testing.TestEventListener;
import com.android.documentsui.testing.android.TestPackageManager;
@@ -39,10 +42,13 @@
public TestResources resources;
public TestPackageManager packageMgr;
public Intent intent;
+ public RootInfo currentRoot;
public TestEventListener<Intent> startActivity;
public TestEventListener<Intent> startService;
public TestEventListener<RootInfo> rootPicked;
+ public TestEventListener<DocumentInfo> openContainer;
+ public TestEventListener<Integer> refreshCurrentRootAndDirectory;
public static TestActivity create() {
TestActivity activity = Mockito.mock(TestActivity.class, Mockito.CALLS_REAL_METHODS);
@@ -58,6 +64,8 @@
startActivity = new TestEventListener<>();
startService = new TestEventListener<>();
rootPicked = new TestEventListener<>();
+ openContainer = new TestEventListener<>();
+ refreshCurrentRootAndDirectory = new TestEventListener<>();
}
@Override
@@ -70,12 +78,20 @@
startActivity.accept(intent);
}
+ public final void assertActivityStarted(String expectedAction) {
+ assertEquals(expectedAction, startActivity.getLastValue().getAction());
+ }
+
@Override
public final ComponentName startService(Intent intent) {
startService.accept(intent);
return null;
}
+ public final void assertServiceStarted(String expectedAction) {
+ assertEquals(expectedAction, startService.getLastValue().getAction());
+ }
+
@Override
public final Intent getIntent() {
return intent;
@@ -87,7 +103,7 @@
}
@Override
- public PackageManager getPackageManager() {
+ public final PackageManager getPackageManager() {
return packageMgr;
}
@@ -95,6 +111,26 @@
public final void onRootPicked(RootInfo root) {
rootPicked.accept(root);
}
+
+ @Override
+ public final void onDocumentPicked(DocumentInfo doc) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public final void openContainerDocument(DocumentInfo doc) {
+ openContainer.accept(doc);
+ }
+
+ @Override
+ public final void refreshCurrentRootAndDirectory(int anim) {
+ refreshCurrentRootAndDirectory.accept(anim);
+ }
+
+ @Override
+ public final RootInfo getCurrentRoot() {
+ return currentRoot;
+ }
}
// Trick Mockito into finding our Addons methods correctly. W/o this
diff --git a/tests/common/com/android/documentsui/dirlist/TestModel.java b/tests/common/com/android/documentsui/dirlist/TestModel.java
index 0c3ca92..abe4d5f 100644
--- a/tests/common/com/android/documentsui/dirlist/TestModel.java
+++ b/tests/common/com/android/documentsui/dirlist/TestModel.java
@@ -17,11 +17,14 @@
package com.android.documentsui.dirlist;
import android.database.MatrixCursor;
+import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import com.android.documentsui.DirectoryResult;
+import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.roots.RootCursorWrapper;
-import com.android.documentsui.testing.SortModels;
+
+import libcore.net.MimeUtils;
import java.util.Random;
@@ -37,34 +40,94 @@
};
private final String mAuthority;
+ private int mLastId = 0;
+ private Random mRand = new Random();
+ private MatrixCursor mCursor;
public TestModel(String authority) {
super();
mAuthority = authority;
+ reset();
}
- public void update(String... names) {
- Random rand = new Random();
+ public void reset() {
+ mLastId = 0;
+ mCursor = new MatrixCursor(COLUMNS);
+ }
- MatrixCursor c = new MatrixCursor(COLUMNS);
- for (int i = 0; i < names.length; i++) {
- MatrixCursor.RowBuilder row = c.newRow();
- row.add(RootCursorWrapper.COLUMN_AUTHORITY, mAuthority);
- row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
- row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE);
- // Generate random document names and sizes. This forces the model's internal sort code
- // to actually do something.
- row.add(Document.COLUMN_DISPLAY_NAME, names[i]);
- row.add(Document.COLUMN_SIZE, rand.nextInt());
+ public void update() {
+ DirectoryResult r = new DirectoryResult();
+ r.cursor = mCursor;
+ super.update(r);
+ }
+
+ public DocumentInfo createFile(String name) {
+ return createFile(
+ name,
+ Document.FLAG_SUPPORTS_WRITE
+ | Document.FLAG_SUPPORTS_DELETE
+ | Document.FLAG_SUPPORTS_RENAME);
+ }
+
+ public DocumentInfo createFile(String name, int flags) {
+ return createDocument(
+ name,
+ guessMimeType(name),
+ flags);
+ }
+
+ public DocumentInfo createFolder(String name) {
+ return createFolder(
+ name,
+ Document.FLAG_SUPPORTS_WRITE
+ | Document.FLAG_SUPPORTS_DELETE
+ | Document.FLAG_SUPPORTS_REMOVE
+ | Document.FLAG_DIR_SUPPORTS_CREATE);
+ }
+
+ public DocumentInfo createFolder(String name, int flags) {
+ return createDocument(
+ name,
+ DocumentsContract.Document.MIME_TYPE_DIR,
+ flags);
+ }
+
+ public DocumentInfo createDocument(String name, String mimeType, int flags) {
+ DocumentInfo doc = new DocumentInfo();
+ doc.authority = mAuthority;
+ doc.documentId = Integer.toString(++mLastId);
+ doc.displayName = name;
+ doc.mimeType = mimeType;
+ doc.flags = flags;
+ doc.size = mRand.nextInt();
+
+ addToCursor(doc);
+
+ return doc;
+ }
+
+ private void addToCursor(DocumentInfo doc) {
+ MatrixCursor.RowBuilder row = mCursor.newRow();
+ row.add(Document.COLUMN_DOCUMENT_ID, doc.documentId);
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, doc.authority);
+ row.add(Document.COLUMN_DISPLAY_NAME, doc.displayName);
+ row.add(Document.COLUMN_MIME_TYPE, doc.mimeType);
+ row.add(Document.COLUMN_FLAGS, doc.flags);
+ row.add(Document.COLUMN_SIZE, doc.size);
+ }
+
+ private static String guessMimeType(String name) {
+ int i = name.indexOf('.');
+
+ while(i != -1) {
+ name = name.substring(i + 1);
+ String type = MimeUtils.guessMimeTypeFromExtension(name);
+ if (type != null) {
+ return type;
+ }
+ i = name.indexOf('.');
}
- DirectoryResult r = new DirectoryResult();
- r.cursor = c;
- r.sortModel = SortModels.createTestSortModel();
- update(r);
- }
-
- String idForPosition(int p) {
- return Integer.toString(p);
+ return "text/plain";
}
}
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index 4581a5c..08bee36 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -34,7 +34,7 @@
}
public TestActionHandler(TestEnv env) {
- super(TestActivity.create(), env.state, env.roots, (String authority) -> null);
+ super(TestActivity.create(), env.state, env.roots, env.docs, (String authority) -> null);
}
@Override
diff --git a/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java b/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java
new file mode 100644
index 0000000..40f799e
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java
@@ -0,0 +1,45 @@
+/*
+ * 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.testing;
+
+import android.net.Uri;
+
+import com.android.documentsui.DocumentsAccess;
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.RootInfo;
+
+import javax.annotation.Nullable;
+
+public class TestDocumentsAccess implements DocumentsAccess {
+
+ public @Nullable DocumentInfo nextRootDocument;
+ public @Nullable DocumentInfo nextDocument;
+
+ @Override
+ public DocumentInfo getRootDocument(Uri uri) {
+ return nextRootDocument;
+ }
+
+ @Override
+ public DocumentInfo getRootDocument(RootInfo root) {
+ return nextRootDocument;
+ }
+
+ @Override
+ public DocumentInfo getDocument(Uri uri) {
+ return nextDocument;
+ }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java
index f898e6b..10de113 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -17,35 +17,94 @@
import android.os.Handler;
import android.os.Looper;
+import android.provider.DocumentsContract.Document;
+import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.dirlist.TestModel;
+import junit.framework.Assert;
+
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
public class TestEnv {
- public static final String AUTHORITY = "hullabaloo";
+ public static DocumentInfo FOLDER_0;
+ public static DocumentInfo FOLDER_1;
+ public static DocumentInfo FOLDER_2;
+ public static DocumentInfo FILE_TXT;
+ public static DocumentInfo FILE_PNG;
+ public static DocumentInfo FILE_JPG;
+ public static DocumentInfo FILE_GIF;
+ public static DocumentInfo FILE_PDF;
+ public static DocumentInfo FILE_APK;
+ public static DocumentInfo FILE_PARTIAL;
+ public static DocumentInfo FILE_ARCHIVE;
+ public static DocumentInfo FILE_VIRTUAL;
public final TestScheduledExecutorService mExecutor;
public final State state = new State();
public final TestRootsAccess roots = new TestRootsAccess();
- public final TestModel model = new TestModel(AUTHORITY);
+ public final TestDocumentsAccess docs = new TestDocumentsAccess();
+ public final TestModel model;
- private TestEnv() {
+ private TestEnv(String authority) {
mExecutor = new TestScheduledExecutorService();
+ model = new TestModel(authority);
}
public static TestEnv create() {
- TestEnv env = new TestEnv();
+ return create(TestRootsAccess.HOME.authority);
+ }
+
+ public static TestEnv create(String authority) {
+ TestEnv env = new TestEnv(authority);
env.reset();
return env;
}
+ public void clear() {
+ model.reset();
+ model.update();
+ }
+
public void reset() {
- model.update("a", "b", "c", "x", "y", "z");
- state.stack.push(model.getDocument("1"));
+ model.reset();
+ FOLDER_0 = model.createFolder("folder 0");
+ FOLDER_1 = model.createFolder("folder 1");
+ FOLDER_2 = model.createFolder("folder 2");
+ FILE_TXT = model.createFile("woowoo.txt");
+ FILE_PNG = model.createFile("peppey.png");
+ FILE_JPG = model.createFile("jiffy.jpg");
+ FILE_GIF = model.createFile("glibby.gif");
+ FILE_PDF = model.createFile("busy.pdf");
+ FILE_APK = model.createFile("becareful.apk");
+ FILE_PARTIAL = model.createFile(
+ "UbuntuFlappyBird.iso",
+ Document.FLAG_SUPPORTS_DELETE
+ | Document.FLAG_PARTIAL);
+ FILE_ARCHIVE = model.createFile(
+ "whatsinthere.zip",
+ Document.FLAG_ARCHIVE
+ | Document.FLAG_SUPPORTS_DELETE);
+ FILE_VIRTUAL = model.createDocument(
+ "virtualdoc.vnd",
+ "application/vnd.google-apps.document",
+ Document.FLAG_VIRTUAL_DOCUMENT
+ | Document.FLAG_SUPPORTS_DELETE
+ | Document.FLAG_SUPPORTS_RENAME);
+
+ model.update();
+ }
+
+ public void populateStack() {
+ DocumentInfo rootDoc = model.getDocument("1");
+ Assert.assertNotNull(rootDoc);
+ Assert.assertEquals(rootDoc.displayName, FOLDER_0.displayName);
+
+ state.stack.root = TestRootsAccess.HOME;
+ state.stack.push(rootDoc);
}
public void beforeAsserts() throws Exception {
diff --git a/tests/common/com/android/documentsui/testing/android/TestPackageManager.java b/tests/common/com/android/documentsui/testing/android/TestPackageManager.java
index a078c6c..a5b28b1 100644
--- a/tests/common/com/android/documentsui/testing/android/TestPackageManager.java
+++ b/tests/common/com/android/documentsui/testing/android/TestPackageManager.java
@@ -16,7 +16,10 @@
package com.android.documentsui.testing.android;
+import android.annotation.UserIdInt;
import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
@@ -38,8 +41,6 @@
public Map<String, ResolveInfo> contentProviders;
- private TestPackageManager() {}
-
public void addStubContentProviderForRoot(RootInfo... roots) {
for (RootInfo root : roots) {
// only one entry per authority is required.
@@ -60,9 +61,24 @@
}
@Override
- public List<ResolveInfo> queryIntentContentProviders(Intent intent, int flags) {
+ public final List<ResolveInfo> queryIntentContentProviders(Intent intent, int flags) {
List<ResolveInfo> result = new ArrayList<>();
result.addAll(contentProviders.values());
return result;
}
+
+ public final ResolveInfo resolveActivity(Intent intent, int flags) {
+ ResolveInfo info = new ResolveInfo();
+ info.activityInfo = new ActivityInfo();
+ info.activityInfo.packageName = intent.getPackage();
+ info.activityInfo.applicationInfo = new ApplicationInfo();
+ info.activityInfo.applicationInfo.packageName = intent.getPackage();
+ info.activityInfo.name = "Fake Quick Viewer";
+ return info;
+ }
+
+ public final ResolveInfo resolveActivityAsUser(
+ Intent intent, int flags, @UserIdInt int userId) {
+ return resolveActivity(intent, flags);
+ }
}
diff --git a/tests/common/com/android/documentsui/testing/android/TestResources.java b/tests/common/com/android/documentsui/testing/android/TestResources.java
index d72aee1..246b4e8 100644
--- a/tests/common/com/android/documentsui/testing/android/TestResources.java
+++ b/tests/common/com/android/documentsui/testing/android/TestResources.java
@@ -16,11 +16,20 @@
package com.android.documentsui.testing.android;
+import android.annotation.BoolRes;
+import android.annotation.NonNull;
+import android.annotation.StringRes;
import android.content.res.Resources;
+import android.util.SparseArray;
import android.util.SparseBooleanArray;
+import com.android.documentsui.R;
+import com.android.documentsui.files.QuickViewIntentBuilder;
+
import org.mockito.Mockito;
+import javax.annotation.Nullable;
+
/**
* Abstract to avoid having to implement unnecessary Activity stuff.
* Instances are created using {@link #create()}.
@@ -28,20 +37,48 @@
public abstract class TestResources extends Resources {
public SparseBooleanArray bools;
+ public SparseArray<String> strings;
public TestResources() {
super(ClassLoader.getSystemClassLoader());
}
public static TestResources create() {
- TestResources resources = Mockito.mock(
+ TestResources res = Mockito.mock(
TestResources.class, Mockito.CALLS_REAL_METHODS);
- resources.bools = new SparseBooleanArray();
- return resources;
+ res.bools = new SparseBooleanArray();
+ res.strings = new SparseArray<>();
+
+ res.setProductivityDeviceEnabled(false);
+
+ // quick view package can be set via system property on debug builds.
+ // unfortunately that interfers with testing. For that reason we have
+ // this little hack....QuickViewIntentBuilder will check for this value
+ // and ignore
+ res.setQuickViewerPackage(QuickViewIntentBuilder.IGNORE_DEBUG_PROP);
+ return res;
+ }
+
+ public void setQuickViewerPackage(String packageName) {
+ strings.put(R.string.trusted_quick_viewer_package, packageName);
+ }
+
+ public void setProductivityDeviceEnabled(boolean enabled) {
+ bools.put(R.bool.productivity_device, enabled);
}
@Override
- public boolean getBoolean(int id) throws NotFoundException {
+ public final boolean getBoolean(@BoolRes int id) throws NotFoundException {
return bools.get(id);
}
+
+ @Override
+ public final @Nullable String getString(@StringRes int id) throws NotFoundException {
+ return strings.get(id);
+ }
+
+ @NonNull
+ public final String getString(@StringRes int id, Object... formatArgs) throws NotFoundException {
+ return getString(id);
+ }
}
diff --git a/tests/common/com/android/documentsui/ui/TestDialogController.java b/tests/common/com/android/documentsui/ui/TestDialogController.java
index c7c1bbf..dbac0f5 100644
--- a/tests/common/com/android/documentsui/ui/TestDialogController.java
+++ b/tests/common/com/android/documentsui/ui/TestDialogController.java
@@ -27,6 +27,7 @@
public int mNextConfirmationCode;
private boolean mFileOpFailed;
+ private boolean mNoApplicationFound;
public TestDialogController() {
// by default, always confirm
@@ -45,10 +46,19 @@
}
}
+ @Override
+ public void showNoApplicationFound() {
+ mNoApplicationFound = true;
+ }
+
public void assertNoFileFailures() {
Assert.assertFalse(mFileOpFailed);
}
+ public void assertNoAppFoundShown() {
+ Assert.assertFalse(mNoApplicationFound);
+ }
+
public void confirmNext() {
mNextConfirmationCode = ConfirmationCallback.CONFIRM;
}
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index ee9ee4d..d924a44 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -54,6 +54,7 @@
mActivity,
mEnv.state,
mEnv.roots,
+ mEnv.docs,
mEnv::lookupExecutor) {
@Override
diff --git a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
index 1208f03..eb1ee6f 100644
--- a/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/ModelBackedDocumentsAdapterTest.java
@@ -18,36 +18,24 @@
import android.content.Context;
import android.database.Cursor;
+import android.support.test.filters.MediumTest;
import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
import com.android.documentsui.base.State;
+import com.android.documentsui.testing.TestEnv;
-@SmallTest
+@MediumTest
public class ModelBackedDocumentsAdapterTest extends AndroidTestCase {
private static final String AUTHORITY = "test_authority";
- private static final String[] NAMES = new String[] {
- "4",
- "foo",
- "1",
- "bar",
- "*(Ljifl;a",
- "0",
- "baz",
- "2",
- "3",
- "%$%VD"
- };
- private TestModel mModel;
+ private TestEnv mEnv;
private ModelBackedDocumentsAdapter mAdapter;
public void setUp() {
final Context testContext = TestContext.createStorageTestContext(getContext(), AUTHORITY);
- mModel = new TestModel(AUTHORITY);
- mModel.update(NAMES);
+ mEnv = TestEnv.create(AUTHORITY);
DocumentsAdapter.Environment env = new TestEnvironment(testContext);
@@ -58,7 +46,7 @@
// Tests that the item count is correct.
public void testItemCount() {
- assertEquals(mModel.getItemCount(), mAdapter.getItemCount());
+ assertEquals(mEnv.model.getItemCount(), mAdapter.getItemCount());
}
private final class TestEnvironment implements DocumentsAdapter.Environment {
@@ -83,7 +71,7 @@
@Override
public Model getModel() {
- return mModel;
+ return mEnv.model;
}
@Override
diff --git a/tests/unit/com/android/documentsui/dirlist/ModelTest.java b/tests/unit/com/android/documentsui/dirlist/ModelTest.java
index b9dab0d..75f46e4 100644
--- a/tests/unit/com/android/documentsui/dirlist/ModelTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/ModelTest.java
@@ -22,9 +22,9 @@
import android.database.MatrixCursor;
import android.database.MergeCursor;
import android.provider.DocumentsContract.Document;
+import android.support.test.filters.SmallTest;
import android.test.AndroidTestCase;
import android.test.mock.MockContentResolver;
-import android.test.suitebuilder.annotation.SmallTest;
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.base.DocumentInfo;
@@ -74,7 +74,6 @@
private Cursor cursor;
private Model model;
private TestContentProvider provider;
- private SortModel sortModel;
@Override
public void setUp() {
@@ -94,11 +93,9 @@
row.add(Document.COLUMN_SIZE, rand.nextInt());
}
cursor = c;
- sortModel = SortModels.createTestSortModel();
DirectoryResult r = new DirectoryResult();
r.cursor = cursor;
- r.sortModel = sortModel;
// Instantiate the model with a dummy view adapter and listener that (for now) do nothing.
model = new Model();
@@ -136,7 +133,6 @@
// Update the model, then make sure it contains all the expected items.
DirectoryResult r = new DirectoryResult();
r.cursor = cIn;
- r.sortModel = SortModels.createTestSortModel();
model.update(r);
assertEquals(ITEM_COUNT * 2, model.getItemCount());
@@ -174,291 +170,6 @@
assertEquals(i, c.getPosition());
}
}
-
- // Tests sorting ascending by item name.
- public void testSort_names_ascending() {
- BitSet seen = new BitSet(ITEM_COUNT);
- List<String> names = new ArrayList<>();
-
- sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_TITLE,
- SortDimension.SORT_DIRECTION_ASCENDING);
-
- DirectoryResult r = new DirectoryResult();
- r.cursor = cursor;
- r.sortModel = sortModel;
- model.update(r);
-
- for (String id: model.getModelIds()) {
- Cursor c = model.getItem(id);
- seen.set(c.getPosition());
- names.add(DocumentInfo.getCursorString(c, Document.COLUMN_DISPLAY_NAME));
- }
-
- assertEquals(ITEM_COUNT, seen.cardinality());
- for (int i = 0; i < names.size()-1; ++i) {
- assertTrue(Shared.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) <= 0);
- }
- }
-
- // Tests sorting descending by item name.
- public void testSort_names_descending() {
- BitSet seen = new BitSet(ITEM_COUNT);
- List<String> names = new ArrayList<>();
-
- sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_TITLE,
- SortDimension.SORT_DIRECTION_DESCENDING);
-
- DirectoryResult r = new DirectoryResult();
- r.cursor = cursor;
- r.sortModel = sortModel;
- model.update(r);
-
- for (String id: model.getModelIds()) {
- Cursor c = model.getItem(id);
- seen.set(c.getPosition());
- names.add(DocumentInfo.getCursorString(c, Document.COLUMN_DISPLAY_NAME));
- }
-
- assertEquals(ITEM_COUNT, seen.cardinality());
- for (int i = 0; i < names.size()-1; ++i) {
- assertTrue(Shared.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) >= 0);
- }
- }
-
- // Tests sorting by item size.
- public void testSort_sizes_ascending() {
- sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
- SortDimension.SORT_DIRECTION_ASCENDING);
-
- DirectoryResult r = new DirectoryResult();
- r.cursor = cursor;
- r.sortModel = sortModel;
- model.update(r);
-
- BitSet seen = new BitSet(ITEM_COUNT);
- int previousSize = Integer.MIN_VALUE;
- for (String id: model.getModelIds()) {
- Cursor c = model.getItem(id);
- seen.set(c.getPosition());
- // Check sort order - descending numerical
- int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
- assertTrue(previousSize <= size);
- previousSize = size;
- }
- // Check that all items were accounted for.
- assertEquals(ITEM_COUNT, seen.cardinality());
- }
-
- // Tests sorting by item size.
- public void testSort_sizes_descending() {
- sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
- SortDimension.SORT_DIRECTION_DESCENDING);
-
- DirectoryResult r = new DirectoryResult();
- r.cursor = cursor;
- r.sortModel = sortModel;
- model.update(r);
-
- BitSet seen = new BitSet(ITEM_COUNT);
- int previousSize = Integer.MAX_VALUE;
- for (String id: model.getModelIds()) {
- Cursor c = model.getItem(id);
- seen.set(c.getPosition());
- // Check sort order - descending numerical
- int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
- assertTrue(previousSize >= size);
- previousSize = size;
- }
- // Check that all items were accounted for.
- assertEquals(ITEM_COUNT, seen.cardinality());
- }
-
- // Tests that directories and files are properly bucketed when sorting by size
- public void testSort_sizesWithBucketing_ascending() {
- MatrixCursor c = new MatrixCursor(COLUMNS);
-
- for (int i = 0; i < ITEM_COUNT; ++i) {
- MatrixCursor.RowBuilder row = c.newRow();
- row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
- row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
- row.add(Document.COLUMN_SIZE, i);
- // Interleave directories and text files.
- String mimeType =(i % 2 == 0) ? Document.MIME_TYPE_DIR : "text/*";
- row.add(Document.COLUMN_MIME_TYPE, mimeType);
- }
-
- sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
- SortDimension.SORT_DIRECTION_ASCENDING);
-
- DirectoryResult r = new DirectoryResult();
- r.cursor = c;
- r.sortModel = sortModel;
- model.update(r);
-
- boolean seenAllDirs = false;
- int previousSize = Integer.MIN_VALUE;
- BitSet seen = new BitSet(ITEM_COUNT);
- // Iterate over items in sort order. Once we've encountered a document (i.e. not a
- // directory), all subsequent items must also be documents. That is, all directories are
- // bucketed at the front of the list, sorted by size, followed by documents, sorted by size.
- for (String id: model.getModelIds()) {
- Cursor cOut = model.getItem(id);
- seen.set(cOut.getPosition());
-
- String mimeType = DocumentInfo.getCursorString(cOut, Document.COLUMN_MIME_TYPE);
- if (seenAllDirs) {
- assertFalse(Document.MIME_TYPE_DIR.equals(mimeType));
- } else {
- if (!Document.MIME_TYPE_DIR.equals(mimeType)) {
- seenAllDirs = true;
- // Reset the previous size seen, because documents are bucketed separately by
- // the sort.
- previousSize = Integer.MIN_VALUE;
- }
- }
- // Check sort order - descending numerical
- int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
- assertTrue(previousSize <= size);
- previousSize = size;
- }
-
- // Check that all items were accounted for.
- assertEquals(ITEM_COUNT, seen.cardinality());
- }
-
- // Tests that directories and files are properly bucketed when sorting by size
- public void testSort_sizesWithBucketing_descending() {
- MatrixCursor c = new MatrixCursor(COLUMNS);
-
- for (int i = 0; i < ITEM_COUNT; ++i) {
- MatrixCursor.RowBuilder row = c.newRow();
- row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
- row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
- row.add(Document.COLUMN_SIZE, i);
- // Interleave directories and text files.
- String mimeType =(i % 2 == 0) ? Document.MIME_TYPE_DIR : "text/*";
- row.add(Document.COLUMN_MIME_TYPE, mimeType);
- }
-
- sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
- SortDimension.SORT_DIRECTION_DESCENDING);
-
- DirectoryResult r = new DirectoryResult();
- r.cursor = c;
- r.sortModel = sortModel;
- model.update(r);
-
- boolean seenAllDirs = false;
- int previousSize = Integer.MAX_VALUE;
- BitSet seen = new BitSet(ITEM_COUNT);
- // Iterate over items in sort order. Once we've encountered a document (i.e. not a
- // directory), all subsequent items must also be documents. That is, all directories are
- // bucketed at the front of the list, sorted by size, followed by documents, sorted by size.
- for (String id: model.getModelIds()) {
- Cursor cOut = model.getItem(id);
- seen.set(cOut.getPosition());
-
- String mimeType = DocumentInfo.getCursorString(cOut, Document.COLUMN_MIME_TYPE);
- if (seenAllDirs) {
- assertFalse(Document.MIME_TYPE_DIR.equals(mimeType));
- } else {
- if (!Document.MIME_TYPE_DIR.equals(mimeType)) {
- seenAllDirs = true;
- // Reset the previous size seen, because documents are bucketed separately by
- // the sort.
- previousSize = Integer.MAX_VALUE;
- }
- }
- // Check sort order - descending numerical
- int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
- assertTrue(previousSize >= size);
- previousSize = size;
- }
-
- // Check that all items were accounted for.
- assertEquals(ITEM_COUNT, seen.cardinality());
- }
-
- public void testSort_time_ascending() {
- final int DL_COUNT = 3;
- MatrixCursor c = new MatrixCursor(COLUMNS);
- Set<String> currentDownloads = new HashSet<>();
-
- // Add some files
- for (int i = 0; i < ITEM_COUNT; i++) {
- MatrixCursor.RowBuilder row = c.newRow();
- row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
- row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
- row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
- }
- // Add some current downloads (no timestamp)
- for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
- MatrixCursor.RowBuilder row = c.newRow();
- String id = Integer.toString(i);
- row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
- row.add(Document.COLUMN_DOCUMENT_ID, id);
- currentDownloads.add(id);
- }
-
- sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_DATE,
- SortDimension.SORT_DIRECTION_ASCENDING);
-
- DirectoryResult r = new DirectoryResult();
- r.cursor = c;
- r.sortModel = sortModel;
- model.update(r);
-
- String[] ids = model.getModelIds();
-
- // Check that all items were accounted for
- assertEquals(ITEM_COUNT + DL_COUNT, ids.length);
-
- // Check that active downloads are sorted to the bottom.
- for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
- assertTrue(currentDownloads.contains(ids[i]));
- }
- }
-
- public void testSort_time_descending() {
- final int DL_COUNT = 3;
- MatrixCursor c = new MatrixCursor(COLUMNS);
- Set<String> currentDownloads = new HashSet<>();
-
- // Add some files
- for (int i = 0; i < ITEM_COUNT; i++) {
- MatrixCursor.RowBuilder row = c.newRow();
- row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
- row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
- row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
- }
- // Add some current downloads (no timestamp)
- for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
- MatrixCursor.RowBuilder row = c.newRow();
- String id = Integer.toString(i);
- row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
- row.add(Document.COLUMN_DOCUMENT_ID, id);
- currentDownloads.add(id);
- }
-
- sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_DATE,
- SortDimension.SORT_DIRECTION_DESCENDING);
-
- DirectoryResult r = new DirectoryResult();
- r.cursor = c;
- r.sortModel = sortModel;
- model.update(r);
-
- String[] ids = model.getModelIds();
-
- // Check that all items were accounted for
- assertEquals(ITEM_COUNT + DL_COUNT, ids.length);
-
- // Check that active downloads are sorted to the top.
- for (int i = 0; i < DL_COUNT; i++) {
- assertTrue(currentDownloads.contains(ids[i]));
- }
- }
-
private void setupTestContext() {
final MockContentResolver resolver = new MockContentResolver();
new ContextWrapper(getContext()) {
diff --git a/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java b/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
index dec287b..39ab24a 100644
--- a/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/SectionBreakDocumentsAdapterWrapperTest.java
@@ -18,95 +18,63 @@
import android.content.Context;
import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.provider.DocumentsContract.Document;
+import android.support.test.filters.MediumTest;
import android.support.v7.widget.RecyclerView;
import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
import android.view.ViewGroup;
-import com.android.documentsui.DirectoryResult;
import com.android.documentsui.base.State;
-import com.android.documentsui.roots.RootCursorWrapper;
-import com.android.documentsui.testing.SortModels;
+import com.android.documentsui.testing.TestEnv;
-@SmallTest
+@MediumTest
public class SectionBreakDocumentsAdapterWrapperTest extends AndroidTestCase {
private static final String AUTHORITY = "test_authority";
- private static final String[] NAMES = new String[] {
- "4",
- "foo",
- "1",
- "bar",
- "*(Ljifl;a",
- "0",
- "baz",
- "2",
- "3",
- "%$%VD"
- };
- private TestModel mModel;
+ private TestEnv mEnv;
private SectionBreakDocumentsAdapterWrapper mAdapter;
public void setUp() {
+ mEnv = TestEnv.create(AUTHORITY);
+ mEnv.clear();
+
final Context testContext = TestContext.createStorageTestContext(getContext(), AUTHORITY);
DocumentsAdapter.Environment env = new TestEnvironment(testContext);
- mModel = new TestModel(AUTHORITY);
mAdapter = new SectionBreakDocumentsAdapterWrapper(
env,
new ModelBackedDocumentsAdapter(
- env, new IconHelper(testContext, State.MODE_GRID)));
+ env, new IconHelper(testContext, State.MODE_GRID)));
- mModel.addUpdateListener(mAdapter.getModelUpdateListener());
- }
-
- // Tests that the item count is correct for a directory containing only subdirs.
- public void testItemCount_allDirs() {
- MatrixCursor c = new MatrixCursor(TestModel.COLUMNS);
-
- for (int i = 0; i < 5; ++i) {
- MatrixCursor.RowBuilder row = c.newRow();
- row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
- row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
- row.add(Document.COLUMN_SIZE, i);
- row.add(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
- }
- DirectoryResult r = new DirectoryResult();
- r.cursor = c;
- r.sortModel = SortModels.createTestSortModel();
- mModel.update(r);
-
- assertEquals(mModel.getItemCount(), mAdapter.getItemCount());
- }
-
- // Tests that the item count is correct for a directory containing only files.
- public void testItemCount_allFiles() {
- mModel.update(NAMES);
- assertEquals(mModel.getItemCount(), mAdapter.getItemCount());
+ mEnv.model.addUpdateListener(mAdapter.getModelUpdateListener());
}
// Tests that the item count is correct for a directory containing files and subdirs.
public void testItemCount_mixed() {
- MatrixCursor c = new MatrixCursor(TestModel.COLUMNS);
+ mEnv.reset(); // creates a mix of folders and files for us.
- for (int i = 0; i < 5; ++i) {
- MatrixCursor.RowBuilder row = c.newRow();
- row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
- row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
- row.add(Document.COLUMN_SIZE, i);
- String mimeType =(i < 2) ? Document.MIME_TYPE_DIR : "text/*";
- row.add(Document.COLUMN_MIME_TYPE, mimeType);
+ assertEquals(mEnv.model.getItemCount() + 1, mAdapter.getItemCount());
+ }
+
+ // Tests that the item count is correct for a directory containing only subdirs.
+ public void testItemCount_allDirs() {
+ String[] names = {"Trader Joe's", "Alphabeta", "Lucky", "Vons", "Gelson's"};
+ for (String name : names) {
+ mEnv.model.createFolder(name);
}
- DirectoryResult r = new DirectoryResult();
- r.cursor = c;
- r.sortModel = SortModels.createTestSortModel();
- mModel.update(r);
+ mEnv.model.update();
+ assertEquals(mEnv.model.getItemCount(), mAdapter.getItemCount());
+ }
- assertEquals(mModel.getItemCount() + 1, mAdapter.getItemCount());
+ // Tests that the item count is correct for a directory containing only files.
+ public void testItemCount_allFiles() {
+ String[] names = {"123.txt", "234.jpg", "abc.pdf"};
+ for (String name : names) {
+ mEnv.model.createFile(name);
+ }
+ mEnv.model.update();
+ assertEquals(mEnv.model.getItemCount(), mAdapter.getItemCount());
}
private final class TestEnvironment implements DocumentsAdapter.Environment {
@@ -131,7 +99,7 @@
@Override
public Model getModel() {
- return mModel;
+ return mEnv.model;
}
@Override
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index ada1b5f..457c2ba 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -26,6 +26,7 @@
import android.support.test.runner.AndroidJUnit4;
import com.android.documentsui.R;
+import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.testing.TestConfirmationCallback;
@@ -60,6 +61,7 @@
mActivity,
mEnv.state,
mEnv.roots,
+ mEnv.docs,
mEnv::lookupExecutor,
mDialogs,
null, // tuner, not currently used.
@@ -71,10 +73,14 @@
mSelection = new Selection();
mSelection.add("1");
+
+ mHandler.reset(mEnv.model, null);
}
@Test
public void testDeleteDocuments() {
+ mEnv.populateStack();
+
mHandler.deleteDocuments(mEnv.model, mSelection, mCallback);
mDialogs.assertNoFileFailures();
mActivity.startService.assertCalled();
@@ -83,6 +89,8 @@
@Test
public void testDeleteDocuments_Cancelable() {
+ mEnv.populateStack();
+
mDialogs.rejectNext();
mHandler.deleteDocuments(mEnv.model, mSelection, mCallback);
mDialogs.assertNoFileFailures();
@@ -91,6 +99,63 @@
}
@Test
+ public void testDocumentPicked_DefaultsToView() throws Exception {
+ mActivity.currentRoot = TestRootsAccess.HOME;
+
+ mHandler.onDocumentPicked(TestEnv.FILE_GIF);
+ mActivity.assertActivityStarted(Intent.ACTION_VIEW);
+ }
+
+ @Test
+ public void testDocumentPicked_PreviewsWhenResourceSet() throws Exception {
+ mActivity.resources.setQuickViewerPackage("corptropolis.viewer");
+ mActivity.currentRoot = TestRootsAccess.HOME;
+
+ mHandler.onDocumentPicked(TestEnv.FILE_GIF);
+ mActivity.assertActivityStarted(Intent.ACTION_QUICK_VIEW);
+ }
+
+ @Test
+ public void testDocumentPicked_Downloads_ManagesApks() throws Exception {
+ mActivity.currentRoot = TestRootsAccess.DOWNLOADS;
+
+ mHandler.onDocumentPicked(TestEnv.FILE_APK);
+ mActivity.assertActivityStarted(DocumentsContract.ACTION_MANAGE_DOCUMENT);
+ }
+
+ @Test
+ public void testDocumentPicked_Downloads_ManagesPartialFiles() throws Exception {
+ mActivity.currentRoot = TestRootsAccess.DOWNLOADS;
+
+ mHandler.onDocumentPicked(TestEnv.FILE_PARTIAL);
+ mActivity.assertActivityStarted(DocumentsContract.ACTION_MANAGE_DOCUMENT);
+ }
+
+ @Test
+ public void testDocumentPicked_OpensArchives() throws Exception {
+ mActivity.currentRoot = TestRootsAccess.HOME;
+
+ mHandler.onDocumentPicked(TestEnv.FILE_ARCHIVE);
+ mActivity.openContainer.assertLastArgument(TestEnv.FILE_ARCHIVE);
+ }
+
+ @Test
+ public void testDocumentPicked_OpensDirectories() throws Exception {
+ mActivity.currentRoot = TestRootsAccess.HOME;
+
+ mHandler.onDocumentPicked(TestEnv.FOLDER_1);
+ mActivity.openContainer.assertLastArgument(TestEnv.FOLDER_1);
+ }
+
+ @Test
+ public void testShowChooser() throws Exception {
+ mActivity.currentRoot = TestRootsAccess.DOWNLOADS;
+
+ mHandler.showChooserForDoc(TestEnv.FILE_PDF);
+ mActivity.assertActivityStarted(Intent.ACTION_CHOOSER);
+ }
+
+ @Test
public void testInitLocation_DefaultsToDownloads() throws Exception {
mActivity.resources.bools.put(R.bool.productivity_device, false);
@@ -107,19 +172,39 @@
}
@Test
- public void testInitLocation_LoadsFromRootUri() throws Exception {
- mActivity.resources.bools.put(R.bool.productivity_device, true);
+ public void testInitLocation_ViewDocument() throws Exception {
+ Intent intent = mActivity.getIntent();
+ intent.setAction(Intent.ACTION_VIEW);
+ // configure DocumentsAccess to return something.
+ mEnv.docs.nextRootDocument = TestEnv.FOLDER_0;
+ mEnv.docs.nextDocument = TestEnv.FILE_GIF;
+
+ Uri destUri = mEnv.model.getItemUri(TestEnv.FILE_GIF.documentId);
+ intent.setData(destUri);
+
+ mEnv.state.stack.clear(); // Stack must be clear, we've even got an assert!
+ mHandler.initLocation(intent);
+ assertDocumentPicked(destUri);
+ }
+
+ private void assertDocumentPicked(Uri expectedUri) throws Exception {
+ mEnv.beforeAsserts();
+
+ mActivity.refreshCurrentRootAndDirectory.assertCalled();
+ DocumentInfo doc = mEnv.state.stack.peekLast();
+ Uri actualUri = mEnv.model.getItemUri(doc.documentId);
+ assertEquals(expectedUri, actualUri);
+ }
+
+ @Test
+ public void testInitLocation_BrowseRoot() throws Exception {
Intent intent = mActivity.getIntent();
intent.setAction(DocumentsContract.ACTION_BROWSE);
intent.setData(TestRootsAccess.PICKLES.getUri());
mHandler.initLocation(intent);
- assertRootPicked(TestRootsAccess.PICKLES);
- }
-
- private void assertRootPicked(RootInfo root) throws Exception {
- assertRootPicked(root.getUri());
+ assertRootPicked(TestRootsAccess.PICKLES.getUri());
}
private void assertRootPicked(Uri expectedUri) throws Exception {
diff --git a/tests/unit/com/android/documentsui/sorting/SortModelTest.java b/tests/unit/com/android/documentsui/sorting/SortModelTest.java
index e418848..aa50fb0 100644
--- a/tests/unit/com/android/documentsui/sorting/SortModelTest.java
+++ b/tests/unit/com/android/documentsui/sorting/SortModelTest.java
@@ -17,9 +17,7 @@
package com.android.documentsui.sorting;
import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.support.annotation.Nullable;
@@ -82,11 +80,6 @@
}
@Test
- public void testEnabledByDefault() {
- assertTrue(mModel.isSortEnabled());
- }
-
- @Test
public void testSizeEquals() {
assertEquals(DIMENSIONS.length, mModel.getSize());
}
@@ -193,36 +186,6 @@
}
@Test
- public void testSetSortEnabled() {
- mModel.setSortEnabled(false);
-
- assertFalse(mModel.isSortEnabled());
- }
-
- @Test
- public void testSetDefaultDimension_sortDisabled() {
- mModel.setSortEnabled(false);
-
- mModel.setDefaultDimension(DIMENSION_1.getId());
-
- SortDimension sortedDimension = getSortedDimension();
- assertSame(DIMENSION_1, sortedDimension);
- assertEquals(DIMENSION_1.getDefaultSortDirection(), sortedDimension.getSortDirection());
- }
-
- @Test
- public void testSortByUser_sortDisabled() {
- mModel.setSortEnabled(false);
-
- try {
- mModel.sortByUser(DIMENSION_1.getId(), SortDimension.SORT_DIRECTION_ASCENDING);
- fail("Expect exception but not raised.");
- } catch(IllegalStateException expected) {
- // Expected
- }
- }
-
- @Test
public void testSetDefaultDimension_noSortingCapability() {
try {
mModel.setDefaultDimension(DIMENSION_3.getId());
diff --git a/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java b/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java
new file mode 100644
index 0000000..1d6a09c
--- /dev/null
+++ b/tests/unit/com/android/documentsui/sorting/SortingCursorWrapperTest.java
@@ -0,0 +1,377 @@
+/*
+ * 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.sorting;
+
+import static com.android.documentsui.base.DocumentInfo.getCursorString;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.provider.DocumentsContract.Document;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.roots.RootCursorWrapper;
+import com.android.documentsui.sorting.SortModel.SortDimensionId;
+import com.android.documentsui.testing.SortModels;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+public class SortingCursorWrapperTest {
+ private static final int ITEM_COUNT = 10;
+ private static final String AUTHORITY = "test_authority";
+
+ private static final String[] COLUMNS = new String[]{
+ RootCursorWrapper.COLUMN_AUTHORITY,
+ Document.COLUMN_DOCUMENT_ID,
+ Document.COLUMN_FLAGS,
+ Document.COLUMN_DISPLAY_NAME,
+ Document.COLUMN_SIZE,
+ Document.COLUMN_LAST_MODIFIED,
+ Document.COLUMN_MIME_TYPE
+ };
+
+ private static final String[] NAMES = new String[] {
+ "4",
+ "foo",
+ "1",
+ "bar",
+ "*(Ljifl;a",
+ "0",
+ "baz",
+ "2",
+ "3",
+ "%$%VD"
+ };
+
+ private SortModel sortModel;
+ private Cursor cursor;
+
+ @Before
+ public void setUp() {
+ sortModel = SortModels.createTestSortModel();
+
+ Random rand = new Random();
+ MatrixCursor c = new MatrixCursor(COLUMNS);
+ for (int i = 0; i < ITEM_COUNT; ++i) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+ row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+ row.add(Document.COLUMN_FLAGS, Document.FLAG_SUPPORTS_DELETE);
+ // Generate random document names and sizes. This forces the model's internal sort code
+ // to actually do something.
+ row.add(Document.COLUMN_DISPLAY_NAME, NAMES[i]);
+ row.add(Document.COLUMN_SIZE, rand.nextInt());
+ }
+
+ cursor = c;
+ }
+
+ // Tests sorting ascending by item name.
+ @Test
+ public void testSort_names_ascending() {
+ BitSet seen = new BitSet(ITEM_COUNT);
+ List<String> names = new ArrayList<>();
+
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_TITLE,
+ SortDimension.SORT_DIRECTION_ASCENDING);
+
+ final Cursor cursor = createSortingCursorWrapper();
+
+ for (int i = 0; i < cursor.getCount(); ++i) {
+ cursor.moveToPosition(i);
+ seen.set(Integer.parseInt(
+ DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+ names.add(getCursorString(cursor, Document.COLUMN_DISPLAY_NAME));
+ }
+
+ assertEquals(ITEM_COUNT, seen.cardinality());
+ for (int i = 0; i < names.size()-1; ++i) {
+ assertTrue(Shared.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) <= 0);
+ }
+ }
+
+ // Tests sorting descending by item name.
+ @Test
+ public void testSort_names_descending() {
+ BitSet seen = new BitSet(ITEM_COUNT);
+ List<String> names = new ArrayList<>();
+
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_TITLE,
+ SortDimension.SORT_DIRECTION_DESCENDING);
+
+ final Cursor cursor = createSortingCursorWrapper();
+
+ for (int i = 0; i < cursor.getCount(); ++i) {
+ cursor.moveToPosition(i);
+ seen.set(Integer.parseInt(
+ DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+ names.add(getCursorString(cursor, Document.COLUMN_DISPLAY_NAME));
+ }
+
+ assertEquals(ITEM_COUNT, seen.cardinality());
+ for (int i = 0; i < names.size()-1; ++i) {
+ assertTrue(Shared.compareToIgnoreCaseNullable(names.get(i), names.get(i+1)) >= 0);
+ }
+ }
+
+ // Tests sorting by item size.
+ @Test
+ public void testSort_sizes_ascending() {
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+ SortDimension.SORT_DIRECTION_ASCENDING);
+
+ final Cursor cursor = createSortingCursorWrapper();
+
+ BitSet seen = new BitSet(ITEM_COUNT);
+ int previousSize = Integer.MIN_VALUE;
+ for (int i = 0; i < cursor.getCount(); ++i) {
+ cursor.moveToPosition(i);
+ seen.set(Integer.parseInt(
+ DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+ // Check sort order - descending numerical
+ int size = DocumentInfo.getCursorInt(cursor, Document.COLUMN_SIZE);
+ assertTrue(previousSize <= size);
+ previousSize = size;
+ }
+ // Check that all items were accounted for.
+ assertEquals(ITEM_COUNT, seen.cardinality());
+ }
+
+ // Tests sorting by item size.
+ @Test
+ public void testSort_sizes_descending() {
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+ SortDimension.SORT_DIRECTION_DESCENDING);
+
+ Cursor cursor = createSortingCursorWrapper();
+
+ BitSet seen = new BitSet(ITEM_COUNT);
+ int previousSize = Integer.MAX_VALUE;
+ for (int i = 0; i < cursor.getCount(); ++i) {
+ cursor.moveToPosition(i);
+ seen.set(Integer.parseInt(
+ DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+ // Check sort order - descending numerical
+ int size = DocumentInfo.getCursorInt(cursor, Document.COLUMN_SIZE);
+ assertTrue(previousSize >= size);
+ previousSize = size;
+ }
+ // Check that all items were accounted for.
+ assertEquals(ITEM_COUNT, seen.cardinality());
+ }
+
+ // Tests that directories and files are properly bucketed when sorting by size
+ @Test
+ public void testSort_sizesWithBucketing_ascending() {
+ MatrixCursor c = new MatrixCursor(COLUMNS);
+
+ for (int i = 0; i < ITEM_COUNT; ++i) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+ row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+ row.add(Document.COLUMN_SIZE, i);
+ // Interleave directories and text files.
+ String mimeType =(i % 2 == 0) ? Document.MIME_TYPE_DIR : "text/*";
+ row.add(Document.COLUMN_MIME_TYPE, mimeType);
+ }
+
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+ SortDimension.SORT_DIRECTION_ASCENDING);
+
+ final Cursor cursor = createSortingCursorWrapper(c);
+
+ boolean seenAllDirs = false;
+ int previousSize = Integer.MIN_VALUE;
+ BitSet seen = new BitSet(ITEM_COUNT);
+ // Iterate over items in sort order. Once we've encountered a document (i.e. not a
+ // directory), all subsequent items must also be documents. That is, all directories are
+ // bucketed at the front of the list, sorted by size, followed by documents, sorted by size.
+ for (int i = 0; i < cursor.getCount(); ++i) {
+ cursor.moveToPosition(i);
+ seen.set(Integer.parseInt(
+ DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+
+ String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+ if (seenAllDirs) {
+ assertFalse(Document.MIME_TYPE_DIR.equals(mimeType));
+ } else {
+ if (!Document.MIME_TYPE_DIR.equals(mimeType)) {
+ seenAllDirs = true;
+ // Reset the previous size seen, because documents are bucketed separately by
+ // the sort.
+ previousSize = Integer.MIN_VALUE;
+ }
+ }
+ // Check sort order - descending numerical
+ int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
+ assertTrue(previousSize <= size);
+ previousSize = size;
+ }
+
+ // Check that all items were accounted for.
+ assertEquals(ITEM_COUNT, seen.cardinality());
+ }
+
+ // Tests that directories and files are properly bucketed when sorting by size
+ @Test
+ public void testSort_sizesWithBucketing_descending() {
+ MatrixCursor c = new MatrixCursor(COLUMNS);
+
+ for (int i = 0; i < ITEM_COUNT; ++i) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+ row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+ row.add(Document.COLUMN_SIZE, i);
+ // Interleave directories and text files.
+ String mimeType =(i % 2 == 0) ? Document.MIME_TYPE_DIR : "text/*";
+ row.add(Document.COLUMN_MIME_TYPE, mimeType);
+ }
+
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_SIZE,
+ SortDimension.SORT_DIRECTION_DESCENDING);
+
+ final Cursor cursor = createSortingCursorWrapper(c);
+
+ boolean seenAllDirs = false;
+ int previousSize = Integer.MAX_VALUE;
+ BitSet seen = new BitSet(ITEM_COUNT);
+ // Iterate over items in sort order. Once we've encountered a document (i.e. not a
+ // directory), all subsequent items must also be documents. That is, all directories are
+ // bucketed at the front of the list, sorted by size, followed by documents, sorted by size.
+ for (int i = 0; i < cursor.getCount(); ++i) {
+ cursor.moveToPosition(i);
+ seen.set(cursor.getPosition());
+
+ String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
+ if (seenAllDirs) {
+ assertFalse(Document.MIME_TYPE_DIR.equals(mimeType));
+ } else {
+ if (!Document.MIME_TYPE_DIR.equals(mimeType)) {
+ seenAllDirs = true;
+ // Reset the previous size seen, because documents are bucketed separately by
+ // the sort.
+ previousSize = Integer.MAX_VALUE;
+ }
+ }
+ // Check sort order - descending numerical
+ int size = DocumentInfo.getCursorInt(c, Document.COLUMN_SIZE);
+ assertTrue(previousSize >= size);
+ previousSize = size;
+ }
+
+ // Check that all items were accounted for.
+ assertEquals(ITEM_COUNT, seen.cardinality());
+ }
+
+ @Test
+ public void testSort_time_ascending() {
+ final int DL_COUNT = 3;
+ MatrixCursor c = new MatrixCursor(COLUMNS);
+ Set<String> currentDownloads = new HashSet<>();
+
+ // Add some files
+ for (int i = 0; i < ITEM_COUNT; i++) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+ row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+ row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
+ }
+ // Add some current downloads (no timestamp)
+ for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ String id = Integer.toString(i);
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+ row.add(Document.COLUMN_DOCUMENT_ID, id);
+ currentDownloads.add(id);
+ }
+
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_DATE,
+ SortDimension.SORT_DIRECTION_ASCENDING);
+
+ final Cursor cursor = createSortingCursorWrapper(c);
+
+ // Check that all items were accounted for
+ assertEquals(ITEM_COUNT + DL_COUNT, cursor.getCount());
+
+ // Check that active downloads are sorted to the bottom.
+ for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
+ assertTrue(currentDownloads.contains(
+ DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+ }
+ }
+
+ @Test
+ public void testSort_time_descending() {
+ final int DL_COUNT = 3;
+ MatrixCursor c = new MatrixCursor(COLUMNS);
+ Set<String> currentDownloads = new HashSet<>();
+
+ // Add some files
+ for (int i = 0; i < ITEM_COUNT; i++) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+ row.add(Document.COLUMN_DOCUMENT_ID, Integer.toString(i));
+ row.add(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());
+ }
+ // Add some current downloads (no timestamp)
+ for (int i = ITEM_COUNT; i < ITEM_COUNT + DL_COUNT; i++) {
+ MatrixCursor.RowBuilder row = c.newRow();
+ String id = Integer.toString(i);
+ row.add(RootCursorWrapper.COLUMN_AUTHORITY, AUTHORITY);
+ row.add(Document.COLUMN_DOCUMENT_ID, id);
+ currentDownloads.add(id);
+ }
+
+ sortModel.sortByUser(SortModel.SORT_DIMENSION_ID_DATE,
+ SortDimension.SORT_DIRECTION_DESCENDING);
+
+ final Cursor cursor = createSortingCursorWrapper(c);
+
+ // Check that all items were accounted for
+ assertEquals(ITEM_COUNT + DL_COUNT, cursor.getCount());
+
+ // Check that active downloads are sorted to the top.
+ for (int i = 0; i < DL_COUNT; i++) {
+ assertTrue(currentDownloads.contains(
+ DocumentInfo.getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)));
+ }
+ }
+
+ private Cursor createSortingCursorWrapper() {
+ return createSortingCursorWrapper(cursor);
+ }
+
+ private Cursor createSortingCursorWrapper(Cursor c) {
+ final @SortDimensionId int id = sortModel.getSortedDimensionId();
+ return new SortingCursorWrapper(c, sortModel.getDimensionById(id));
+ }
+}