diff options
author | 2016-09-26 10:01:45 -0700 | |
---|---|---|
committer | 2016-10-18 13:54:42 -0700 | |
commit | 1686883a8d049b399e34954a4feaa98490277ae8 (patch) | |
tree | 3765df9473bdda9a7699b052a9e989edfc240655 | |
parent | 07f58a055d1ad67e59ea3f3c1cee2cdbeebaf310 (diff) |
Features around findPath API.
* Add folders into search result
* Allow callers passing data to launch pickers at specfic location
* Rewire loadDocument() to LoadDocStackTask
* Remove VIEW intent for FilesActivity and related OpenUriForViewTask
* Add a ProviderAccess to enable testing in LoadDocStackTask
* Fix a wrong assertion in files/ActionHandlerTest
Change-Id: Iacc2b99dc68cbb4a40a4c445c69473973123c5bf
27 files changed, 456 insertions, 191 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 2e678b74f..65c7cdca5 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -79,19 +79,6 @@ <action android:name="android.intent.action.VIEW_DOWNLOADS" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> - <intent-filter> - <action android:name="android.intent.action.VIEW" /> - <category android:name="android.intent.category.DEFAULT" /> - <data android:mimeType="application/zip" - android:host="com.android.providers.downloads.documents" - android:scheme="content" /> - <data android:mimeType="application/x-zip" - android:host="com.android.providers.downloads.documents" - android:scheme="content" /> - <data android:mimeType="application/x-zip-compressed" - android:host="com.android.providers.downloads.documents" - android:scheme="content" /> - </intent-filter> </activity> <activity diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java index f5417753e..390dd0053 100644 --- a/src/com/android/documentsui/AbstractActionHandler.java +++ b/src/com/android/documentsui/AbstractActionHandler.java @@ -22,10 +22,12 @@ import android.content.Intent; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Parcelable; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import android.util.Log; import com.android.documentsui.AbstractActionHandler.CommonAddons; -import com.android.documentsui.archives.ArchivesProvider; +import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback; import com.android.documentsui.base.BooleanConsumer; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; @@ -37,7 +39,6 @@ import com.android.documentsui.dirlist.AnimationView.AnimationType; import com.android.documentsui.dirlist.AnimationView; import com.android.documentsui.dirlist.DocumentDetails; import com.android.documentsui.files.LauncherActivity; -import com.android.documentsui.files.OpenUriForViewTask; import com.android.documentsui.roots.LoadRootTask; import com.android.documentsui.roots.RootsAccess; import com.android.documentsui.selection.Selection; @@ -45,6 +46,7 @@ import com.android.documentsui.selection.SelectionManager; import com.android.documentsui.sidebar.EjectRootTask; import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -53,10 +55,13 @@ import java.util.concurrent.Executor; public abstract class AbstractActionHandler<T extends Activity & CommonAddons> implements ActionHandler { + private static final String TAG = "AbstractActionHandler"; + protected final T mActivity; protected final State mState; protected final RootsAccess mRoots; protected final DocumentsAccess mDocs; + protected final ProviderAccess mProviders; protected final SelectionManager mSelectionMgr; protected final SearchViewManager mSearchMgr; protected final Lookup<String, Executor> mExecutors; @@ -66,6 +71,7 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons> State state, RootsAccess roots, DocumentsAccess docs, + ProviderAccess providers, SelectionManager selectionMgr, SearchViewManager searchMgr, Lookup<String, Executor> executors) { @@ -73,13 +79,16 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons> assert(activity != null); assert(state != null); assert(roots != null); + assert(providers != null); assert(selectionMgr != null); + assert(searchMgr != null); assert(docs != null); mActivity = activity; mState = state; mRoots = roots; mDocs = docs; + mProviders = providers; mSelectionMgr = selectionMgr; mSearchMgr = searchMgr; mExecutors = executors; @@ -159,6 +168,50 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons> @Override public void openContainerDocument(DocumentInfo doc) { assert(doc.isContainer()); + + if (mSearchMgr.isSearching()) { + loadDocument( + doc.derivedUri, + (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc)); + } else { + openChildContainer(doc); + } + } + + private void openFolderInSearchResult(@Nullable DocumentStack stack, DocumentInfo doc) { + if (stack == null) { + mState.popDocumentsToRoot(); + + // Update navigator to give horizontal breadcrumb a chance to update documents. It + // doesn't update its content if the size of document stack doesn't change. + // TODO: update breadcrumb to take range update. + mActivity.updateNavigator(); + + mState.pushDocument(doc); + } else { + if (!Objects.equals(mState.stack.root, stack.root)) { + Log.w(TAG, "Provider returns " + stack.root + " rather than expected " + + mState.stack.root); + } + + mState.stack.clear(); + // Update navigator to give horizontal breadcrumb a chance to update documents. It + // doesn't update its content if the size of document stack doesn't change. + // TODO: update breadcrumb to take range update. + mActivity.updateNavigator(); + + mState.setStack(stack); + } + + // 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); + } + + private void openChildContainer(DocumentInfo doc) { DocumentInfo currentDoc = null; if (doc.isDirectory()) { @@ -171,8 +224,8 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons> assert(currentDoc != null); mActivity.notifyDirectoryNavigated(currentDoc.derivedUri); - mState.pushDocument(currentDoc); + mState.pushDocument(currentDoc); // 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. @@ -191,10 +244,15 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons> 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())); + protected final void loadDocument(Uri uri, LoadDocStackCallback callback) { + new LoadDocStackTask( + mActivity, + uri, + mRoots, + mDocs, + mProviders, + callback + ).executeOnExecutor(mExecutors.lookup(uri.getAuthority())); } @Override @@ -225,6 +283,9 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons> DocumentInfo getCurrentDirectory(); void setRootsDrawerOpen(boolean open); + // TODO: Let navigator listens to State + void updateNavigator(); + @VisibleForTesting void notifyDirectoryNavigated(Uri docUri); } diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java index 98d24eda9..7f6f923d0 100644 --- a/src/com/android/documentsui/ActionHandler.java +++ b/src/com/android/documentsui/ActionHandler.java @@ -49,8 +49,6 @@ public interface ActionHandler { void loadRoot(Uri uri); - void loadDocument(Uri uri); - void openSelectedInNewWindow(); void openInNewWindow(DocumentStack path); diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java index 235757e43..d0160cebf 100644 --- a/src/com/android/documentsui/BaseActivity.java +++ b/src/com/android/documentsui/BaseActivity.java @@ -413,6 +413,12 @@ public abstract class BaseActivity<T extends ActionHandler> && !root.isDownloads(); } + // TODO: make navigator listen to state + @Override + public final void updateNavigator() { + mNavigator.update(); + } + /** * Refreshes the content of the director and the menu/action bar. * The current directory name and selection will get updated. diff --git a/src/com/android/documentsui/DirectoryLoader.java b/src/com/android/documentsui/DirectoryLoader.java index 56e76bbdf..8af1ab3a1 100644 --- a/src/com/android/documentsui/DirectoryLoader.java +++ b/src/com/android/documentsui/DirectoryLoader.java @@ -33,6 +33,7 @@ import android.util.Log; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.FilteringCursorWrapper; import com.android.documentsui.base.RootInfo; +import com.android.documentsui.base.Shared; import com.android.documentsui.roots.RootCursorWrapper; import com.android.documentsui.sorting.SortModel; @@ -101,8 +102,8 @@ public class DirectoryLoader extends AsyncTaskLoader<DirectoryResult> { cursor = new RootCursorWrapper(mUri.getAuthority(), mRoot.rootId, cursor, -1); - if (mSearchMode) { - // Filter directories out of search results, for now + if (mSearchMode && !Shared.ENABLE_OMC_API_FEATURES) { + // There is no findPath API. Enable filtering on folders in search mode. cursor = new FilteringCursorWrapper(cursor, null, SEARCH_REJECT_MIMES); } diff --git a/src/com/android/documentsui/DocumentsAccess.java b/src/com/android/documentsui/DocumentsAccess.java index 5d72ab8d0..8cfaa5a54 100644 --- a/src/com/android/documentsui/DocumentsAccess.java +++ b/src/com/android/documentsui/DocumentsAccess.java @@ -17,8 +17,11 @@ package com.android.documentsui; import android.annotation.Nullable; +import android.content.ContentProviderClient; import android.content.Context; +import android.database.Cursor; import android.net.Uri; +import android.os.RemoteException; import android.provider.DocumentsContract; import android.util.Log; @@ -27,6 +30,8 @@ import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.RootInfo; import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; /** * Provides synchronous access to {@link DocumentInfo} instances given some identifying information. @@ -34,10 +39,11 @@ import java.io.FileNotFoundException; public interface DocumentsAccess { @Nullable DocumentInfo getRootDocument(RootInfo root); - @Nullable DocumentInfo getRootDocument(Uri uri); @Nullable DocumentInfo getDocument(Uri uri); @Nullable DocumentInfo getArchiveDocument(Uri uri); + @Nullable List<DocumentInfo> getDocuments(String authority, List<String> docIds); + public static DocumentsAccess create(Context context) { return new RuntimeDocumentAccess(context); } @@ -54,29 +60,47 @@ public interface DocumentsAccess { @Override public @Nullable DocumentInfo getRootDocument(RootInfo root) { - return getRootDocument( + return getDocument( DocumentsContract.buildDocumentUri(root.authority, root.documentId)); } @Override - public @Nullable DocumentInfo getRootDocument(Uri uri) { + public @Nullable DocumentInfo getDocument(Uri uri) { try { return DocumentInfo.fromUri(mContext.getContentResolver(), uri); } catch (FileNotFoundException e) { - Log.w(TAG, "Failed to find root", e); - return null; + Log.w(TAG, "Couldn't create DocumentInfo for uri: " + uri); } + + return null; } @Override - public DocumentInfo getDocument(Uri uri) { - try { - return DocumentInfo.fromUri(mContext.getContentResolver(), uri); - } catch (FileNotFoundException e) { - Log.w(TAG, "Couldn't create DocumentInfo for uri: " + uri); + public @Nullable List<DocumentInfo> getDocuments(String authority, List<String> docIds) { + try(final ContentProviderClient client = DocumentsApplication + .acquireUnstableProviderOrThrow(mContext.getContentResolver(), authority)) { + + List<DocumentInfo> result = new ArrayList<>(docIds.size()); + for (String docId : docIds) { + final Uri uri = DocumentsContract.buildDocumentUri(authority, docId); + try (final Cursor cursor = client.query(uri, null, null, null, null)) { + if (!cursor.moveToNext()) { + Log.e(TAG, "Couldn't create DocumentInfo for Uri: " + uri); + return null; + } + + result.add(DocumentInfo.fromCursor(cursor, authority)); + } catch (Exception e) { + Log.e(TAG, "Couldn't create DocumentInfo for Uri: " + uri); + return null; + } + } + + return result; + } catch (RemoteException e) { + Log.w(TAG, "Couldn't get a content provider client." ,e); + return null; } - - return null; } @Override diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java index 3f1d48b76..64eeb1403 100644 --- a/src/com/android/documentsui/DocumentsApplication.java +++ b/src/com/android/documentsui/DocumentsApplication.java @@ -28,6 +28,7 @@ import android.net.Uri; import android.os.RemoteException; import android.text.format.DateUtils; +import com.android.documentsui.ProviderAccess.RuntimeProviderAccess; import com.android.documentsui.clipping.ClipStorage; import com.android.documentsui.clipping.ClipStore; import com.android.documentsui.clipping.DocumentClipper; @@ -42,6 +43,8 @@ public class DocumentsApplication extends Application { private ClipStorage mClipStore; private DocumentClipper mClipper; + private ProviderAccess mProviderAccess; + public static RootsCache getRootsCache(Context context) { return ((DocumentsApplication) context.getApplicationContext()).mRoots; } @@ -51,6 +54,11 @@ public class DocumentsApplication extends Application { return app.mThumbnailCache; } + public static ProviderAccess getProviderAccess(Context context) { + final DocumentsApplication app = (DocumentsApplication) context.getApplicationContext(); + return app.mProviderAccess; + } + public static ContentProviderClient acquireUnstableProviderOrThrow( ContentResolver resolver, String authority) throws RemoteException { final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( @@ -87,6 +95,8 @@ public class DocumentsApplication extends Application { getSharedPreferences(ClipStorage.PREF_NAME, 0)); mClipper = new DocumentClipper(this, mClipStore); + mProviderAccess = new RuntimeProviderAccess(getContentResolver()); + final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); diff --git a/src/com/android/documentsui/LoadDocStackTask.java b/src/com/android/documentsui/LoadDocStackTask.java new file mode 100644 index 000000000..e849a1b4e --- /dev/null +++ b/src/com/android/documentsui/LoadDocStackTask.java @@ -0,0 +1,115 @@ +/* + * 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.Nullable; +import android.app.Activity; +import android.net.Uri; +import android.provider.DocumentsContract.Path; +import android.util.Log; + +import com.android.documentsui.base.DocumentInfo; +import com.android.documentsui.base.DocumentStack; +import com.android.documentsui.base.PairedTask; +import com.android.documentsui.base.RootInfo; +import com.android.documentsui.base.Shared; +import com.android.documentsui.roots.RootsAccess; + +import java.util.List; + +/** + * Loads {@link DocumentStack} for given document. It provides its best effort to find the path of + * the given document. + * + * If it fails to load correct path it calls callback with different result + * depending on the nullness of given root. If given root is null it calls callback with null. If + * given root is not null it calls callback with a {@link DocumentStack} as if the given doc lives + * under the root doc. + */ +public class LoadDocStackTask extends PairedTask<Activity, Void, DocumentStack> { + private static final String TAG = "LoadDocStackTask"; + + private final RootsAccess mRoots; + private final DocumentsAccess mDocs; + private final Uri mDocUri; + private final String mAuthority; + private final ProviderAccess mProviders; + private final LoadDocStackCallback mCallback; + + public LoadDocStackTask( + Activity activity, + Uri docUri, + RootsAccess roots, + DocumentsAccess docs, + ProviderAccess providers, + LoadDocStackCallback callback) { + super(activity); + mRoots = roots; + mDocs = docs; + mDocUri = docUri; + mAuthority = docUri.getAuthority(); + mProviders = providers; + mCallback = callback; + } + + @Override + public @Nullable DocumentStack run(Void... args) { + if (Shared.ENABLE_OMC_API_FEATURES) { + try { + final Path path = mProviders.findPath(mDocUri); + if (path != null) { + return buildStack(path); + } else { + Log.i(TAG, "Remote provider doesn't support findPath."); + } + } catch (Exception e) { + Log.e(TAG, "Failed to build document stack for uri: " + mDocUri, e); + // Fallback to old behavior. + } + } + + return null; + } + + @Override + public void finish(@Nullable DocumentStack stack){ + mCallback.onDocumentStackLoaded(stack); + } + + private @Nullable DocumentStack buildStack(Path path) { + final String rootId = path.getRootId(); + if (rootId == null) { + Log.e(TAG, "Provider doesn't provide root id."); + return null; + } + + RootInfo root = mRoots.getRootOneshot(mAuthority, path.getRootId()); + List<DocumentInfo> docs = mDocs.getDocuments(mAuthority, path.getPath()); + + if (root == null || docs == null) { + Log.e(TAG, "Either root: " + root + " or docs: " + docs + " failed to load."); + return null; + } + + return new DocumentStack(root, docs); + } + + @FunctionalInterface + public interface LoadDocStackCallback { + void onDocumentStackLoaded(@Nullable DocumentStack stack); + } +} diff --git a/src/com/android/documentsui/ProviderAccess.java b/src/com/android/documentsui/ProviderAccess.java new file mode 100644 index 000000000..67a0e3fbe --- /dev/null +++ b/src/com/android/documentsui/ProviderAccess.java @@ -0,0 +1,48 @@ +/* + * 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.content.ContentProviderClient; +import android.content.ContentResolver; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Path; + +/** + * Provides synchronous {@link android.provider.DocumentsProvider} access. + */ +public interface ProviderAccess { + Path findPath(Uri docUri) throws RemoteException; + + class RuntimeProviderAccess implements ProviderAccess { + + private final ContentResolver mResolver; + + RuntimeProviderAccess(ContentResolver resolver) { + mResolver = resolver; + } + + @Override + public Path findPath(Uri docUri) throws RemoteException { + try (final ContentProviderClient client = DocumentsApplication + .acquireUnstableProviderOrThrow(mResolver, docUri.getAuthority())) { + return DocumentsContract.findPath(client, docUri); + } + } + } +} diff --git a/src/com/android/documentsui/base/DocumentStack.java b/src/com/android/documentsui/base/DocumentStack.java index e9784094f..c3a13e0c8 100644 --- a/src/com/android/documentsui/base/DocumentStack.java +++ b/src/com/android/documentsui/base/DocumentStack.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.net.ProtocolException; import java.util.Collection; import java.util.LinkedList; +import java.util.List; /** * Representation of a stack of {@link DocumentInfo}, usually the result of a @@ -53,6 +54,14 @@ public class DocumentStack extends LinkedList<DocumentInfo> implements Durable, this.root = root; } + public DocumentStack(RootInfo root, List<DocumentInfo> docs) { + for (DocumentInfo doc : docs) { + push(doc); + } + + this.root = root; + } + /** * Makes a new copy, and pushes all docs to the new copy in the same order as they're passed * as parameters, i.e. the last document will be at the top of the stack. diff --git a/src/com/android/documentsui/base/Shared.java b/src/com/android/documentsui/base/Shared.java index cfa102dab..63f288669 100644 --- a/src/com/android/documentsui/base/Shared.java +++ b/src/com/android/documentsui/base/Shared.java @@ -47,6 +47,8 @@ public final class Shared { public static final boolean DEBUG = true; + public static final boolean ENABLE_OMC_API_FEATURES = true; + /** Intent action name to pick a copy destination. */ public static final String ACTION_PICK_COPY_DESTINATION = "com.android.documentsui.PICK_COPY_DESTINATION"; @@ -59,7 +61,7 @@ public final class Shared { public static final String EXTRA_PRODUCTIVITY_MODE = "com.android.documentsui.PRODUCTIVITY"; /** - * Extra boolean flag for {@link ACTION_PICK_COPY_DESTINATION}, which + * Extra boolean flag for {@link #ACTION_PICK_COPY_DESTINATION}, which * specifies if the destination directory needs to create new directory or not. */ public static final String EXTRA_DIRECTORY_COPY = "com.android.documentsui.DIRECTORY_COPY"; diff --git a/src/com/android/documentsui/base/State.java b/src/com/android/documentsui/base/State.java index 969b05789..f0d47bbda 100644 --- a/src/com/android/documentsui/base/State.java +++ b/src/com/android/documentsui/base/State.java @@ -147,6 +147,14 @@ public class State implements android.os.Parcelable { mStackTouched = true; } + public void popDocumentsToRoot() { + if (DEBUG) Log.d(TAG, "Popping docs to root folder."); + while (stack.size() > 1) { + stack.pop(); + } + mStackTouched = true; + } + public void setStack(DocumentStack stack) { if (DEBUG) Log.d(TAG, "Setting the whole darn stack to: " + stack); this.stack = stack; diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java index d59e2a7e4..403be64d9 100644 --- a/src/com/android/documentsui/files/ActionHandler.java +++ b/src/com/android/documentsui/files/ActionHandler.java @@ -41,6 +41,7 @@ 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.ProviderAccess; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.clipping.ClipStore; @@ -86,6 +87,7 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa State state, RootsAccess roots, DocumentsAccess docs, + ProviderAccess providers, SelectionManager selectionMgr, SearchViewManager searchMgr, Lookup<String, Executor> executors, @@ -95,7 +97,7 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa DocumentClipper clipper, ClipStore clipStore) { - super(activity, state, roots, docs, selectionMgr, searchMgr, executors); + super(activity, state, roots, docs, providers, selectionMgr, searchMgr, executors); mActionModeAddons = actionModeAddons; mDialogs = dialogs; @@ -297,11 +299,6 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa return; } - if (launchToDocument(intent)) { - if (DEBUG) Log.d(TAG, "Launched to root for viewing (likely a ZIP)."); - return; - } - if (launchToRoot(intent)) { if (DEBUG) Log.d(TAG, "Launched to root for browsing."); return; @@ -336,19 +333,6 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa return true; } - // Zips in downloads are not opened inline, because of Downloads no-folders policy. - // So we're registered to handle VIEWs of zips. - private boolean launchToDocument(Intent intent) { - if (Intent.ACTION_VIEW.equals(intent.getAction())) { - Uri uri = intent.getData(); - assert(uri != null); - loadDocument(uri); - return true; - } - - return false; - } - private boolean launchToRoot(Intent intent) { if (DocumentsContract.ACTION_BROWSE.equals(intent.getAction())) { Uri uri = intent.getData(); diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java index 30bced713..e934d4849 100644 --- a/src/com/android/documentsui/files/FilesActivity.java +++ b/src/com/android/documentsui/files/FilesActivity.java @@ -112,6 +112,7 @@ public class FilesActivity mState, mRoots, mDocs, + DocumentsApplication.getProviderAccess(this), mSelectionMgr, mSearchManager, ProviderExecutor::forAuthority, diff --git a/src/com/android/documentsui/files/OpenUriForViewTask.java b/src/com/android/documentsui/files/OpenUriForViewTask.java deleted file mode 100644 index 2a830c599..000000000 --- a/src/com/android/documentsui/files/OpenUriForViewTask.java +++ /dev/null @@ -1,92 +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.files; - -import android.app.Activity; -import android.net.Uri; -import android.util.Log; - -import com.android.documentsui.AbstractActionHandler.CommonAddons; -import com.android.documentsui.DocumentsAccess; -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; - -import javax.annotation.Nullable; - -/** - * Builds a stack for the specific Uris. Multi roots are not supported, as it's impossible - * to know which root to select. Also, the stack doesn't contain intermediate directories. - * It's primarly used for opening ZIP archives from Downloads app. - */ -public final class OpenUriForViewTask<T extends Activity & CommonAddons> - extends PairedTask<T, Void, Void> { - - private static final String TAG = "OpenUriForViewTask"; - - private final T mActivity; - private final State mState; - private final RootsAccess mRoots; - private final DocumentsAccess mDocs; - private final Uri mUri; - - public OpenUriForViewTask( - T activity, State state, RootsAccess roots, DocumentsAccess docs, Uri uri) { - super(activity); - mActivity = activity; - mState = state; - mRoots = roots; - mDocs = docs; - mUri = uri; - } - - @Override - public Void run(Void... params) { - - final String authority = mUri.getAuthority(); - final Collection<RootInfo> roots = mRoots.getRootsForAuthorityBlocking(authority); - - if (roots.isEmpty()) { - Log.e(TAG, "Failed to find root for the requested Uri: " + mUri); - return null; - } - - assert(mState.stack.isEmpty()); - - // NOTE: There's no guarantee that this root will be the correct root for the doc. - final RootInfo root = roots.iterator().next(); - mState.stack.root = root; - mState.stack.add(mDocs.getRootDocument(root)); - @Nullable DocumentInfo doc = mDocs.getDocument(mUri); - if (doc == null) { - Log.e(TAG, "Failed to resolve DocumentInfo from Uri: " + mUri); - } else { - mState.stack.add(doc); - } - - return null; - } - - @Override - public void finish(Void result) { - mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); - } -} diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java index 03c8a9036..906b26ae7 100644 --- a/src/com/android/documentsui/picker/ActionHandler.java +++ b/src/com/android/documentsui/picker/ActionHandler.java @@ -37,6 +37,7 @@ 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.ProviderAccess; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.Shared; import com.android.documentsui.base.State; @@ -66,12 +67,13 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T State state, RootsAccess roots, DocumentsAccess docs, + ProviderAccess providers, SelectionManager selectionMgr, SearchViewManager searchMgr, Lookup<String, Executor> executors, ActivityConfig activityConfig) { - super(activity, state, roots, docs, selectionMgr, searchMgr, executors); + super(activity, state, roots, docs, providers, selectionMgr, searchMgr, executors); mConfig = activityConfig; mScope = new ContentScope(this::onModelLoaded); @@ -94,13 +96,36 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T if (Shared.ACTION_PICK_COPY_DESTINATION.equals(intent.getAction())) { if (DEBUG) Log.d(TAG, "Launching directly into Home directory."); loadHomeDir(); + } else if (intent.getData() != null) { + Uri uri = intent.getData(); + loadDocument( + uri, + (@Nullable DocumentStack stack) -> onStackLoaded(uri, stack)); } else { - if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package."); - new LoadLastAccessedStackTask<>(mActivity, mState, mRoots).execute(); + loadLastAccessedStack(); } } } + private void onStackLoaded(Uri uri, @Nullable DocumentStack stack) { + if (stack != null) { + if (!stack.peek().isContainer()) { + // Requested document is not a container. Pop it so that we can launch into its + // parent. + stack.pop(); + } + mState.setStack(stack); + } else { + Log.w(TAG, "Failed to launch into the given uri: " + uri); + loadLastAccessedStack(); + } + } + + private void loadLastAccessedStack() { + if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package."); + new LoadLastAccessedStackTask<>(mActivity, mState, mRoots).execute(); + } + @Override public void showAppDetails(ResolveInfo info) { final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java index 17e6bb1d4..645b7f795 100644 --- a/src/com/android/documentsui/picker/PickActivity.java +++ b/src/com/android/documentsui/picker/PickActivity.java @@ -101,6 +101,7 @@ public class PickActivity mState, mRoots, mDocs, + DocumentsApplication.getProviderAccess(this), mSelectionMgr, mSearchManager, ProviderExecutor::forAuthority, diff --git a/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java index 37bd60bd0..7b4795cf0 100644 --- a/tests/common/com/android/documentsui/TestActivity.java +++ b/tests/common/com/android/documentsui/TestActivity.java @@ -20,6 +20,7 @@ import static junit.framework.Assert.assertEquals; import android.app.Activity; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; @@ -139,6 +140,14 @@ public abstract class TestActivity extends AbstractBase { public final void setRootsDrawerOpen(boolean open) { setRootsDrawerOpen.accept(open); } + + @Override + public final ContentResolver getContentResolver() { + return null; + } + + @Override + public final void updateNavigator() {} } // Trick Mockito into finding our Addons methods correctly. W/o this diff --git a/tests/common/com/android/documentsui/dirlist/TestModel.java b/tests/common/com/android/documentsui/dirlist/TestModel.java index abe4d5f53..472114b9b 100644 --- a/tests/common/com/android/documentsui/dirlist/TestModel.java +++ b/tests/common/com/android/documentsui/dirlist/TestModel.java @@ -96,6 +96,7 @@ public class TestModel extends Model { DocumentInfo doc = new DocumentInfo(); doc.authority = mAuthority; doc.documentId = Integer.toString(++mLastId); + doc.derivedUri = DocumentsContract.buildDocumentUri(doc.authority, doc.documentId); doc.displayName = name; doc.mimeType = mimeType; doc.flags = flags; diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java index fd199cef2..bc4295a93 100644 --- a/tests/common/com/android/documentsui/testing/TestActionHandler.java +++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java @@ -39,6 +39,7 @@ public class TestActionHandler extends AbstractActionHandler<TestActivity> { env.state, env.roots, env.docs, + env.providers, env.selectionMgr, env.searchViewManager, (String authority) -> null); diff --git a/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java b/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java index d5c317ff1..7ded62444 100644 --- a/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java +++ b/tests/common/com/android/documentsui/testing/TestDocumentsAccess.java @@ -21,17 +21,15 @@ import com.android.documentsui.DocumentsAccess; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.RootInfo; +import java.util.List; + import javax.annotation.Nullable; public class TestDocumentsAccess implements DocumentsAccess { public @Nullable DocumentInfo nextRootDocument; public @Nullable DocumentInfo nextDocument; - - @Override - public DocumentInfo getRootDocument(Uri uri) { - return nextRootDocument; - } + public @Nullable List<DocumentInfo> nextDocuments; @Override public DocumentInfo getRootDocument(RootInfo root) { @@ -44,6 +42,11 @@ public class TestDocumentsAccess implements DocumentsAccess { } @Override + public List<DocumentInfo> getDocuments(String authority, List<String> docIds) { + return nextDocuments; + } + + @Override public DocumentInfo getArchiveDocument(Uri uri) { return nextDocument; } diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java index 566f3eb5a..7ea147c7d 100644 --- a/tests/common/com/android/documentsui/testing/TestEnv.java +++ b/tests/common/com/android/documentsui/testing/TestEnv.java @@ -19,7 +19,6 @@ import android.os.Handler; 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; @@ -51,9 +50,10 @@ public class TestEnv { public final State state = new State(); public final TestRootsAccess roots = new TestRootsAccess(); public final TestDocumentsAccess docs = new TestDocumentsAccess(); + public final TestProviderAccess providers = new TestProviderAccess(); public final TestModel model; public final SelectionManager selectionMgr; - public final SearchViewManager searchViewManager; + public final TestSearchViewManager searchViewManager; private TestEnv(String authority) { mExecutor = new TestScheduledExecutorService(); diff --git a/tests/common/com/android/documentsui/testing/TestProviderAccess.java b/tests/common/com/android/documentsui/testing/TestProviderAccess.java new file mode 100644 index 000000000..1887f81fb --- /dev/null +++ b/tests/common/com/android/documentsui/testing/TestProviderAccess.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.testing; + +import android.annotation.Nullable; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Path; + +import com.android.documentsui.ProviderAccess; + +public class TestProviderAccess implements ProviderAccess { + + public @Nullable Path nextPath; + + @Override + public DocumentsContract.Path findPath(Uri docUri) + throws RemoteException { + return nextPath; + } +} diff --git a/tests/common/com/android/documentsui/testing/TestSearchViewManager.java b/tests/common/com/android/documentsui/testing/TestSearchViewManager.java index cf06fa2de..3be8f6a93 100644 --- a/tests/common/com/android/documentsui/testing/TestSearchViewManager.java +++ b/tests/common/com/android/documentsui/testing/TestSearchViewManager.java @@ -26,8 +26,10 @@ import com.android.documentsui.SearchViewManager; */ public class TestSearchViewManager extends SearchViewManager { - boolean updateMenuCalled; - boolean showMenuCalled; + public boolean isSearching; + + private boolean mUpdateMenuCalled; + private boolean mShowMenuCalled; public TestSearchViewManager() { super( @@ -43,20 +45,25 @@ public class TestSearchViewManager extends SearchViewManager { } @Override + public boolean isSearching() { + return isSearching; + } + + @Override public void showMenu(boolean visible) { - showMenuCalled = true; + mShowMenuCalled = true; } @Override public void updateMenu() { - updateMenuCalled = true; + mUpdateMenuCalled = true; } public boolean showMenuCalled() { - return showMenuCalled; + return mShowMenuCalled; } public boolean updateMenuCalled() { - return updateMenuCalled; + return mUpdateMenuCalled; } } diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java index 1577ae265..2e56c54c3 100644 --- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import android.content.Intent; import android.os.Parcelable; +import android.provider.DocumentsContract.Path; import android.support.test.filters.MediumTest; import android.support.test.runner.AndroidJUnit4; @@ -30,11 +31,14 @@ import com.android.documentsui.dirlist.DocumentDetails; import com.android.documentsui.files.LauncherActivity; import com.android.documentsui.testing.Roots; import com.android.documentsui.testing.TestEnv; +import com.android.documentsui.testing.TestRootsAccess; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Arrays; + /** * A unit test *for* AbstractActionHandler, not an abstract test baseclass. */ @@ -55,6 +59,7 @@ public class AbstractActionHandlerTest { mEnv.state, mEnv.roots, mEnv.docs, + mEnv.providers, mEnv.selectionMgr, mEnv.searchViewManager, mEnv::lookupExecutor) { @@ -86,4 +91,43 @@ public class AbstractActionHandlerTest { Intent actual = mActivity.startActivity.getLastValue(); assertEquals(expected.toString(), actual.toString()); } + + @Test + public void testOpensContainerDocuments_jumpToNewLocation() throws Exception { + mEnv.populateStack(); + + mEnv.searchViewManager.isSearching = true; + mEnv.providers.nextPath = new Path( + TestRootsAccess.HOME.rootId, + Arrays.asList(TestEnv.FOLDER_1.documentId, TestEnv.FOLDER_2.documentId)); + mEnv.docs.nextDocuments = Arrays.asList(TestEnv.FOLDER_1, TestEnv.FOLDER_2); + + mEnv.state.pushDocument(TestEnv.FOLDER_0); + + mHandler.openContainerDocument(TestEnv.FOLDER_2); + + mEnv.beforeAsserts(); + + assertEquals(mEnv.providers.nextPath.getPath().size(), mEnv.state.stack.size()); + assertEquals(TestEnv.FOLDER_2, mEnv.state.stack.peek()); + } + + + @Test + public void testOpensContainerDocuments_pushToRootDoc_NoFindPathSupport() throws Exception { + mEnv.populateStack(); + + mEnv.searchViewManager.isSearching = true; + mEnv.docs.nextDocuments = Arrays.asList(TestEnv.FOLDER_1, TestEnv.FOLDER_2); + + mEnv.state.pushDocument(TestEnv.FOLDER_0); + + mHandler.openContainerDocument(TestEnv.FOLDER_2); + + mEnv.beforeAsserts(); + + assertEquals(2, mEnv.state.stack.size()); + assertEquals(TestEnv.FOLDER_2, mEnv.state.stack.pop()); + assertEquals(TestEnv.FOLDER_0, mEnv.state.stack.pop()); + } } diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java index bd582630f..97b199d91 100644 --- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java @@ -20,6 +20,7 @@ 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; @@ -32,7 +33,6 @@ import android.support.test.runner.AndroidJUnit4; import com.android.documentsui.R; import com.android.documentsui.TestActionModeAddons; -import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.Shared; @@ -71,6 +71,7 @@ public class ActionHandlerTest { mEnv.state, mEnv.roots, mEnv.docs, + mEnv.providers, mEnv.selectionMgr, mEnv.searchViewManager, mEnv::lookupExecutor, @@ -259,32 +260,6 @@ public class ActionHandlerTest { } @Test - public void testInitLocation_ViewDocument() throws Exception { - Intent intent = mActivity.getIntent(); - intent.setAction(Intent.ACTION_VIEW); - - // configure DocumentsAccess to return something. - mEnv.docs.nextRootDocument = TestEnv.FOLDER_0; - mEnv.docs.nextDocument = TestEnv.FILE_GIF; - - Uri destUri = mEnv.model.getItemUri(TestEnv.FILE_GIF.documentId); - intent.setData(destUri); - - mEnv.state.stack.clear(); // Stack must be clear, we've even got an assert! - mHandler.initLocation(intent); - assertDocumentPicked(destUri); - } - - private void assertDocumentPicked(Uri expectedUri) throws Exception { - mEnv.beforeAsserts(); - - mActivity.refreshCurrentRootAndDirectory.assertCalled(); - DocumentInfo doc = mEnv.state.stack.peekLast(); - Uri actualUri = mEnv.model.getItemUri(doc.documentId); - assertEquals(expectedUri, actualUri); - } - - @Test public void testInitLocation_BrowseRoot() throws Exception { Intent intent = mActivity.getIntent(); intent.setAction(DocumentsContract.ACTION_BROWSE); diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java index 83114433d..b6897de85 100644 --- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java @@ -59,6 +59,7 @@ public class ActionHandlerTest { mEnv.state, mEnv.roots, mEnv.docs, + mEnv.providers, mEnv.selectionMgr, mEnv.searchViewManager, mEnv::lookupExecutor, |