Merge "Allow Drag and Drop to form new selection with CTRL held down." into nyc-andromeda-dev
diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml
index bfed16c..7a3a9d2 100644
--- a/res/values-nl/strings.xml
+++ b/res/values-nl/strings.xml
@@ -132,14 +132,14 @@
<item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g> item</item>
</plurals>
<string name="delete_filename_confirmation_message" msgid="8338069763240613258">"\'<xliff:g id="NAME">%1$s</xliff:g>\' verwijderen?"</string>
- <string name="delete_foldername_confirmation_message" msgid="9084085260877704140">"Map \'<xliff:g id="NAME">%1$s</xliff:g>\' en de bijbehorende inhoud verwijderen?"</string>
+ <string name="delete_foldername_confirmation_message" msgid="9084085260877704140">"Map \'<xliff:g id="NAME">%1$s</xliff:g>\' en de bijbehorende content verwijderen?"</string>
<plurals name="delete_files_confirmation_message" formatted="false" msgid="4866664063250034142">
<item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> bestanden verwijderen?</item>
<item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g> bestand verwijderen?</item>
</plurals>
<plurals name="delete_folders_confirmation_message" formatted="false" msgid="1028946402799686388">
- <item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> mappen en de bijbehorende inhoud verwijderen?</item>
- <item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g> map en de bijbehorende inhoud verwijderen?</item>
+ <item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> mappen en de bijbehorende content verwijderen?</item>
+ <item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g> map en de bijbehorende content verwijderen?</item>
</plurals>
<plurals name="delete_items_confirmation_message" formatted="false" msgid="7285090426511028179">
<item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> items verwijderen?</item>
diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml
index 466e90b..2c34546 100644
--- a/res/values-sr/strings.xml
+++ b/res/values-sr/strings.xml
@@ -46,7 +46,7 @@
<string name="button_copy" msgid="8219059853840996027">"Копирај"</string>
<string name="button_move" msgid="8596460499325291272">"Премести"</string>
<string name="button_dismiss" msgid="7235249361023803349">"Одбаци"</string>
- <string name="button_retry" msgid="4011461781916631389">"Покушај поново"</string>
+ <string name="button_retry" msgid="4011461781916631389">"Пробај поново"</string>
<string name="not_sorted" msgid="7813496644889115530">"Нису сортирани"</string>
<string name="sort_dimension_name" msgid="6325591541414177579">"Назив"</string>
<string name="sort_dimension_summary" msgid="7724534446881397860">"Резиме"</string>
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index 5088149..0809a37 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -22,6 +22,7 @@
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Parcelable;
+import android.support.annotation.VisibleForTesting;
import com.android.documentsui.AbstractActionHandler.CommonAddons;
import com.android.documentsui.base.BooleanConsumer;
@@ -31,6 +32,7 @@
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
+import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.dirlist.AnimationView.AnimationType;
import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.files.LauncherActivity;
@@ -55,6 +57,7 @@
protected final RootsAccess mRoots;
protected final DocumentsAccess mDocs;
protected final SelectionManager mSelectionMgr;
+ protected final SearchViewManager mSearchMgr;
protected final Lookup<String, Executor> mExecutors;
public AbstractActionHandler(
@@ -63,6 +66,7 @@
RootsAccess roots,
DocumentsAccess docs,
SelectionManager selectionMgr,
+ SearchViewManager searchMgr,
Lookup<String, Executor> executors) {
assert(activity != null);
@@ -76,6 +80,7 @@
mRoots = roots;
mDocs = docs;
mSelectionMgr = selectionMgr;
+ mSearchMgr = searchMgr;
mExecutors = executors;
}
@@ -151,11 +156,31 @@
}
@Override
+ public void openContainerDocument(DocumentInfo doc) {
+ assert(doc.isContainer());
+
+ mActivity.notifyDirectoryNavigated(doc.derivedUri);
+
+ mState.pushDocument(doc);
+ // Show an opening animation only if pressing "back" would get us back to the
+ // previous directory. Especially after opening a root document, pressing
+ // back, wouldn't go to the previous root, but close the activity.
+ final int anim = (mState.hasLocationChanged() && mState.stack.size() > 1)
+ ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
+ mActivity.refreshCurrentRootAndDirectory(anim);
+ }
+
+ @Override
public void deleteSelectedDocuments() {
throw new UnsupportedOperationException("Delete not supported!");
}
@Override
+ public void shareSelectedDocuments() {
+ throw new UnsupportedOperationException("Share not supported!");
+ }
+
+ @Override
public final void loadDocument(Uri uri) {
new OpenUriForViewTask<>(mActivity, mState, mRoots, mDocs, uri)
.executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
@@ -174,6 +199,7 @@
protected Selection getStableSelection() {
return mSelectionMgr.getSelection(new Selection());
}
+
/**
* A class primarily for the support of isolating our tests
* from our concrete activity implementations.
@@ -184,9 +210,11 @@
// 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();
void setRootsDrawerOpen(boolean open);
+
+ @VisibleForTesting
+ void notifyDirectoryNavigated(Uri docUri);
}
}
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index fe6be0e..98d24ed 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -65,8 +65,12 @@
void showChooserForDoc(DocumentInfo doc);
+ void openContainerDocument(DocumentInfo doc);
+
void deleteSelectedDocuments();
+ void shareSelectedDocuments();
+
/**
* Called when initial activity setup is complete. Implementations
* should override this method to set the initial location of the
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 005f887..235757e 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -32,11 +32,9 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
-import android.database.ContentObserver;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
-import android.os.Handler;
import android.os.MessageQueue.IdleHandler;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Root;
@@ -59,7 +57,6 @@
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.base.Events;
import com.android.documentsui.base.LocalPreferences;
-import com.android.documentsui.base.PairedTask;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
@@ -80,12 +77,11 @@
import com.android.documentsui.ui.MessageBuilder;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executor;
-public abstract class BaseActivity
+public abstract class BaseActivity<T extends ActionHandler>
extends Activity implements CommonAddons, NavigationViewManager.Environment {
private static final String BENCHMARK_TESTING_PACKAGE = "com.android.documentsui.appperftests";
@@ -95,25 +91,23 @@
protected @Nullable RetainedState mRetainedState;
protected RootsCache mRoots;
+ protected DocumentsAccess mDocs;
protected MessageBuilder mMessages;
protected DrawerController mDrawer;
protected NavigationViewManager mNavigator;
protected FocusManager mFocusManager;
protected SortController mSortController;
+ protected T mActions;
+
private final List<EventListener> mEventListeners = new ArrayList<>();
private final String mTag;
- private final ContentObserver mRootsCacheObserver = new ContentObserver(
- new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- new HandleRootsChangedTask(BaseActivity.this).execute(getCurrentRoot());
- }
- };
@LayoutRes
private int mLayoutId;
+ private RootsMonitor<BaseActivity> mRootsMonitor;
+
private boolean mNavDrawerHasFocus;
private long mStartTime;
@@ -202,9 +196,8 @@
// support to that fragment.
mRetainedState = (RetainedState) getLastNonConfigurationInstance();
mRoots = DocumentsApplication.getRootsCache(this);
+ mDocs = DocumentsAccess.create(this);
mMessages = new MessageBuilder(this);
- getContentResolver().registerContentObserver(
- RootsCache.sNotificationUri, false, mRootsCacheObserver);
DocumentsToolbar toolbar = (DocumentsToolbar) findViewById(R.id.toolbar);
setActionBar(toolbar);
@@ -246,6 +239,20 @@
}
@Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+
+ mRootsMonitor = new RootsMonitor<>(
+ this,
+ mActions,
+ mRoots,
+ mDocs,
+ mState,
+ mSearchManager);
+ mRootsMonitor.start();
+ }
+
+ @Override
public boolean onCreateOptionsMenu(Menu menu) {
boolean showMenu = super.onCreateOptionsMenu(menu);
@@ -267,7 +274,7 @@
@Override
protected void onDestroy() {
- getContentResolver().unregisterContentObserver(mRootsCacheObserver);
+ mRootsMonitor.stop();
super.onDestroy();
}
@@ -344,7 +351,7 @@
new GetRootDocumentTask(
root,
this,
- this::openContainerDocument)
+ mActions::openContainerDocument)
.executeOnExecutor(getExecutorForCurrentDirectory());
}
}
@@ -406,22 +413,6 @@
&& !root.isDownloads();
}
- // TODO: Move to ActionHandler...currently blocked by the notifyDirectory....business.
- @Override
- public void openContainerDocument(DocumentInfo doc) {
- assert(doc.isContainer());
-
- notifyDirectoryNavigated(doc.derivedUri);
-
- mState.pushDocument(doc);
- // Show an opening animation only if pressing "back" would get us back to the
- // previous directory. Especially after opening a root document, pressing
- // back, wouldn't go to the previous root, but close the activity.
- final int anim = (mState.hasLocationChanged() && mState.stack.size() > 1)
- ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
- refreshCurrentRootAndDirectory(anim);
- }
-
/**
* Refreshes the content of the director and the menu/action bar.
* The current directory name and selection will get updated.
@@ -638,21 +629,26 @@
return super.onKeyDown(keyCode, event);
}
+ @VisibleForTesting
public void addEventListener(EventListener listener) {
mEventListeners.add(listener);
}
+ @VisibleForTesting
public void removeEventListener(EventListener listener) {
mEventListeners.remove(listener);
}
+ @VisibleForTesting
public void notifyDirectoryLoaded(Uri uri) {
for (EventListener listener : mEventListeners) {
listener.onDirectoryLoaded(uri);
}
}
- void notifyDirectoryNavigated(Uri uri) {
+ @VisibleForTesting
+ @Override
+ public void notifyDirectoryNavigated(Uri uri) {
for (EventListener listener : mEventListeners) {
listener.onDirectoryNavigated(uri);
}
@@ -728,62 +724,6 @@
});
}
- private static final class HandleRootsChangedTask
- extends PairedTask<BaseActivity, RootInfo, RootInfo> {
- RootInfo mCurrentRoot;
- DocumentInfo mDefaultRootDocument;
-
- public HandleRootsChangedTask(BaseActivity activity) {
- super(activity);
- }
-
- @Override
- protected RootInfo run(RootInfo... roots) {
- assert(roots.length == 1);
- mCurrentRoot = roots[0];
- final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
- for (final RootInfo root : cachedRoots) {
- if (root.getUri().equals(mCurrentRoot.getUri())) {
- // We don't need to change the current root as the current root was not removed.
- return null;
- }
- }
-
- // Choose the default root.
- final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState);
- assert(defaultRoot != null);
- if (!defaultRoot.isRecents()) {
- mDefaultRootDocument = defaultRoot.getRootDocumentBlocking(mOwner);
- }
- return defaultRoot;
- }
-
- @Override
- protected void finish(RootInfo defaultRoot) {
- if (defaultRoot == null) {
- return;
- }
-
- // If the activity has been launched for the specific root and it is removed, finish the
- // activity.
- final Uri uri = mOwner.getIntent().getData();
- if (uri != null && uri.equals(mCurrentRoot.getUri())) {
- mOwner.finish();
- return;
- }
-
- // Clear entire backstack and start in new root.
- mOwner.mState.onRootChanged(defaultRoot);
- mOwner.mSearchManager.update(defaultRoot);
-
- if (defaultRoot.isRecents()) {
- mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
- } else {
- mOwner.openContainerDocument(mDefaultRootDocument);
- }
- }
- }
-
public final class RetainedState {
public @Nullable Selection selection;
diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java
index d2c7d10..56e76bb 100644
--- a/src/com/android/documentsui/DirectoryLoader.java
+++ b/src/com/android/documentsui/DirectoryLoader.java
@@ -20,9 +20,11 @@
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
+import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.CancellationSignal;
+import android.os.Handler;
import android.os.OperationCanceledException;
import android.os.RemoteException;
import android.provider.DocumentsContract.Document;
@@ -42,8 +44,7 @@
private static final String[] SEARCH_REJECT_MIMES = new String[] { Document.MIME_TYPE_DIR };
- private final ForceLoadContentObserver mObserver = new ForceLoadContentObserver();
-
+ private final LockingContentObserver mObserver;
private final RootInfo mRoot;
private final Uri mUri;
private final SortModel mModel;
@@ -59,6 +60,7 @@
DocumentInfo doc,
Uri uri,
SortModel model,
+ DirectoryReloadLock lock,
boolean inSearchMode) {
super(context, ProviderExecutor.forAuthority(root.authority));
@@ -67,6 +69,7 @@
mModel = model;
mDoc = doc;
mSearchMode = inSearchMode;
+ mObserver = new LockingContentObserver(lock, this::onContentChanged);
}
@Override
@@ -181,4 +184,25 @@
getContext().getContentResolver().unregisterContentObserver(mObserver);
}
+
+ private static final class LockingContentObserver extends ContentObserver {
+ private final DirectoryReloadLock mLock;
+ private final Runnable mContentChangedCallback;
+
+ public LockingContentObserver(DirectoryReloadLock lock, Runnable contentChangedCallback) {
+ super(new Handler());
+ mLock = lock;
+ mContentChangedCallback = contentChangedCallback;
+ }
+
+ @Override
+ public boolean deliverSelfNotifications() {
+ return true;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mLock.tryUpdate(mContentChangedCallback);
+ }
+ }
}
diff --git a/src/com/android/documentsui/DirectoryReloadLock.java b/src/com/android/documentsui/DirectoryReloadLock.java
new file mode 100644
index 0000000..8033bb7
--- /dev/null
+++ b/src/com/android/documentsui/DirectoryReloadLock.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+import android.annotation.MainThread;
+import android.annotation.Nullable;
+
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.selection.BandController;
+
+/**
+ * A lock used by {@link DirectoryLoader} and {@link BandController} to ensure refresh is blocked
+ * while Band Selection is active.
+ */
+public final class DirectoryReloadLock {
+ private int mPauseCount = 0;
+ private @Nullable Runnable mCallback;
+
+ /**
+ * Increment the block count by 1
+ */
+ @MainThread
+ public void block() {
+ Shared.checkMainLoop();
+ mPauseCount++;
+ }
+
+ /**
+ * Decrement the block count by 1; If no other object is trying to block and there exists some
+ * callback, that callback will be run
+ */
+ @MainThread
+ public void unblock() {
+ Shared.checkMainLoop();
+ mPauseCount--;
+ if (mPauseCount == 0 && mCallback != null) {
+ mCallback.run();
+ mCallback = null;
+ }
+ }
+
+ /**
+ * Attempts to run the given Runnable if not-blocked, or else the Runnable is set to be ran next
+ * (replacing any previous set Runnables).
+ */
+ public void tryUpdate(Runnable update) {
+ if (mPauseCount == 0) {
+ update.run();
+ } else {
+ mCallback = update;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/documentsui/IconUtils.java b/src/com/android/documentsui/IconUtils.java
index 52f3e73..5a87f87 100644
--- a/src/com/android/documentsui/IconUtils.java
+++ b/src/com/android/documentsui/IconUtils.java
@@ -44,12 +44,6 @@
public static Drawable loadMimeIcon(
Context context, String mimeType, String authority, String docId, int mode) {
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
- // TODO: eventually move these hacky assets into that package
- if ("com.android.providers.media.documents".equals(authority)
- && docId.startsWith("album")) {
- return context.getDrawable(R.drawable.ic_doc_album);
- }
-
if (mode == State.MODE_GRID) {
return context.getDrawable(R.drawable.ic_grid_folder);
} else {
diff --git a/src/com/android/documentsui/OnRootsChangedTask.java b/src/com/android/documentsui/OnRootsChangedTask.java
deleted file mode 100644
index 0776e8f..0000000
--- a/src/com/android/documentsui/OnRootsChangedTask.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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;
-
-import android.net.Uri;
-
-import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.PairedTask;
-import com.android.documentsui.base.RootInfo;
-import com.android.documentsui.dirlist.AnimationView;
-
-import java.util.Collection;
-
-final class OnRootsChangedTask
- extends PairedTask<BaseActivity, RootInfo, RootInfo> {
- RootInfo mCurrentRoot;
- DocumentInfo mDefaultRootDocument;
-
- public OnRootsChangedTask(BaseActivity activity) {
- super(activity);
- }
-
- @Override
- protected RootInfo run(RootInfo... roots) {
- assert(roots.length == 1);
- mCurrentRoot = roots[0];
- final Collection<RootInfo> cachedRoots = mOwner.mRoots.getRootsBlocking();
- for (final RootInfo root : cachedRoots) {
- if (root.getUri().equals(mCurrentRoot.getUri())) {
- // We don't need to change the current root as the current root was not removed.
- return null;
- }
- }
-
- // Choose the default root.
- final RootInfo defaultRoot = mOwner.mRoots.getDefaultRootBlocking(mOwner.mState);
- assert(defaultRoot != null);
- if (!defaultRoot.isRecents()) {
- mDefaultRootDocument = defaultRoot.getRootDocumentBlocking(mOwner);
- }
- return defaultRoot;
- }
-
- @Override
- protected void finish(RootInfo defaultRoot) {
- if (defaultRoot == null) {
- return;
- }
-
- // If the activity has been launched for the specific root and it is removed, finish the
- // activity.
- final Uri uri = mOwner.getIntent().getData();
- if (uri != null && uri.equals(mCurrentRoot.getUri())) {
- mOwner.finish();
- return;
- }
-
- // Clear entire backstack and start in new root.
- mOwner.mState.onRootChanged(defaultRoot);
- mOwner.mSearchManager.update(defaultRoot);
-
- if (defaultRoot.isRecents()) {
- mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
- } else {
- mOwner.openContainerDocument(mDefaultRootDocument);
- }
- }
-}
\ No newline at end of file
diff --git a/src/com/android/documentsui/RecentsLoader.java b/src/com/android/documentsui/RecentsLoader.java
index 6ce1896..c3ccbba 100644
--- a/src/com/android/documentsui/RecentsLoader.java
+++ b/src/com/android/documentsui/RecentsLoader.java
@@ -140,7 +140,7 @@
// Collect all finished tasks
boolean allDone = true;
int totalQuerySize = 0;
- List<Cursor> cursors = new ArrayList<>();
+ List<Cursor> cursors = new ArrayList<>(mTasks.size());
for (RecentsTask task : mTasks.values()) {
if (task.isDone()) {
try {
@@ -168,6 +168,10 @@
throw new RuntimeException(e);
} catch (ExecutionException e) {
// We already logged on other side
+ } catch (Exception e) {
+ // Catch exceptions thrown when we read the cursor.
+ Log.e(TAG, "Failed to query Recents for authority: " + task.authority
+ + ". Skip this authority in Recents.", e);
}
} else {
allDone = false;
@@ -189,7 +193,6 @@
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
diff --git a/src/com/android/documentsui/RootsMonitor.java b/src/com/android/documentsui/RootsMonitor.java
new file mode 100644
index 0000000..feead7a
--- /dev/null
+++ b/src/com/android/documentsui/RootsMonitor.java
@@ -0,0 +1,147 @@
+/*
+ * 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;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.documentsui.AbstractActionHandler.CommonAddons;
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.PairedTask;
+import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.base.State;
+import com.android.documentsui.dirlist.AnimationView;
+import com.android.documentsui.roots.RootsAccess;
+
+import java.util.Collection;
+
+/**
+ * Monitors roots change and refresh the page when necessary.
+ */
+final class RootsMonitor<T extends Activity & CommonAddons> {
+
+ private final ContentResolver mResolver;
+ private final ContentObserver mObserver;
+
+ RootsMonitor(
+ final T activity,
+ final ActionHandler actions,
+ final RootsAccess roots,
+ final DocumentsAccess docs,
+ final State state,
+ final SearchViewManager searchMgr) {
+ mResolver = activity.getContentResolver();
+
+ mObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
+ @Override
+ public void onChange(boolean selfChange) {
+ new HandleRootsChangedTask<T>(
+ activity,
+ actions,
+ roots,
+ docs,
+ state,
+ searchMgr).execute(activity.getCurrentRoot());
+ }
+ };
+ }
+
+ void start() {
+ mResolver.registerContentObserver(RootsAccess.NOTIFICATION_URI, false, mObserver);
+ }
+
+ void stop() {
+ mResolver.unregisterContentObserver(mObserver);
+ }
+
+ private static class HandleRootsChangedTask<T extends Activity & CommonAddons>
+ extends PairedTask<T, RootInfo, RootInfo> {
+ private final ActionHandler mActions;
+ private final RootsAccess mRoots;
+ private final DocumentsAccess mDocs;
+ private final State mState;
+ private final SearchViewManager mSearchMgr;
+
+ private RootInfo mCurrentRoot;
+ private DocumentInfo mDefaultRootDocument;
+
+ private HandleRootsChangedTask(
+ T activity,
+ ActionHandler actions,
+ RootsAccess roots,
+ DocumentsAccess docs,
+ State state,
+ SearchViewManager searchMgr) {
+ super(activity);
+ mActions = actions;
+ mRoots = roots;
+ mDocs = docs;
+ mState = state;
+ mSearchMgr = searchMgr;
+ }
+
+ @Override
+ protected RootInfo run(RootInfo... roots) {
+ assert (roots.length == 1);
+ mCurrentRoot = roots[0];
+ final Collection<RootInfo> cachedRoots = mRoots.getRootsBlocking();
+ for (final RootInfo root : cachedRoots) {
+ if (root.getUri().equals(mCurrentRoot.getUri())) {
+ // We don't need to change the current root as the current root was not removed.
+ return null;
+ }
+ }
+
+ // Choose the default root.
+ final RootInfo defaultRoot = mRoots.getDefaultRootBlocking(mState);
+ assert (defaultRoot != null);
+ if (!defaultRoot.isRecents()) {
+ mDefaultRootDocument = mDocs.getRootDocument(defaultRoot);
+ }
+ return defaultRoot;
+ }
+
+ @Override
+ protected void finish(RootInfo defaultRoot) {
+ if (defaultRoot == null) {
+ return;
+ }
+
+ // If the activity has been launched for the specific root and it is removed, finish the
+ // activity.
+ final Uri uri = mOwner.getIntent().getData();
+ if (uri != null && uri.equals(mCurrentRoot.getUri())) {
+ mOwner.finish();
+ return;
+ }
+
+ // Clear entire backstack and start in new root.
+ mState.onRootChanged(defaultRoot);
+ mSearchMgr.update(defaultRoot);
+
+ if (defaultRoot.isRecents()) {
+ mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
+ } else {
+ mActions.openContainerDocument(mDefaultRootDocument);
+ }
+ }
+ }
+}
diff --git a/src/com/android/documentsui/base/FilteringCursorWrapper.java b/src/com/android/documentsui/base/FilteringCursorWrapper.java
index 6604171..0288f4f 100644
--- a/src/com/android/documentsui/base/FilteringCursorWrapper.java
+++ b/src/com/android/documentsui/base/FilteringCursorWrapper.java
@@ -55,13 +55,13 @@
while (cursor.moveToNext() && mCount < count) {
final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
final long lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
- if (rejectMimes != null && MimePredicate.mimeMatches(rejectMimes, mimeType)) {
+ if (rejectMimes != null && MimeTypes.mimeMatches(rejectMimes, mimeType)) {
continue;
}
if (lastModified < rejectBefore) {
continue;
}
- if (MimePredicate.mimeMatches(acceptMimes, mimeType)) {
+ if (MimeTypes.mimeMatches(acceptMimes, mimeType)) {
mPosition[mCount++] = cursor.getPosition();
}
}
diff --git a/src/com/android/documentsui/base/MimePredicate.java b/src/com/android/documentsui/base/MimeTypes.java
similarity index 76%
rename from src/com/android/documentsui/base/MimePredicate.java
rename to src/com/android/documentsui/base/MimeTypes.java
index b3ba862..0f61e67 100644
--- a/src/com/android/documentsui/base/MimePredicate.java
+++ b/src/com/android/documentsui/base/MimeTypes.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,16 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.android.documentsui.base;
import android.annotation.Nullable;
import android.provider.DocumentsContract.Document;
-import com.android.internal.util.Predicate;
+import java.util.List;
-public class MimePredicate implements Predicate<DocumentInfo> {
- private final String[] mFilters;
+public final class MimeTypes {
+
+ private MimeTypes() {}
private static final String APK_TYPE = "application/vnd.android.package-archive";
/**
@@ -31,19 +31,28 @@
*/
public static final String[] VISUAL_MIMES = new String[] { "image/*", "video/*" };
- public MimePredicate(String[] filters) {
- mFilters = filters;
- }
+ public static String findCommonMimeType(List<String> mimeTypes) {
+ String[] commonType = mimeTypes.get(0).split("/");
+ if (commonType.length != 2) {
+ return "*/*";
+ }
- @Override
- public boolean apply(DocumentInfo doc) {
- if (doc.isDirectory()) {
- return true;
+ for (int i = 1; i < mimeTypes.size(); i++) {
+ String[] type = mimeTypes.get(i).split("/");
+ if (type.length != 2) continue;
+
+ if (!commonType[1].equals(type[1])) {
+ commonType[1] = "*";
+ }
+
+ if (!commonType[0].equals(type[0])) {
+ commonType[0] = "*";
+ commonType[1] = "*";
+ break;
+ }
}
- if (mimeMatches(mFilters, doc.mimeType)) {
- return true;
- }
- return false;
+
+ return commonType[0] + "/" + commonType[1];
}
public static boolean mimeMatches(String[] filters, String[] tests) {
diff --git a/src/com/android/documentsui/base/RootInfo.java b/src/com/android/documentsui/base/RootInfo.java
index f84f915..3a19ef7 100644
--- a/src/com/android/documentsui/base/RootInfo.java
+++ b/src/com/android/documentsui/base/RootInfo.java
@@ -350,7 +350,7 @@
}
/**
- * @deprecate use {@link DocumentsAccess#getRootDocumentBlocking}.
+ * @deprecate use {@link DocumentsAccess#getRootDocument}.
*/
@Deprecated
public @Nullable DocumentInfo getRootDocumentBlocking(Context context) {
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 306644d..e230311 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -67,6 +67,7 @@
import com.android.documentsui.BaseActivity.RetainedState;
import com.android.documentsui.DirectoryLoader;
import com.android.documentsui.DirectoryResult;
+import com.android.documentsui.DirectoryReloadLock;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.FocusManager;
import com.android.documentsui.ItemDragListener;
@@ -117,8 +118,7 @@
* Display the documents inside a single directory.
*/
public class DirectoryFragment extends Fragment
- implements DocumentsAdapter.Environment, LoaderCallbacks<DirectoryResult>,
- ItemDragListener.DragHost, SwipeRefreshLayout.OnRefreshListener {
+ implements ItemDragListener.DragHost, SwipeRefreshLayout.OnRefreshListener {
@IntDef(flag = true, value = {
TYPE_NORMAL,
@@ -142,8 +142,12 @@
private static final int CACHE_EVICT_LIMIT = 100;
private static final int REFRESH_SPINNER_DISMISS_DELAY = 500;
+ private BaseActivity<?> mActivity;
+ private State mState;
private final Model mModel = new Model();
private final EventListener<Model.Update> mModelUpdateListener = new ModelUpdateListener();
+ private final DocumentsAdapter.Environment mAdapterEnv = new AdapterEnvironment();
+ private final LoaderCallbacks<DirectoryResult> mLoaderCallbacks = new LoaderBindings();
// This dependency is informally "injected" from the owning Activity in our onCreate method.
private ActivityConfig mActivityConfig;
@@ -183,6 +187,7 @@
private View mProgressBar;
private DirectoryState mLocalState;
+ private DirectoryReloadLock mReloadLock = new DirectoryReloadLock();
// Note, we use !null to indicate that selection was restored (from rotation).
// So don't fiddle with this field unless you've got the bigger picture in mind.
@@ -191,7 +196,7 @@
private SortModel.UpdateListener mSortListener = (model, updateType) -> {
// Only when sort order has changed do we need to trigger another loading.
if ((updateType & SortModel.UPDATE_TYPE_SORTING) != 0) {
- getLoaderManager().restartLoader(LOADER_ID, null, this);
+ getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
}
};
@@ -199,10 +204,11 @@
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ BaseActivity activity = (BaseActivity<?>) getActivity();
final View view = inflater.inflate(R.layout.fragment_directory, container, false);
mMessageBar = MessageBar.create(getChildFragmentManager());
- mProgressBar = getActivity().findViewById(R.id.progressbar);
+ mProgressBar = activity.findViewById(R.id.progressbar);
assert(mProgressBar != null);
mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
@@ -217,10 +223,10 @@
cancelThumbnailTask(holder.itemView);
}
});
- mRecView.setItemAnimator(new DirectoryItemAnimator(getActivity()));
+ mRecView.setItemAnimator(new DirectoryItemAnimator(activity));
mFileList = view.findViewById(R.id.file_list);
- mActivityConfig = getBaseActivity().getActivityConfig();
+ mActivityConfig = activity.getActivityConfig();
mDragHoverListener = mActivityConfig.dragAndDropEnabled()
? DragHoverListener.create(new DirectoryDragListener(this), mRecView)
: null;
@@ -250,8 +256,8 @@
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
- final Context context = getActivity();
- final State state = getDisplayState();
+ mActivity = (BaseActivity) getActivity();
+ mState = mActivity.getDisplayState();
// Read arguments when object created for the first time.
// Restore state if fragment recreated.
@@ -261,7 +267,7 @@
mLocalState.restore(args);
// Restore any selection we may have squirreled away in retained state.
- @Nullable RetainedState retained = getBaseActivity().getRetainedState();
+ @Nullable RetainedState retained = mActivity.getRetainedState();
if (retained != null && retained.hasSelection()) {
// We claim the selection for ourselves and null it out once used
// so we don't have a rando selection hanging around in RetainedState.
@@ -269,11 +275,11 @@
retained.selection = null;
}
- mIconHelper = new IconHelper(context, MODE_GRID);
+ mIconHelper = new IconHelper(mActivity, MODE_GRID);
mClipper = DocumentsApplication.getDocumentClipper(getContext());
mAdapter = new SectionBreakDocumentsAdapterWrapper(
- this, new ModelBackedDocumentsAdapter(this, mIconHelper));
+ mAdapterEnv, new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper));
mRecView.setAdapter(mAdapter);
@@ -294,24 +300,23 @@
mModel.addUpdateListener(mAdapter.getModelUpdateListener());
mModel.addUpdateListener(mModelUpdateListener);
-
- final BaseActivity activity = getBaseActivity();
- mSelectionMgr = activity.getSelectionManager(mAdapter, this::canSetSelectionState);
- mFocusManager = activity.getFocusManager(mRecView, mModel);
- mActions = activity.getActionHandler(mModel, mLocalState.mSearchMode);
- mMenuManager = activity.getMenuManager();
- mDialogs = activity.getDialogController();
+ mSelectionMgr = mActivity.getSelectionManager(mAdapter, this::canSetSelectionState);
+ mFocusManager = mActivity.getFocusManager(mRecView, mModel);
+ mActions = mActivity.getActionHandler(mModel, mLocalState.mSearchMode);
+ mMenuManager = mActivity.getMenuManager();
+ mDialogs = mActivity.getDialogController();
mSelectionMetadata = new SelectionMetadata(mModel::getItem);
mSelectionMgr.addItemCallback(mSelectionMetadata);
- GestureSelector gestureSel = GestureSelector.create(mSelectionMgr, mRecView);
+ GestureSelector gestureSel = GestureSelector.create(mSelectionMgr, mRecView, mReloadLock);
- if (state.allowMultiple) {
+ if (mState.allowMultiple) {
mBandController = new BandController(
mRecView,
mAdapter,
mSelectionMgr,
+ mReloadLock,
(int pos) -> {
// The band selection model only operates on documents and directories.
// Exclude other types of adapter items like whitespace and dividers.
@@ -323,17 +328,17 @@
DragStartListener mDragStartListener = mActivityConfig.dragAndDropEnabled()
? DragStartListener.create(
mIconHelper,
- getContext(),
+ mActivity,
mModel,
mSelectionMgr,
mClipper,
- getDisplayState(),
+ mState,
this::getModelId,
mRecView::findChildViewUnder,
getContext().getDrawable(com.android.internal.R.drawable.ic_doc_generic))
: DragStartListener.DUMMY;
- EventHandler<InputEvent> gestureHandler = state.allowMultiple
+ EventHandler<InputEvent> gestureHandler = mState.allowMultiple
? gestureSel::start
: EventHandler.createStub(false);
mInputHandler = new UserInputHandler<>(
@@ -355,16 +360,16 @@
mInputHandler,
mBandController);
- mMenuManager = activity.getMenuManager();
+ mMenuManager = mActivity.getMenuManager();
- mActionModeController = activity.getActionModeController(
+ mActionModeController = mActivity.getActionModeController(
mSelectionMetadata,
this::handleMenuItemClick,
mRecView);
mSelectionMgr.addCallback(mActionModeController);
- final ActivityManager am = (ActivityManager) context.getSystemService(
+ final ActivityManager am = (ActivityManager) mActivity.getSystemService(
Context.ACTIVITY_SERVICE);
boolean svelte = am.isLowRamDevice() && (mLocalState.mType == TYPE_RECENT_OPEN);
mIconHelper.setThumbnailsEnabled(!svelte);
@@ -375,13 +380,13 @@
? (mLocalState.mDocument.flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0
: true;
// Call this before adding the listener to avoid restarting the loader one more time
- state.sortModel.setDefaultDimension(
+ mState.sortModel.setDefaultDimension(
prefersLastModified
? SortModel.SORT_DIMENSION_ID_DATE
: SortModel.SORT_DIMENSION_ID_TITLE);
// Kick off loader at least once
- getLoaderManager().restartLoader(LOADER_ID, null, this);
+ getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
}
@Override
@@ -389,20 +394,19 @@
super.onStart();
// Add listener to update contents on sort model change
- getDisplayState().sortModel.addListener(mSortListener);
+ mState.sortModel.addListener(mSortListener);
}
@Override
public void onStop() {
super.onStop();
- getDisplayState().sortModel.removeListener(mSortListener);
+ mState.sortModel.removeListener(mSortListener);
// Remember last scroll location
final SparseArray<Parcelable> container = new SparseArray<>();
getView().saveHierarchyState(container);
- final State state = getDisplayState();
- state.dirConfigs.put(mLocalState.getConfigKey(), container);
+ mState.dirConfigs.put(mLocalState.getConfigKey(), container);
}
public void retainState(RetainedState state) {
@@ -417,17 +421,6 @@
}
@Override
- public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
- switch (requestCode) {
- case REQUEST_COPY_DESTINATION:
- handleCopyResult(resultCode, data);
- break;
- default:
- throw new UnsupportedOperationException("Unknown request code: " + requestCode);
- }
- }
-
- @Override
public void onCreateContextMenu(ContextMenu menu,
View v,
ContextMenu.ContextMenuInfo menuInfo) {
@@ -463,7 +456,7 @@
operation.setDestination(data.getParcelableExtra(Shared.EXTRA_STACK));
- FileOperations.start(getBaseActivity(), operation, mDialogs::showFileOperationFailures);
+ FileOperations.start(mActivity, operation, mDialogs::showFileOperationFailures);
}
protected boolean onRightClick(InputEvent e) {
@@ -494,8 +487,7 @@
}
private void updateDisplayState() {
- State state = getDisplayState();
- updateLayout(state.derivedMode);
+ updateLayout(mState.derivedMode);
mRecView.setAdapter(mAdapter);
}
@@ -547,17 +539,6 @@
}
}
- @Override
- public int getColumnCount() {
- return mColumnCount;
- }
-
- // Support method to replace getOwner().foo() with something
- // slightly less clumsy like: getOwner().foo().
- private BaseActivity getBaseActivity() {
- return (BaseActivity) getActivity();
- }
-
private boolean handleMenuItemClick(MenuItem item) {
Selection selection = mSelectionMgr.getSelection(new Selection());
@@ -576,7 +557,7 @@
return true;
case R.id.menu_share:
- shareDocuments(selection);
+ mActions.shareSelectedDocuments();
// TODO: Only finish selection if share action is completed.
mActionModeController.finishActionMode();
return true;
@@ -629,7 +610,7 @@
default:
// See if BaseActivity can handle this particular MenuItem
- if (!getBaseActivity().onOptionsItemSelected(item)) {
+ if (!mActivity.onOptionsItemSelected(item)) {
if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
return false;
}
@@ -659,11 +640,10 @@
// Model must be accessed in UI thread, since underlying cursor is not threadsafe.
List<DocumentInfo> docs = mModel.getDocuments(selected);
- BaseActivity activity = getBaseActivity();
if (docs.size() > 1) {
- activity.onDocumentsPicked(docs);
+ mActivity.onDocumentsPicked(docs);
} else {
- activity.onDocumentPicked(docs.get(0));
+ mActivity.onDocumentPicked(docs.get(0));
}
}
@@ -676,54 +656,6 @@
mActions.showChooserForDoc(doc);
}
- private void shareDocuments(final Selection selected) {
- Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SHARE);
-
- // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
- List<DocumentInfo> docs = mModel.getDocuments(selected);
-
- Intent intent;
-
- // Filter out directories and virtual files - those can't be shared.
- List<DocumentInfo> docsForSend = new ArrayList<>();
- for (DocumentInfo doc: docs) {
- if (!doc.isDirectory() && !doc.isVirtualDocument()) {
- docsForSend.add(doc);
- }
- }
-
- if (docsForSend.size() == 1) {
- final DocumentInfo doc = docsForSend.get(0);
-
- intent = new Intent(Intent.ACTION_SEND);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- intent.addCategory(Intent.CATEGORY_DEFAULT);
- intent.setType(doc.mimeType);
- intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
-
- } else if (docsForSend.size() > 1) {
- intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- intent.addCategory(Intent.CATEGORY_DEFAULT);
-
- final ArrayList<String> mimeTypes = new ArrayList<>();
- final ArrayList<Uri> uris = new ArrayList<>();
- for (DocumentInfo doc : docsForSend) {
- mimeTypes.add(doc.mimeType);
- uris.add(doc.derivedUri);
- }
-
- intent.setType(findCommonMimeType(mimeTypes));
- intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
-
- } else {
- return;
- }
-
- intent = Intent.createChooser(intent, getActivity().getText(R.string.share_via));
- startActivity(intent);
- }
-
private void transferDocuments(final Selection selected, final @OpType int mode) {
if (mode == FileOperationService.OPERATION_COPY) {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_TO);
@@ -747,7 +679,7 @@
throw new RuntimeException("Failed to create uri supplier.", e);
}
- Uri srcParent = getDisplayState().stack.peek().derivedUri;
+ Uri srcParent = mState.stack.peek().derivedUri;
mLocalState.mPendingOperation = new FileOperation.Builder()
.withOpType(mode)
.withSrcParent(srcParent)
@@ -784,6 +716,17 @@
startActivityForResult(intent, REQUEST_COPY_DESTINATION);
}
+ @Override
+ public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case REQUEST_COPY_DESTINATION:
+ handleCopyResult(resultCode, data);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unknown request code: " + requestCode);
+ }
+ }
+
private static boolean hasDirectory(List<DocumentInfo> docs) {
for (DocumentInfo info : docs) {
if (Document.MIME_TYPE_DIR.equals(info.mimeType)) {
@@ -805,30 +748,8 @@
RenameDocumentFragment.show(getFragmentManager(), docs.get(0));
}
- @Override
- public void initDocumentHolder(DocumentHolder holder) {
- holder.addKeyEventListener(mInputHandler);
- holder.itemView.setOnFocusChangeListener(mFocusManager);
- }
-
- @Override
- public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
- setupDragAndDropOnDocumentView(holder.itemView, cursor);
- }
-
- @Override
- public State getDisplayState() {
- return getBaseActivity().getDisplayState();
- }
-
- @Override
- public Model getModel() {
- return mModel;
- }
-
- @Override
- public boolean isDocumentEnabled(String docMimeType, int docFlags) {
- return mActivityConfig.isDocumentEnabled(docMimeType, docFlags, getDisplayState());
+ private boolean isDocumentEnabled(String mimeType, int flags) {
+ return mActivityConfig.isDocumentEnabled(mimeType, flags, mState);
}
private void showEmptyDirectory() {
@@ -866,30 +787,6 @@
mRecView.requestFocus();
}
- private String findCommonMimeType(List<String> mimeTypes) {
- String[] commonType = mimeTypes.get(0).split("/");
- if (commonType.length != 2) {
- return "*/*";
- }
-
- for (int i = 1; i < mimeTypes.size(); i++) {
- String[] type = mimeTypes.get(i).split("/");
- if (type.length != 2) continue;
-
- if (!commonType[1].equals(type[1])) {
- commonType[1] = "*";
- }
-
- if (!commonType[0].equals(type[0])) {
- commonType[0] = "*";
- commonType[1] = "*";
- break;
- }
- }
-
- return commonType[0] + "/" + commonType[1];
- }
-
public void copySelectedToClipboard() {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD);
@@ -913,7 +810,7 @@
}
mSelectionMgr.clearSelection();
- mClipper.clipDocumentsForCut(mModel::getItemUri, selection, getDisplayState().stack.peek());
+ mClipper.clipDocumentsForCut(mModel::getItemUri, selection, mState.stack.peek());
Snackbars.showDocumentsClipped(getActivity(), selection.size());
}
@@ -924,7 +821,7 @@
BaseActivity activity = (BaseActivity) getActivity();
DocumentInfo destination = activity.getCurrentDirectory();
mClipper.copyFromClipboard(
- destination, activity.getDisplayState().stack, mDialogs::showFileOperationFailures);
+ destination, mState.stack, mDialogs::showFileOperationFailures);
getActivity().invalidateOptionsMenu();
}
@@ -937,10 +834,10 @@
Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + modelId);
return;
}
- BaseActivity activity = getBaseActivity();
+ BaseActivity activity = mActivity;
DocumentInfo destination = DocumentInfo.fromDirectoryCursor(dstCursor);
mClipper.copyFromClipboard(
- destination, activity.getDisplayState().stack, mDialogs::showFileOperationFailures);
+ destination, mState.stack, mDialogs::showFileOperationFailures);
getActivity().invalidateOptionsMenu();
}
@@ -950,7 +847,7 @@
// Exclude disabled files
List<String> enabled = new ArrayList<>();
for (String id : mAdapter.getModelIds()) {
- Cursor cursor = getModel().getItem(id);
+ Cursor cursor = mModel.getItem(id);
if (cursor == null) {
Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id);
continue;
@@ -1003,7 +900,7 @@
*/
@Override
public void onDragEntered(View view) {
- getBaseActivity().setRootsDrawerOpen(false);
+ mActivity.setRootsDrawerOpen(false);
}
/**
@@ -1013,7 +910,7 @@
*/
@Override
public void onViewHovered(View view) {
- BaseActivity activity = getBaseActivity();
+ BaseActivity activity = mActivity;
if (getModelId(view) != null) {
activity.springOpenDirectory(getDestination(view));
}
@@ -1044,7 +941,7 @@
DocumentInfo dst = getDestination(v);
mClipper.copyFromClipData(
- dst, getDisplayState().stack, clipData, mDialogs::showFileOperationFailures);
+ dst, mState.stack, clipData, mDialogs::showFileOperationFailures);
return true;
}
@@ -1077,7 +974,7 @@
}
if (v == mRecView || v == mEmptyView) {
- return getDisplayState().stack.peek();
+ return mState.stack.peek();
}
return null;
@@ -1112,48 +1009,6 @@
return null;
}
- @Override
- public boolean isSelected(String modelId) {
- return mSelectionMgr.getSelection().contains(modelId);
- }
-
- private final class ModelUpdateListener implements EventListener<Model.Update> {
-
- @Override
- public void accept(Model.Update update) {
- if (update.hasError()) {
- showQueryError();
- return;
- }
-
- if (DEBUG) Log.d(TAG, "Received model update. Loading=" + mModel.isLoading());
-
- if (mModel.info != null || mModel.error != null) {
- mMessageBar.setInfo(mModel.info);
- mMessageBar.setError(mModel.error);
- mMessageBar.show();
- }
-
- mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE);
-
- if (mModel.isEmpty()) {
- if (mLocalState.mSearchMode) {
- showNoResults(getDisplayState().stack.root);
- } else {
- showEmptyDirectory();
- }
- } else {
- showDirectory();
- mAdapter.notifyDataSetChanged();
- }
-
- if (!mModel.isLoading()) {
- getBaseActivity().notifyDirectoryLoaded(
- mModel.doc != null ? mModel.doc.derivedUri : null);
- }
- }
- }
-
// TODO: Move to activities when Model becomes activity level object.
private boolean canSelect(DocumentDetails doc) {
return canSetSelectionState(doc.getModelId(), true);
@@ -1171,7 +1026,7 @@
final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
final int docFlags = getCursorInt(cursor, Document.COLUMN_FLAGS);
- return mActivityConfig.canSelectType(docMimeType, docFlags, getDisplayState());
+ return mActivityConfig.canSelectType(docMimeType, docFlags, mState);
} else {
// Right now all selected items can be deselected.
return true;
@@ -1193,7 +1048,7 @@
DirectoryFragment df = get(fm);
df.mLocalState.update(root, doc, query);
- df.getLoaderManager().restartLoader(LOADER_ID, null, df);
+ df.getLoaderManager().restartLoader(LOADER_ID, null, df.mLoaderCallbacks);
}
public static void reload(FragmentManager fm, int type, RootInfo root, DocumentInfo doc,
@@ -1202,7 +1057,7 @@
DirectoryFragment df = get(fm);
df.mLocalState.update(type, root, doc, query);
- df.getLoaderManager().restartLoader(LOADER_ID, null, df);
+ df.getLoaderManager().restartLoader(LOADER_ID, null, df.mLoaderCallbacks);
}
public static void create(
@@ -1263,96 +1118,193 @@
}
// Trigger loading
- getLoaderManager().restartLoader(LOADER_ID, null, this);
+ getLoaderManager().restartLoader(LOADER_ID, null, mLoaderCallbacks);
}
- @Override
- public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
- Context context = getActivity();
- State state = getDisplayState();
+ private final class ModelUpdateListener implements EventListener<Model.Update> {
- Uri contentsUri;
- switch (mLocalState.mType) {
- case TYPE_NORMAL:
- contentsUri = mLocalState.mSearchMode ? DocumentsContract.buildSearchDocumentsUri(
- mLocalState.mRoot.authority, mLocalState.mRoot.rootId, mLocalState.mQuery)
- : DocumentsContract.buildChildDocumentsUri(
- mLocalState.mDocument.authority, mLocalState.mDocument.documentId);
- if (mActivityConfig.managedModeEnabled(state.stack)) {
- contentsUri = DocumentsContract.setManageMode(contentsUri);
+ @Override
+ public void accept(Model.Update update) {
+ if (update.hasError()) {
+ showQueryError();
+ return;
+ }
+
+ if (DEBUG) Log.d(TAG, "Received model update. Loading=" + mModel.isLoading());
+
+ if (mModel.info != null || mModel.error != null) {
+ mMessageBar.setInfo(mModel.info);
+ mMessageBar.setError(mModel.error);
+ mMessageBar.show();
+ }
+
+ mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE);
+
+ if (mModel.isEmpty()) {
+ if (mLocalState.mSearchMode) {
+ showNoResults(mState.stack.root);
+ } else {
+ showEmptyDirectory();
}
- if (DEBUG) Log.d(TAG, "Creating new directory loader for: "
- + DocumentInfo.debugString(mLocalState.mDocument));
- return new DirectoryLoader(
- context, mLocalState.mRoot, mLocalState.mDocument, contentsUri, state.sortModel,
- mLocalState.mSearchMode);
- case TYPE_RECENT_OPEN:
- if (DEBUG) Log.d(TAG, "Creating new loader recents.");
- final RootsAccess roots = DocumentsApplication.getRootsCache(context);
- return new RecentsLoader(context, roots, state);
+ } else {
+ showDirectory();
+ mAdapter.notifyDataSetChanged();
+ }
- default:
- throw new IllegalStateException("Unknown type " + mLocalState.mType);
+ if (!mModel.isLoading()) {
+ mActivity.notifyDirectoryLoaded(
+ mModel.doc != null ? mModel.doc.derivedUri : null);
+ }
}
}
- @Override
- public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
- if (DEBUG) Log.d(TAG, "Loader has finished for: "
- + DocumentInfo.debugString(mLocalState.mDocument));
- assert(result != null);
+ private final class AdapterEnvironment implements DocumentsAdapter.Environment {
- if (!isAdded()) return;
-
- if (mLocalState.mSearchMode) {
- Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SEARCH);
+ @Override
+ public Context getContext() {
+ return mActivity;
}
- State state = getDisplayState();
-
- mAdapter.notifyDataSetChanged();
- mModel.update(result);
-
- updateLayout(state.derivedMode);
-
- if (mRestoredSelection != null) {
- mSelectionMgr.restoreSelection(mRestoredSelection);
- // Note, we'll take care of cleaning up retained selection
- // in the selection handler where we already have some
- // specialized code to handle when selection was restored.
+ @Override
+ public State getDisplayState() {
+ return mState;
}
- // Restore any previous instance state
- final SparseArray<Parcelable> container = state.dirConfigs.remove(mLocalState.getConfigKey());
- final int curSortedDimensionId = state.sortModel.getSortedDimensionId();
-
- final SortDimension curSortedDimension =
- state.sortModel.getDimensionById(curSortedDimensionId);
- if (container != null && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
- getView().restoreHierarchyState(container);
- } else if (mLocalState.mLastSortDimensionId != curSortedDimension.getId()
- || mLocalState.mLastSortDimensionId == SortModel.SORT_DIMENSION_ID_UNKNOWN
- || mLocalState.mLastSortDirection != curSortedDimension.getSortDirection()) {
- // Scroll to the top if the sort order actually changed.
- mRecView.smoothScrollToPosition(0);
+ @Override
+ public Model getModel() {
+ return mModel;
}
- mLocalState.mLastSortDimensionId = curSortedDimension.getId();
- mLocalState.mLastSortDirection = curSortedDimension.getSortDirection();
+ @Override
+ public int getColumnCount() {
+ return mColumnCount;
+ }
- if (mRefreshLayout.isRefreshing()) {
- new Handler().postDelayed(
- () -> mRefreshLayout.setRefreshing(false),
- REFRESH_SPINNER_DISMISS_DELAY);
+ @Override
+ public boolean isSelected(String id) {
+ return mSelectionMgr.getSelection().contains(id);
+ }
+
+ @Override
+ public boolean isDocumentEnabled(String mimeType, int flags) {
+ return mActivityConfig.isDocumentEnabled(mimeType, flags, mState);
+ }
+
+ @Override
+ public void initDocumentHolder(DocumentHolder holder) {
+ holder.addKeyEventListener(mInputHandler);
+ holder.itemView.setOnFocusChangeListener(mFocusManager);
+ }
+
+ @Override
+ public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) {
+ setupDragAndDropOnDocumentView(holder.itemView, cursor);
}
}
- @Override
- public void onLoaderReset(Loader<DirectoryResult> loader) {
- if (DEBUG) Log.d(TAG, "Resetting loader for: "
- + DocumentInfo.debugString(mLocalState.mDocument));
- mModel.onLoaderReset();
+ private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> {
- mRefreshLayout.setRefreshing(false);
+ @Override
+ public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
+ Context context = getActivity();
+
+ Uri contentsUri;
+ switch (mLocalState.mType) {
+ case TYPE_NORMAL:
+ contentsUri = mLocalState.mSearchMode
+ ? DocumentsContract.buildSearchDocumentsUri(
+ mLocalState.mRoot.authority,
+ mLocalState.mRoot.rootId,
+ mLocalState.mQuery)
+ : DocumentsContract.buildChildDocumentsUri(
+ mLocalState.mDocument.authority,
+ mLocalState.mDocument.documentId);
+
+ if (mActivityConfig.managedModeEnabled(mState.stack)) {
+ contentsUri = DocumentsContract.setManageMode(contentsUri);
+ }
+
+ if (DEBUG) Log.d(TAG,
+ "Creating new directory loader for: "
+ + DocumentInfo.debugString(mLocalState.mDocument));
+
+ return new DirectoryLoader(
+ context,
+ mLocalState.mRoot,
+ mLocalState.mDocument,
+ contentsUri,
+ mState.sortModel,
+ mReloadLock,
+ mLocalState.mSearchMode);
+
+ case TYPE_RECENT_OPEN:
+ if (DEBUG) Log.d(TAG, "Creating new loader recents.");
+ final RootsAccess roots = DocumentsApplication.getRootsCache(context);
+ return new RecentsLoader(context, roots, mState);
+
+ default:
+ throw new IllegalStateException("Unknown type " + mLocalState.mType);
+ }
+ }
+
+ @Override
+ public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
+ if (DEBUG) Log.d(TAG, "Loader has finished for: "
+ + DocumentInfo.debugString(mLocalState.mDocument));
+ assert(result != null);
+
+ if (!isAdded()) return;
+
+ if (mLocalState.mSearchMode) {
+ Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SEARCH);
+ }
+
+ mAdapter.notifyDataSetChanged();
+ mModel.update(result);
+
+ updateLayout(mState.derivedMode);
+
+ if (mRestoredSelection != null) {
+ mSelectionMgr.restoreSelection(mRestoredSelection);
+ // Note, we'll take care of cleaning up retained selection
+ // in the selection handler where we already have some
+ // specialized code to handle when selection was restored.
+ }
+
+ // Restore any previous instance state
+ final SparseArray<Parcelable> container =
+ mState.dirConfigs.remove(mLocalState.getConfigKey());
+ final int curSortedDimensionId = mState.sortModel.getSortedDimensionId();
+
+ final SortDimension curSortedDimension =
+ mState.sortModel.getDimensionById(curSortedDimensionId);
+ if (container != null
+ && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, false)) {
+ getView().restoreHierarchyState(container);
+ } else if (mLocalState.mLastSortDimensionId != curSortedDimension.getId()
+ || mLocalState.mLastSortDimensionId == SortModel.SORT_DIMENSION_ID_UNKNOWN
+ || mLocalState.mLastSortDirection != curSortedDimension.getSortDirection()) {
+ // Scroll to the top if the sort order actually changed.
+ mRecView.smoothScrollToPosition(0);
+ }
+
+ mLocalState.mLastSortDimensionId = curSortedDimension.getId();
+ mLocalState.mLastSortDirection = curSortedDimension.getSortDirection();
+
+ if (mRefreshLayout.isRefreshing()) {
+ new Handler().postDelayed(
+ () -> mRefreshLayout.setRefreshing(false),
+ REFRESH_SPINNER_DISMISS_DELAY);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<DirectoryResult> loader) {
+ if (DEBUG) Log.d(TAG, "Resetting loader for: "
+ + DocumentInfo.debugString(mLocalState.mDocument));
+ mModel.onLoaderReset();
+
+ mRefreshLayout.setRefreshing(false);
+ }
}
}
diff --git a/src/com/android/documentsui/dirlist/IconHelper.java b/src/com/android/documentsui/dirlist/IconHelper.java
index 14c569d..6f6327b 100644
--- a/src/com/android/documentsui/dirlist/IconHelper.java
+++ b/src/com/android/documentsui/dirlist/IconHelper.java
@@ -45,7 +45,7 @@
import com.android.documentsui.ThumbnailCache;
import com.android.documentsui.ThumbnailCache.Result;
import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.MimeTypes;
import com.android.documentsui.base.State;
import com.android.documentsui.base.State.ViewMode;
@@ -233,7 +233,7 @@
final boolean supportsThumbnail = (docFlags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
final boolean allowThumbnail = (mMode == MODE_GRID)
- || MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mimeType);
+ || MimeTypes.mimeMatches(MimeTypes.VISUAL_MIMES, mimeType);
final boolean showThumbnail = supportsThumbnail && allowThumbnail && mThumbnailsEnabled;
if (showThumbnail) {
loadedThumbnail =
diff --git a/src/com/android/documentsui/dirlist/Model.java b/src/com/android/documentsui/dirlist/Model.java
index 016346a..8bdf079 100644
--- a/src/com/android/documentsui/dirlist/Model.java
+++ b/src/com/android/documentsui/dirlist/Model.java
@@ -42,12 +42,25 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Predicate;
/**
* The data model for the current loaded directory.
*/
@VisibleForTesting
public class Model {
+
+ /**
+ * Filter that passes (returns true) all non-virtual, non-partial files.
+ */
+ public static final Predicate<Cursor> CONCRETE_FILE_FILTER = (Cursor c) -> {
+ int flags = DocumentInfo.getCursorInt(c, Document.COLUMN_FLAGS);
+ return (flags & Document.FLAG_VIRTUAL_DOCUMENT) == 0
+ && (flags & Document.FLAG_PARTIAL) == 0;
+ };
+
+ private static final Predicate<Cursor> ANY_FILE_FILTER = (Cursor c) -> true;
+
private static final String TAG = "Model";
private boolean mIsLoading;
@@ -193,19 +206,7 @@
}
public List<DocumentInfo> getDocuments(Selection selection) {
- final int size = (selection != null) ? selection.size() : 0;
-
- final List<DocumentInfo> docs = new ArrayList<>(size);
- // NOTE: That as this now iterates over only final (non-provisional) selection.
- for (String modelId: selection) {
- DocumentInfo doc = getDocument(modelId);
- if (doc == null) {
- Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
- continue;
- }
- docs.add(doc);
- }
- return docs;
+ return loadDocuments(selection, ANY_FILE_FILTER);
}
public @Nullable DocumentInfo getDocument(String modelId) {
@@ -215,6 +216,35 @@
: DocumentInfo.fromDirectoryCursor(cursor);
}
+ public List<DocumentInfo> loadDocuments(Selection selection, Predicate<Cursor> filter) {
+ final int size = (selection != null) ? selection.size() : 0;
+
+ final List<DocumentInfo> docs = new ArrayList<>(size);
+ for (String modelId: selection) {
+ loadDocument(docs, modelId, filter);
+ }
+ return docs;
+ }
+
+ /**
+ * @param docs
+ * @return DocumentInfo, or null. If filter returns false, null will be returned.
+ */
+ private void loadDocument(
+ List<DocumentInfo> docs, String modelId, Predicate<Cursor> filter) {
+ final Cursor cursor = getItem(modelId);
+
+ if (cursor == null) {
+ Log.w(TAG, "Unable to obtain document for modelId: " + modelId);
+ }
+
+ if (filter.test(cursor)) {
+ docs.add(DocumentInfo.fromDirectoryCursor(cursor));
+ } else {
+ if (DEBUG) Log.v(TAG, "Filtered document from results: " + modelId);
+ }
+ }
+
public Uri getItemUri(String modelId) {
final Cursor cursor = getItem(modelId);
return DocumentInfo.getUri(cursor);
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 993af36..d59e2a7 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -32,12 +32,15 @@
import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.Metrics;
+import com.android.documentsui.R;
+import com.android.documentsui.SearchViewManager;
import com.android.documentsui.base.ConfirmationCallback;
import com.android.documentsui.base.ConfirmationCallback.Result;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.EventListener;
import com.android.documentsui.base.Lookup;
+import com.android.documentsui.base.MimeTypes;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.clipping.ClipStore;
@@ -58,6 +61,7 @@
import com.android.documentsui.ui.DialogController;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
@@ -83,6 +87,7 @@
RootsAccess roots,
DocumentsAccess docs,
SelectionManager selectionMgr,
+ SearchViewManager searchMgr,
Lookup<String, Executor> executors,
ActionModeAddons actionModeAddons,
DialogController dialogs,
@@ -90,7 +95,7 @@
DocumentClipper clipper,
ClipStore clipStore) {
- super(activity, state, roots, docs, selectionMgr, executors);
+ super(activity, state, roots, docs, selectionMgr, searchMgr, executors);
mActionModeAddons = actionModeAddons;
mDialogs = dialogs;
@@ -231,6 +236,54 @@
}
@Override
+ public void shareSelectedDocuments() {
+ Metrics.logUserAction(mActivity, Metrics.USER_ACTION_SHARE);
+
+ Selection selection = getStableSelection();
+
+ assert(!selection.isEmpty());
+
+ // Model must be accessed in UI thread, since underlying cursor is not threadsafe.
+ List<DocumentInfo> docs =
+ mScope.model.loadDocuments(selection, Model.CONCRETE_FILE_FILTER);
+
+ Intent intent;
+
+ if (docs.size() == 1) {
+ final DocumentInfo doc = docs.get(0);
+
+ intent = new Intent(Intent.ACTION_SEND);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.setType(doc.mimeType);
+ intent.putExtra(Intent.EXTRA_STREAM, doc.derivedUri);
+
+ } else if (docs.size() > 1) {
+ intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+
+ final ArrayList<String> mimeTypes = new ArrayList<>();
+ final ArrayList<Uri> uris = new ArrayList<>();
+ for (DocumentInfo doc : docs) {
+ mimeTypes.add(doc.mimeType);
+ uris.add(doc.derivedUri);
+ }
+
+ intent.setType(MimeTypes.findCommonMimeType(mimeTypes));
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
+
+ } else {
+ return;
+ }
+
+ Intent chooserIntent = Intent.createChooser(
+ intent, mActivity.getResources().getText(R.string.share_via));
+
+ mActivity.startActivity(chooserIntent);
+ }
+
+ @Override
public void initLocation(Intent intent) {
assert(intent != null);
@@ -329,7 +382,7 @@
public void onDocumentPicked(DocumentInfo doc) {
if (doc.isContainer()) {
- mActivity.openContainerDocument(doc);
+ openContainerDocument(doc);
return;
}
@@ -351,7 +404,7 @@
}
if (doc.isContainer()) {
- mActivity.openContainerDocument(doc);
+ openContainerDocument(doc);
return true;
}
@@ -440,10 +493,10 @@
return false;
}
- // managament is only supported in downloads.
+ // management 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)
+ return MimeTypes.isApkType(doc.mimeType)
|| doc.isPartial();
}
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 500bcf7..c02fced 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -35,7 +35,6 @@
import com.android.documentsui.ActionModeController;
import com.android.documentsui.ActivityConfig;
import com.android.documentsui.BaseActivity;
-import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.MenuManager.DirectoryDetails;
import com.android.documentsui.MenuManager.SelectionDetails;
@@ -67,17 +66,17 @@
/**
* Standalone file management activity.
*/
-public class FilesActivity extends BaseActivity implements ActionHandler.Addons {
+public class FilesActivity
+ extends BaseActivity<ActionHandler<FilesActivity>> implements ActionHandler.Addons {
public static final String TAG = "FilesActivity";
private final Config mConfig = new Config();
private SelectionManager mSelectionMgr;
private MenuManager mMenuManager;
- private ActionHandler<FilesActivity> mActions;
private DialogController mDialogs;
private DocumentClipper mClipper;
- protected ActionModeController mActionModeController;
+ private ActionModeController mActionModeController;
public FilesActivity() {
super(R.layout.files_activity, TAG);
@@ -111,8 +110,9 @@
this,
mState,
mRoots,
- DocumentsAccess.create(this),
+ mDocs,
mSelectionMgr,
+ mSearchManager,
ProviderExecutor::forAuthority,
mActionModeController,
mDialogs,
@@ -281,16 +281,7 @@
public void springOpenDirectory(DocumentInfo doc) {
assert(doc.isContainer());
assert(!doc.isArchive());
- openContainerDocument(doc);
- }
-
- /**
- * @deprecated use {@link ActionHandler#showChooserForDoc(DocumentInfo)}
- * @param doc
- */
- @Deprecated
- public void showChooserForDoc(DocumentInfo doc) {
- mActions.showChooserForDoc(doc);
+ mActions.openContainerDocument(doc);
}
@Override
@@ -357,6 +348,7 @@
return mConfig;
}
+ @Override
public SelectionManager getSelectionManager(
DocumentsAdapter adapter, SelectionPredicate canSetState) {
return mSelectionMgr.reset(adapter, canSetState);
diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java
index 50f45d1..03c8a90 100644
--- a/src/com/android/documentsui/picker/ActionHandler.java
+++ b/src/com/android/documentsui/picker/ActionHandler.java
@@ -31,11 +31,12 @@
import com.android.documentsui.ActivityConfig;
import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.Metrics;
+import com.android.documentsui.SearchViewManager;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.EventListener;
import com.android.documentsui.base.Lookup;
-import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.MimeTypes;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
@@ -66,10 +67,11 @@
RootsAccess roots,
DocumentsAccess docs,
SelectionManager selectionMgr,
+ SearchViewManager searchMgr,
Lookup<String, Executor> executors,
ActivityConfig activityConfig) {
- super(activity, state, roots, docs, selectionMgr, executors);
+ super(activity, state, roots, docs, selectionMgr, searchMgr, executors);
mConfig = activityConfig;
mScope = new ContentScope(this::onModelLoaded);
@@ -152,7 +154,7 @@
private void onModelLoaded(Model.Update update) {
boolean showDrawer = false;
- if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) {
+ if (MimeTypes.mimeMatches(MimeTypes.VISUAL_MIMES, mState.acceptMimes)) {
showDrawer = false;
}
if (mState.external && mState.action == ACTION_GET_CONTENT) {
diff --git a/src/com/android/documentsui/picker/Config.java b/src/com/android/documentsui/picker/Config.java
index 2d83ba1..959da9f 100644
--- a/src/com/android/documentsui/picker/Config.java
+++ b/src/com/android/documentsui/picker/Config.java
@@ -24,9 +24,9 @@
import android.provider.DocumentsContract.Document;
-import com.android.documentsui.base.MimePredicate;
-import com.android.documentsui.base.State;
import com.android.documentsui.ActivityConfig;
+import com.android.documentsui.base.MimeTypes;
+import com.android.documentsui.base.State;
/**
* Provides support for Platform specific specializations of DirectoryFragment.
@@ -39,7 +39,7 @@
return false;
}
- if (MimePredicate.isDirectoryType(docMimeType)) {
+ if (MimeTypes.isDirectoryType(docMimeType)) {
return false;
}
@@ -56,7 +56,7 @@
@Override
public boolean isDocumentEnabled(String mimeType, int docFlags, State state) {
// Directories are always enabled.
- if (MimePredicate.isDirectoryType(mimeType)) {
+ if (MimeTypes.isDirectoryType(mimeType)) {
return true;
}
@@ -74,6 +74,6 @@
}
}
- return MimePredicate.mimeMatches(state.acceptMimes, mimeType);
+ return MimeTypes.mimeMatches(state.acceptMimes, mimeType);
}
}
diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java
index 42d32ae..17e6bb1 100644
--- a/src/com/android/documentsui/picker/PickActivity.java
+++ b/src/com/android/documentsui/picker/PickActivity.java
@@ -46,7 +46,6 @@
import com.android.documentsui.ActionModeController;
import com.android.documentsui.ActivityConfig;
import com.android.documentsui.BaseActivity;
-import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.MenuManager.DirectoryDetails;
import com.android.documentsui.MenuManager.SelectionDetails;
@@ -54,7 +53,7 @@
import com.android.documentsui.R;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.EventHandler;
-import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.MimeTypes;
import com.android.documentsui.base.PairedTask;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
@@ -73,7 +72,8 @@
import java.util.Arrays;
import java.util.List;
-public class PickActivity extends BaseActivity implements ActionHandler.Addons {
+public class PickActivity
+ extends BaseActivity<ActionHandler<PickActivity>> implements ActionHandler.Addons {
private static final int CODE_FORWARD = 42;
private static final String TAG = "PickActivity";
@@ -81,7 +81,6 @@
private SelectionManager mSelectionMgr;
private MenuManager mMenuManager;
- private ActionHandler<PickActivity> mActionHandler;
private ActionModeController mActionModeController;
public PickActivity() {
@@ -97,12 +96,13 @@
? SelectionManager.MODE_MULTIPLE
: SelectionManager.MODE_SINGLE);
mMenuManager = new MenuManager(mSearchManager, mState, new DirectoryDetails(this));
- mActionHandler = new ActionHandler<>(
+ mActions = new ActionHandler<>(
this,
mState,
- DocumentsApplication.getRootsCache(this),
- DocumentsAccess.create(this),
+ mRoots,
+ mDocs,
mSelectionMgr,
+ mSearchManager,
ProviderExecutor::forAuthority,
mConfig);
@@ -115,7 +115,7 @@
Intent intent = getIntent();
setupLayout(intent);
- mActionHandler.initLocation(intent);
+ mActions.initLocation(intent);
}
private void setupLayout(Intent intent) {
@@ -262,7 +262,7 @@
// No directory means recents
if (mState.action == ACTION_CREATE ||
mState.action == ACTION_PICK_COPY_DESTINATION) {
- mActionHandler.loadRoot(Shared.getDefaultRootUri(this));
+ mActions.loadRoot(Shared.getDefaultRootUri(this));
} else {
DirectoryFragment.showRecentsOpen(fm, anim);
@@ -270,8 +270,8 @@
// picking GRID for visual types. We intentionally don't
// consult a user's saved preferences here since they are
// set per root (not per root and per mimetype).
- boolean visualMimes = MimePredicate.mimeMatches(
- MimePredicate.VISUAL_MIMES, mState.acceptMimes);
+ boolean visualMimes = MimeTypes.mimeMatches(
+ MimeTypes.VISUAL_MIMES, mState.acceptMimes);
mState.derivedMode = visualMimes ? State.MODE_GRID : State.MODE_LIST;
}
} else {
@@ -312,7 +312,7 @@
@Override
protected void onDirectoryCreated(DocumentInfo doc) {
assert(doc.isDirectory());
- openContainerDocument(doc);
+ mActions.openContainerDocument(doc);
}
void onSaveRequested(String mimeType, String displayName) {
@@ -324,7 +324,7 @@
public void onDocumentPicked(DocumentInfo doc) {
final FragmentManager fm = getFragmentManager();
if (doc.isContainer()) {
- openContainerDocument(doc);
+ mActions.openContainerDocument(doc);
} else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) {
// Explicit file picked, return
new ExistingFinishTask(this, doc.derivedUri)
@@ -436,10 +436,10 @@
// provide our friend, RootsFragment, early access to this special feature!
if (model == null) {
- return mActionHandler;
+ return mActions;
}
- return mActionHandler.reset(model, searchMode);
+ return mActions.reset(model, searchMode);
}
@Override
diff --git a/src/com/android/documentsui/roots/RootsAccess.java b/src/com/android/documentsui/roots/RootsAccess.java
index 6323840..0d0a975 100644
--- a/src/com/android/documentsui/roots/RootsAccess.java
+++ b/src/com/android/documentsui/roots/RootsAccess.java
@@ -18,9 +18,10 @@
import static com.android.documentsui.base.Shared.DEBUG;
+import android.net.Uri;
import android.util.Log;
-import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.MimeTypes;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
@@ -33,6 +34,8 @@
*/
public interface RootsAccess {
+ Uri NOTIFICATION_URI = Uri.parse("content://com.android.documentsui.roots/");
+
/**
* Return the requested {@link RootInfo}, but only loading the roots for the
* requested authority. This is useful when we want to load fast without
@@ -104,8 +107,8 @@
}
final boolean overlap =
- MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
- MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
+ MimeTypes.mimeMatches(root.derivedMimeTypes, state.acceptMimes) ||
+ MimeTypes.mimeMatches(state.acceptMimes, root.derivedMimeTypes);
if (!overlap) {
if (DEBUG) Log.v(
tag, "Excluding root because: unsupported content types > "
diff --git a/src/com/android/documentsui/roots/RootsCache.java b/src/com/android/documentsui/roots/RootsCache.java
index ea08b40..4111a95 100644
--- a/src/com/android/documentsui/roots/RootsCache.java
+++ b/src/com/android/documentsui/roots/RootsCache.java
@@ -44,11 +44,11 @@
import com.android.documentsui.base.State;
import com.android.internal.annotations.GuardedBy;
+import libcore.io.IoUtils;
+
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
-import libcore.io.IoUtils;
-
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -62,9 +62,6 @@
* Cache of known storage backends and their roots.
*/
public class RootsCache implements RootsAccess {
- public static final Uri sNotificationUri = Uri.parse(
- "content://com.android.documentsui.roots/");
-
private static final String TAG = "RootsCache";
private final Context mContext;
@@ -393,7 +390,7 @@
mStoppedAuthorities = mTaskStoppedAuthorities;
}
mFirstLoad.countDown();
- resolver.notifyChange(sNotificationUri, null, false);
+ resolver.notifyChange(NOTIFICATION_URI, null, false);
return null;
}
diff --git a/src/com/android/documentsui/roots/RootsLoader.java b/src/com/android/documentsui/roots/RootsLoader.java
index 7e55be4..cae2639 100644
--- a/src/com/android/documentsui/roots/RootsLoader.java
+++ b/src/com/android/documentsui/roots/RootsLoader.java
@@ -38,7 +38,7 @@
mState = state;
context.getContentResolver().registerContentObserver(
- RootsCache.sNotificationUri, false, mObserver);
+ RootsAccess.NOTIFICATION_URI, false, mObserver);
}
@Override
diff --git a/src/com/android/documentsui/selection/BandController.java b/src/com/android/documentsui/selection/BandController.java
index a255e52..b6bca11 100644
--- a/src/com/android/documentsui/selection/BandController.java
+++ b/src/com/android/documentsui/selection/BandController.java
@@ -33,6 +33,7 @@
import android.util.SparseIntArray;
import android.view.View;
+import com.android.documentsui.DirectoryReloadLock;
import com.android.documentsui.R;
import com.android.documentsui.base.Events.InputEvent;
import com.android.documentsui.dirlist.DocumentsAdapter;
@@ -61,6 +62,7 @@
private final SelectionEnvironment mEnvironment;
private final DocumentsAdapter mAdapter;
private final SelectionManager mSelectionManager;
+ private final DirectoryReloadLock mLock;
private final Runnable mViewScroller;
private final GridModel.OnSelectionChangedListener mGridListener;
@@ -75,16 +77,19 @@
final RecyclerView view,
DocumentsAdapter adapter,
SelectionManager selectionManager,
+ DirectoryReloadLock lock,
IntPredicate gridItemTester) {
- this(new RuntimeSelectionEnvironment(view), adapter, selectionManager, gridItemTester);
+ this(new RuntimeSelectionEnvironment(view), adapter, selectionManager, lock, gridItemTester);
}
private BandController(
SelectionEnvironment env,
DocumentsAdapter adapter,
SelectionManager selectionManager,
+ DirectoryReloadLock lock,
IntPredicate gridItemTester) {
+ mLock = lock;
selectionManager.bindContoller(this);
mEnvironment = env;
@@ -265,6 +270,7 @@
private void startBandSelect(Point origin) {
if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
+ mLock.block();
mOrigin = origin;
mModelBuilder.run(); // Creates a new selection model.
mModel.startSelection(mOrigin);
@@ -314,6 +320,7 @@
mModel = null;
mOrigin = null;
+ mLock.unblock();
}
private void onSelectionChanged(Set<String> updatedSelection) {
diff --git a/src/com/android/documentsui/selection/GestureSelector.java b/src/com/android/documentsui/selection/GestureSelector.java
index 08afac5..959080c 100644
--- a/src/com/android/documentsui/selection/GestureSelector.java
+++ b/src/com/android/documentsui/selection/GestureSelector.java
@@ -21,6 +21,7 @@
import android.support.v7.widget.RecyclerView;
import android.view.View;
+import com.android.documentsui.DirectoryReloadLock;
import com.android.documentsui.base.Events.InputEvent;
import com.android.documentsui.ui.ViewAutoScroller;
import com.android.documentsui.ui.ViewAutoScroller.ScrollActionDelegate;
@@ -40,6 +41,7 @@
private final Runnable mDragScroller;
private final IntSupplier mHeight;
private final ViewFinder mViewFinder;
+ private final DirectoryReloadLock mLock;
private int mLastStartedItemPos = -1;
private boolean mStarted = false;
private Point mLastInterceptedPoint;
@@ -48,10 +50,12 @@
SelectionManager selectionMgr,
IntSupplier heightSupplier,
ViewFinder viewFinder,
- ScrollActionDelegate actionDelegate) {
+ ScrollActionDelegate actionDelegate,
+ DirectoryReloadLock lock) {
mSelectionMgr = selectionMgr;
mHeight = heightSupplier;
mViewFinder = viewFinder;
+ mLock = lock;
ScrollDistanceDelegate distanceDelegate = new ScrollDistanceDelegate() {
@Override
@@ -75,7 +79,8 @@
public static GestureSelector create(
SelectionManager selectionMgr,
- RecyclerView scrollView) {
+ RecyclerView scrollView,
+ DirectoryReloadLock lock) {
ScrollActionDelegate actionDelegate = new ScrollActionDelegate() {
@Override
public void scrollBy(int dy) {
@@ -97,7 +102,8 @@
selectionMgr,
scrollView::getHeight,
scrollView::findChildViewUnder,
- actionDelegate);
+ actionDelegate,
+ lock);
return helper;
}
@@ -162,6 +168,8 @@
mLastInterceptedPoint = e.getOrigin();
if (mStarted) {
mSelectionMgr.startRangeSelection(mLastStartedItemPos);
+ // Gesture Selection about to start
+ mLock.block();
return true;
}
return false;
@@ -173,6 +181,7 @@
mLastStartedItemPos = -1;
mStarted = false;
mSelectionMgr.getSelection().applyProvisionalSelection();
+ mLock.unblock();
return false;
}
diff --git a/src/com/android/documentsui/selection/SelectionMetadata.java b/src/com/android/documentsui/selection/SelectionMetadata.java
index e5f64d9..1da5748 100644
--- a/src/com/android/documentsui/selection/SelectionMetadata.java
+++ b/src/com/android/documentsui/selection/SelectionMetadata.java
@@ -24,7 +24,7 @@
import android.util.Log;
import com.android.documentsui.MenuManager;
-import com.android.documentsui.base.MimePredicate;
+import com.android.documentsui.base.MimeTypes;
import java.util.function.Function;
@@ -63,7 +63,7 @@
final int delta = selected ? 1 : -1;
final String mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
- if (MimePredicate.isDirectoryType(mimeType)) {
+ if (MimeTypes.isDirectoryType(mimeType)) {
mDirectoryCount += delta;
} else {
mFileCount += delta;
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index f6a4db3..ecdc053 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -361,8 +361,8 @@
mList.requestFocus();
}
- private BaseActivity getBaseActivity() {
- return (BaseActivity) getActivity();
+ private BaseActivity<?> getBaseActivity() {
+ return (BaseActivity<?>) getActivity();
}
@Override
@@ -409,7 +409,7 @@
AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo;
final Item item = mAdapter.getItem(adapterMenuInfo.position);
- BaseActivity activity = getBaseActivity();
+ BaseActivity<?> activity = getBaseActivity();
item.createContextMenu(menu, activity.getMenuInflater(), activity.getMenuManager());
}
diff --git a/tests/common/com/android/documentsui/TestActionModeAddons.java b/tests/common/com/android/documentsui/TestActionModeAddons.java
index 5e7b7d7..02984fa 100644
--- a/tests/common/com/android/documentsui/TestActionModeAddons.java
+++ b/tests/common/com/android/documentsui/TestActionModeAddons.java
@@ -17,12 +17,9 @@
import com.android.documentsui.testing.TestConfirmationCallback;
-/**
- *
- */
public class TestActionModeAddons implements ActionModeAddons {
- public final TestConfirmationCallback finishOnConfimed = new TestConfirmationCallback();
+ public final TestConfirmationCallback finishOnConfirmed = new TestConfirmationCallback();
@Override
public void finishActionMode() {
@@ -31,6 +28,6 @@
@Override
public void finishOnConfirmed(int code) {
- finishOnConfimed.accept(code);
+ finishOnConfirmed.accept(code);
}
}
diff --git a/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java
index 9fc4fca..37bd60b 100644
--- a/tests/common/com/android/documentsui/TestActivity.java
+++ b/tests/common/com/android/documentsui/TestActivity.java
@@ -48,9 +48,9 @@
public TestEventListener<Intent> startActivity;
public TestEventListener<Intent> startService;
public TestEventListener<RootInfo> rootPicked;
- public TestEventListener<DocumentInfo> openContainer;
public TestEventListener<Integer> refreshCurrentRootAndDirectory;
public TestEventListener<Boolean> setRootsDrawerOpen;
+ public TestEventListener<Uri> notifyDirectoryNavigated;
public static TestActivity create() {
TestActivity activity = Mockito.mock(TestActivity.class, Mockito.CALLS_REAL_METHODS);
@@ -66,9 +66,9 @@
startActivity = new TestEventListener<>();
startService = new TestEventListener<>();
rootPicked = new TestEventListener<>();
- openContainer = new TestEventListener<>();
refreshCurrentRootAndDirectory = new TestEventListener<>();
setRootsDrawerOpen = new TestEventListener<>();
+ notifyDirectoryNavigated = new TestEventListener<>();
}
@Override
@@ -121,8 +121,8 @@
}
@Override
- public final void openContainerDocument(DocumentInfo doc) {
- openContainer.accept(doc);
+ public final void notifyDirectoryNavigated(Uri uri) {
+ notifyDirectoryNavigated.accept(uri);
}
@Override
diff --git a/tests/common/com/android/documentsui/dirlist/TestModel.java b/tests/common/com/android/documentsui/dirlist/TestModel.java
index 82849a3..abe4d5f 100644
--- a/tests/common/com/android/documentsui/dirlist/TestModel.java
+++ b/tests/common/com/android/documentsui/dirlist/TestModel.java
@@ -22,13 +22,10 @@
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.EventListener;
import com.android.documentsui.roots.RootCursorWrapper;
import libcore.net.MimeUtils;
-import java.util.ArrayList;
-import java.util.List;
import java.util.Random;
public class TestModel extends Model {
@@ -46,7 +43,6 @@
private int mLastId = 0;
private Random mRand = new Random();
private MatrixCursor mCursor;
- private List<EventListener<Update>> mUpdateListeners = new ArrayList<>();
public TestModel(String authority) {
super();
diff --git a/tests/common/com/android/documentsui/testing/IntentAsserts.java b/tests/common/com/android/documentsui/testing/IntentAsserts.java
new file mode 100644
index 0000000..6fc1ae0
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/IntentAsserts.java
@@ -0,0 +1,67 @@
+/*
+ * 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 static android.content.Intent.EXTRA_INTENT;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Parcelable;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Handy-dandy Junit asserts covering Intents.
+ */
+public final class IntentAsserts {
+
+ private IntentAsserts() {}
+
+ public static void assertHasAction(Intent intent, String expected) {
+ assertEquals(expected, intent.getAction());
+ }
+
+ public static Intent assertHasExtraIntent(Intent intent) {
+ Intent extra = (Intent) intent.getExtra(EXTRA_INTENT);
+ assertNotNull(extra);
+ return extra;
+ }
+
+ public static Uri assertHasExtraUri(Intent intent, String key) {
+ Object value = intent.getExtra(key);
+ assertNotNull(value);
+ assertTrue(value instanceof Uri);
+ return (Uri) value;
+ }
+
+ public static List<Parcelable> assertHasExtraList(Intent intent, String key) {
+ ArrayList<Parcelable> list = intent.getParcelableArrayListExtra(key);
+ assertNotNull(list);
+ return list;
+ }
+
+ public static List<Parcelable> assertHasExtraList(Intent intent, String key, int size) {
+ List<Parcelable> list = assertHasExtraList(intent, key);
+ Assert.assertEquals(size, list.size());
+ return list;
+ }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index 381b738..fd199ce 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -40,6 +40,7 @@
env.roots,
env.docs,
env.selectionMgr,
+ env.searchViewManager,
(String authority) -> null);
}
diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java
index 2f8587b..566f3eb 100644
--- a/tests/common/com/android/documentsui/testing/TestEnv.java
+++ b/tests/common/com/android/documentsui/testing/TestEnv.java
@@ -19,6 +19,7 @@
import android.os.Looper;
import android.provider.DocumentsContract.Document;
+import com.android.documentsui.SearchViewManager;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.State;
import com.android.documentsui.dirlist.TestModel;
@@ -26,6 +27,8 @@
import junit.framework.Assert;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
@@ -50,11 +53,13 @@
public final TestDocumentsAccess docs = new TestDocumentsAccess();
public final TestModel model;
public final SelectionManager selectionMgr;
+ public final SearchViewManager searchViewManager;
private TestEnv(String authority) {
mExecutor = new TestScheduledExecutorService();
model = new TestModel(authority);
selectionMgr = SelectionManagers.createTestInstance();
+ searchViewManager = new TestSearchViewManager();
}
public static TestEnv create() {
@@ -127,4 +132,10 @@
public Executor lookupExecutor(String authority) {
return mExecutor;
}
+
+ public void selectDocument(DocumentInfo info) {
+ List<String> ids = new ArrayList<>(1);
+ ids.add(info.documentId);
+ selectionMgr.setItemsSelected(ids, true);
+ }
}
diff --git a/tests/common/com/android/documentsui/testing/TestResources.java b/tests/common/com/android/documentsui/testing/TestResources.java
index 206d46b..a014af3 100644
--- a/tests/common/com/android/documentsui/testing/TestResources.java
+++ b/tests/common/com/android/documentsui/testing/TestResources.java
@@ -82,4 +82,8 @@
@StringRes int id, Object... formatArgs) throws NotFoundException {
return getString(id);
}
+
+ public final CharSequence getText(@StringRes int resId) {
+ return getString(resId);
+ }
}
diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
index 254fdbf..1577ae2 100644
--- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java
@@ -56,6 +56,7 @@
mEnv.roots,
mEnv.docs,
mEnv.selectionMgr,
+ mEnv.searchViewManager,
mEnv::lookupExecutor) {
@Override
diff --git a/tests/unit/com/android/documentsui/DirectoryReloadLockTest.java b/tests/unit/com/android/documentsui/DirectoryReloadLockTest.java
new file mode 100644
index 0000000..dbbc9c6
--- /dev/null
+++ b/tests/unit/com/android/documentsui/DirectoryReloadLockTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DirectoryReloadLockTest {
+
+ private DirectoryReloadLock lock = new DirectoryReloadLock();
+ private boolean called;
+ private Runnable callback = () -> {
+ called = true;
+ };
+
+ @Before
+ public void setUp() {
+ called = false;
+ }
+
+ @Test
+ public void testNotBlocking_callbackNotBlocked() {
+ lock.tryUpdate(callback);
+ assertTrue(called);
+ }
+
+ @Test
+ public void testToggleBlocking_callbackNotBlocked() {
+ lock.block();
+ lock.unblock();
+ lock.tryUpdate(callback);
+ assertTrue(called);
+ }
+
+ @Test
+ public void testBlocking_callbackBlocked() {
+ lock.block();
+ lock.tryUpdate(callback);
+ assertFalse(called);
+ }
+
+ @Test
+ public void testBlockthenUnblock_callbackNotBlocked() {
+ lock.block();
+ lock.tryUpdate(callback);
+ lock.unblock();
+ assertTrue(called);
+ }
+}
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index 2620f14..7de09b1 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -16,6 +16,10 @@
package com.android.documentsui.files;
+import static com.android.documentsui.testing.IntentAsserts.assertHasAction;
+import static com.android.documentsui.testing.IntentAsserts.assertHasExtraIntent;
+import static com.android.documentsui.testing.IntentAsserts.assertHasExtraList;
+import static com.android.documentsui.testing.IntentAsserts.assertHasExtraUri;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -68,6 +72,7 @@
mEnv.roots,
mEnv.docs,
mEnv.selectionMgr,
+ mEnv.searchViewManager,
mEnv::lookupExecutor,
mActionModeAddons,
mDialogs,
@@ -78,7 +83,7 @@
mDialogs.confirmNext();
- mEnv.selectionMgr.toggleSelection("1");
+ mEnv.selectDocument(TestEnv.FILE_GIF);
mHandler.reset(mEnv.model, false);
}
@@ -107,24 +112,76 @@
}
@Test
- public void testDeleteDocuments() {
+ public void testDeleteSelectedDocuments() {
mEnv.populateStack();
mHandler.deleteSelectedDocuments();
mDialogs.assertNoFileFailures();
mActivity.startService.assertCalled();
- mActionModeAddons.finishOnConfimed.assertConfirmed();
+ mActionModeAddons.finishOnConfirmed.assertConfirmed();
}
@Test
- public void testDeleteDocuments_Cancelable() {
+ public void testDeleteSelectedDocuments_Cancelable() {
mEnv.populateStack();
mDialogs.rejectNext();
mHandler.deleteSelectedDocuments();
mDialogs.assertNoFileFailures();
mActivity.startService.assertNotCalled();
- mActionModeAddons.finishOnConfimed.assertRejected();
+ mActionModeAddons.finishOnConfirmed.assertRejected();
+ }
+
+ @Test
+ public void testShareSelectedDocuments_ShowsChooser() {
+ mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
+ mHandler.shareSelectedDocuments();
+
+ mActivity.assertActivityStarted(Intent.ACTION_CHOOSER);
+ }
+
+ @Test
+ public void testShareSelectedDocuments_Single() {
+ mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
+ mHandler.shareSelectedDocuments();
+
+ Intent intent = assertHasExtraIntent(mActivity.startActivity.getLastValue());
+ assertHasAction(intent, Intent.ACTION_SEND);
+ assertHasExtraUri(intent, Intent.EXTRA_STREAM);
+ }
+
+ @Test
+ public void testShareSelectedDocuments_Multiple() {
+ mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
+ mEnv.selectDocument(TestEnv.FILE_PDF);
+ mHandler.shareSelectedDocuments();
+
+ Intent intent = assertHasExtraIntent(mActivity.startActivity.getLastValue());
+ assertHasAction(intent, Intent.ACTION_SEND_MULTIPLE);
+ assertHasExtraList(intent, Intent.EXTRA_STREAM, 2);
+ }
+
+ @Test
+ public void testShareSelectedDocuments_OmitsVirtualFiles() {
+ mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
+ mEnv.selectDocument(TestEnv.FILE_VIRTUAL);
+ mHandler.shareSelectedDocuments();
+
+ Intent intent = assertHasExtraIntent(mActivity.startActivity.getLastValue());
+ assertHasAction(intent, Intent.ACTION_SEND);
+ assertHasExtraUri(intent, Intent.EXTRA_STREAM);
+ }
+
+ @Test
+ public void testShareSelectedDocuments_OmitsPartialFiles() {
+ mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
+ mEnv.selectDocument(TestEnv.FILE_PARTIAL);
+ mEnv.selectDocument(TestEnv.FILE_PNG);
+ mHandler.shareSelectedDocuments();
+
+ Intent intent = assertHasExtraIntent(mActivity.startActivity.getLastValue());
+ assertHasAction(intent, Intent.ACTION_SEND_MULTIPLE);
+ assertHasExtraList(intent, Intent.EXTRA_STREAM, 2);
}
@Test
@@ -165,7 +222,7 @@
mActivity.currentRoot = TestRootsAccess.HOME;
mHandler.onDocumentPicked(TestEnv.FILE_ARCHIVE);
- mActivity.openContainer.assertLastArgument(TestEnv.FILE_ARCHIVE);
+ assertEquals(TestEnv.FILE_ARCHIVE, mEnv.state.stack.peek());
}
@Test
@@ -173,7 +230,7 @@
mActivity.currentRoot = TestRootsAccess.HOME;
mHandler.onDocumentPicked(TestEnv.FOLDER_1);
- mActivity.openContainer.assertLastArgument(TestEnv.FOLDER_1);
+ assertEquals(TestEnv.FOLDER_1, mEnv.state.stack.peek());
}
@Test
diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
index 35199c3..8311443 100644
--- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java
@@ -18,6 +18,7 @@
import static com.android.documentsui.base.State.ACTION_GET_CONTENT;
import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -59,6 +60,7 @@
mEnv.roots,
mEnv.docs,
mEnv.selectionMgr,
+ mEnv.searchViewManager,
mEnv::lookupExecutor,
null // tuner, not currently used.
);
@@ -119,6 +121,15 @@
assertRootPicked(TestRootsAccess.HOME.getUri());
}
+ @Test
+ public void testOpenContainerDocument() {
+ mHandler.openContainerDocument(TestEnv.FOLDER_0);
+
+ assertEquals(TestEnv.FOLDER_0, mEnv.state.stack.peek());
+
+ mActivity.refreshCurrentRootAndDirectory.assertCalled();
+ }
+
private void assertRootPicked(Uri expectedUri) throws Exception {
mEnv.beforeAsserts();