diff options
33 files changed, 1025 insertions, 547 deletions
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java index 8b6812b1b..ec1597b79 100644 --- a/src/com/android/documentsui/AbstractActionHandler.java +++ b/src/com/android/documentsui/AbstractActionHandler.java @@ -20,6 +20,7 @@ import android.app.Activity; import android.content.ClipData; import android.content.Intent; import android.content.pm.ResolveInfo; +import android.net.Uri; import android.os.Parcelable; import com.android.documentsui.AbstractActionHandler.CommonAddons; @@ -27,15 +28,21 @@ import com.android.documentsui.base.BooleanConsumer; import com.android.documentsui.base.ConfirmationCallback; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; +import com.android.documentsui.base.Lookup; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.Shared; +import com.android.documentsui.base.State; +import com.android.documentsui.dirlist.AnimationView.AnimationType; import com.android.documentsui.dirlist.DocumentDetails; import com.android.documentsui.dirlist.Model; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.manager.LauncherActivity; +import com.android.documentsui.roots.LoadRootTask; +import com.android.documentsui.roots.RootsAccess; import com.android.documentsui.sidebar.EjectRootTask; import java.util.List; +import java.util.concurrent.Executor; /** * Provides support for specializing the actions (viewDocument etc.) to the host activity. @@ -44,10 +51,24 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons> implements ActionHandler { protected final T mActivity; + protected final State mState; + protected final RootsAccess mRoots; + protected final Lookup<String, Executor> mExecutors; + + public AbstractActionHandler( + T activity, + State state, + RootsAccess roots, + Lookup<String, Executor> executors) { - public AbstractActionHandler(T activity) { assert(activity != null); + assert(state != null); + assert(roots != null); + mActivity = activity; + mState = state; + mRoots = roots; + mExecutors = executors; } @Override @@ -116,14 +137,25 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons> throw new UnsupportedOperationException("Delete not supported!"); } + @Override + public final void loadRoot(Uri uri) { + new LoadRootTask<>(mActivity, mRoots, mState, uri).executeOnExecutor( + mExecutors.lookup(uri.getAuthority())); + } + + protected final void loadHomeDir() { + loadRoot(Shared.getDefaultRootUri(mActivity)); + } + /** * A class primarily for the support of isolating our tests * from our concrete activity implementations. */ public interface CommonAddons { void onRootPicked(RootInfo root); - void onDocumentPicked(DocumentInfo doc, Model model); // TODO: Move this to PickAddons. void onDocumentsPicked(List<DocumentInfo> docs); + void onDocumentPicked(DocumentInfo doc, Model model); + void refreshCurrentRootAndDirectory(@AnimationType int anim); } } diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java index a654fa0cf..f4aa0caec 100644 --- a/src/com/android/documentsui/ActionHandler.java +++ b/src/com/android/documentsui/ActionHandler.java @@ -17,7 +17,9 @@ package com.android.documentsui; import android.content.ClipData; +import android.content.Intent; import android.content.pm.ResolveInfo; +import android.net.Uri; import com.android.documentsui.base.BooleanConsumer; import com.android.documentsui.base.ConfirmationCallback; @@ -48,6 +50,8 @@ public interface ActionHandler { void openRoot(ResolveInfo app); + void loadRoot(Uri uri); + void openInNewWindow(DocumentStack path); void pasteIntoFolder(RootInfo root); @@ -59,4 +63,11 @@ public interface ActionHandler { boolean openDocument(DocumentDetails doc); void deleteDocuments(Model model, Selection selection, ConfirmationCallback callback); + + /** + * Called when initial activity setup is complete. Implementations + * should override this method to set the initial location of the + * app. + */ + void initLocation(Intent intent); } diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java index b1dfd45c5..d9d6cf41c 100644 --- a/src/com/android/documentsui/BaseActivity.java +++ b/src/com/android/documentsui/BaseActivity.java @@ -29,7 +29,6 @@ import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; import android.content.Intent; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; @@ -69,7 +68,6 @@ import com.android.documentsui.dirlist.FragmentTuner; import com.android.documentsui.dirlist.Model; import com.android.documentsui.dirlist.MultiSelectManager; import com.android.documentsui.dirlist.MultiSelectManager.Selection; -import com.android.documentsui.roots.LoadRootTask; import com.android.documentsui.roots.RootsCache; import com.android.documentsui.sidebar.RootsFragment; import com.android.documentsui.sorting.SortController; @@ -182,7 +180,6 @@ public abstract class BaseActivity getContentResolver().registerContentObserver( RootsCache.sNotificationUri, false, mRootsCacheObserver); - DocumentsToolbar toolbar = (DocumentsToolbar) findViewById(R.id.toolbar); setActionBar(toolbar); @@ -418,11 +415,6 @@ public abstract class BaseActivity invalidateOptionsMenu(); } - protected final void loadRoot(final Uri uri) { - new LoadRootTask(this, mRoots, mState, uri).executeOnExecutor( - ProviderExecutor.forAuthority(uri.getAuthority())); - } - /** * This is called when user hovers over a doc for enough time during a drag n' drop, to open a * folder that accepts drop. We should only open a container that's not an archive. @@ -442,7 +434,7 @@ public abstract class BaseActivity List<String> authorities = new ArrayList<>(); if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) { // Exclude roots provided by the calling package. - String packageName = getCallingPackageMaybeExtra(); + String packageName = Shared.getCallingPackageName(this); try { PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName, PackageManager.GET_PROVIDERS); @@ -461,22 +453,6 @@ public abstract class BaseActivity return (root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0; } - public final String getCallingPackageMaybeExtra() { - String callingPackage = getCallingPackage(); - // System apps can set the calling package name using an extra. - try { - ApplicationInfo info = getPackageManager().getApplicationInfo(callingPackage, 0); - if (info.isSystemApp() || info.isUpdatedSystemApp()) { - final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME); - if (extra != null) { - callingPackage = extra; - } - } - } finally { - return callingPackage; - } - } - public static BaseActivity get(Fragment fragment) { return (BaseActivity) fragment.getActivity(); } @@ -485,17 +461,6 @@ public abstract class BaseActivity return mState; } - /* - * Get the default directory to be presented after starting the activity. - * Method can be overridden if the change of the behavior of the the child activity is needed. - */ - public Uri getDefaultRoot() { - return Shared.shouldShowDocumentsRoot(this, getIntent()) - ? DocumentsContract.buildHomeUri() - : DocumentsContract.buildRootUri( - "com.android.providers.downloads.documents", "downloads"); - } - /** * Set internal storage visible based on explicit user action. */ diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java index b18290569..3f1d48b76 100644 --- a/src/com/android/documentsui/DocumentsApplication.java +++ b/src/com/android/documentsui/DocumentsApplication.java @@ -28,9 +28,9 @@ import android.net.Uri; import android.os.RemoteException; import android.text.format.DateUtils; +import com.android.documentsui.clipping.ClipStorage; import com.android.documentsui.clipping.ClipStore; import com.android.documentsui.clipping.DocumentClipper; -import com.android.documentsui.clipping.ClipStorage; import com.android.documentsui.roots.RootsCache; public class DocumentsApplication extends Application { diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java index 6a9d63336..a95bf72da 100644 --- a/src/com/android/documentsui/NavigationViewManager.java +++ b/src/com/android/documentsui/NavigationViewManager.java @@ -132,6 +132,7 @@ public class NavigationViewManager { interface Environment { RootInfo getCurrentRoot(); String getDrawerTitle(); + @Deprecated // Use CommonAddones#refreshCurrentRootAndDirectory void refreshCurrentRootAndDirectory(int animation); boolean isSearchExpanded(); } diff --git a/src/com/android/documentsui/RecentsLoader.java b/src/com/android/documentsui/RecentsLoader.java index 02c01ee78..dc4d9f2fe 100644 --- a/src/com/android/documentsui/RecentsLoader.java +++ b/src/com/android/documentsui/RecentsLoader.java @@ -37,7 +37,7 @@ import com.android.documentsui.base.FilteringCursorWrapper; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.roots.RootCursorWrapper; -import com.android.documentsui.roots.RootsCache; +import com.android.documentsui.roots.RootsAccess; import com.android.internal.annotations.GuardedBy; import com.google.common.util.concurrent.AbstractFuture; @@ -80,7 +80,7 @@ public class RecentsLoader extends AsyncTaskLoader<DirectoryResult> { private final Semaphore mQueryPermits; - private final RootsCache mRoots; + private final RootsAccess mRoots; private final State mState; @GuardedBy("mTasks") @@ -92,7 +92,7 @@ public class RecentsLoader extends AsyncTaskLoader<DirectoryResult> { private DirectoryResult mResult; - public RecentsLoader(Context context, RootsCache roots, State state) { + public RecentsLoader(Context context, RootsAccess roots, State state) { super(context); mRoots = roots; mState = state; diff --git a/src/com/android/documentsui/base/Lookup.java b/src/com/android/documentsui/base/Lookup.java new file mode 100644 index 000000000..7057bfc11 --- /dev/null +++ b/src/com/android/documentsui/base/Lookup.java @@ -0,0 +1,28 @@ +/* + * 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.base; + +import java.util.function.Function; + +import javax.annotation.Nullable; + +/** + * A {@link Function}-like interface for looking up information. + */ +@FunctionalInterface +public interface Lookup<T, R> { + @Nullable R lookup(T key); +} diff --git a/src/com/android/documentsui/base/Shared.java b/src/com/android/documentsui/base/Shared.java index ebff4fb8b..154b397f1 100644 --- a/src/com/android/documentsui/base/Shared.java +++ b/src/com/android/documentsui/base/Shared.java @@ -20,7 +20,9 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.res.Configuration; +import android.net.Uri; import android.os.Looper; import android.provider.DocumentsContract; import android.text.TextUtils; @@ -163,7 +165,7 @@ public final class Shared { public static <T> ArrayList<T> asArrayList(List<T> list) { return list instanceof ArrayList ? (ArrayList<T>) list - : new ArrayList<T>(list); + : new ArrayList<>(list); } /** @@ -182,6 +184,40 @@ public final class Shared { return sCollator.compare(lhs, rhs); } + /** + * Returns the calling package, possibly overridden by EXTRA_PACKAGE_NAME. + * @param activity + * @return + */ + public static String getCallingPackageName(Activity activity) { + String callingPackage = activity.getCallingPackage(); + // System apps can set the calling package name using an extra. + try { + ApplicationInfo info = + activity.getPackageManager().getApplicationInfo(callingPackage, 0); + if (info.isSystemApp() || info.isUpdatedSystemApp()) { + final String extra = activity.getIntent().getStringExtra( + DocumentsContract.EXTRA_PACKAGE_NAME); + if (extra != null) { + callingPackage = extra; + } + } + } finally { + return callingPackage; + } + } + + /** + * Returns the default directory to be presented after starting the activity. + * Method can be overridden if the change of the behavior of the the child activity is needed. + */ + public static Uri getDefaultRootUri(Activity activity) { + return shouldShowDocumentsRoot(activity, activity.getIntent()) + ? DocumentsContract.buildHomeUri() + : DocumentsContract.buildRootUri( + "com.android.providers.downloads.documents", "downloads"); + } + public static boolean isHardwareKeyboardAvailable(Context context) { return context.getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS; } diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java index 234b86ddd..6e8682be6 100644 --- a/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -87,9 +87,10 @@ import com.android.documentsui.base.State.ViewMode; import com.android.documentsui.clipping.ClipStore; import com.android.documentsui.clipping.DocumentClipper; import com.android.documentsui.clipping.UrisSupplier; +import com.android.documentsui.dirlist.AnimationView.AnimationType; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.picker.PickActivity; -import com.android.documentsui.roots.RootsCache; +import com.android.documentsui.roots.RootsAccess; import com.android.documentsui.services.FileOperation; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperationService.OpType; @@ -1229,7 +1230,7 @@ public class DirectoryFragment extends Fragment RootInfo root, @Nullable DocumentInfo doc, String query, - int anim) { + @AnimationType int anim) { if (DEBUG) { if (doc == null) { @@ -1306,7 +1307,7 @@ public class DirectoryFragment extends Fragment mConfig.mSearchMode); case TYPE_RECENT_OPEN: if (DEBUG) Log.d(TAG, "Creating new loader recents."); - final RootsCache roots = DocumentsApplication.getRootsCache(context); + final RootsAccess roots = DocumentsApplication.getRootsCache(context); return new RecentsLoader(context, roots, state); default: diff --git a/src/com/android/documentsui/manager/ActionHandler.java b/src/com/android/documentsui/manager/ActionHandler.java index 5cd1a61d0..0041a5efa 100644 --- a/src/com/android/documentsui/manager/ActionHandler.java +++ b/src/com/android/documentsui/manager/ActionHandler.java @@ -16,9 +16,12 @@ package com.android.documentsui.manager; +import static com.android.documentsui.base.Shared.DEBUG; + import android.app.Activity; import android.content.ClipData; import android.content.Intent; +import android.net.Uri; import android.provider.DocumentsContract; import android.util.Log; @@ -31,17 +34,20 @@ 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.Lookup; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.clipping.ClipStore; import com.android.documentsui.clipping.DocumentClipper; import com.android.documentsui.clipping.UrisSupplier; +import com.android.documentsui.dirlist.AnimationView; import com.android.documentsui.dirlist.DocumentDetails; import com.android.documentsui.dirlist.FragmentTuner; import com.android.documentsui.dirlist.Model; import com.android.documentsui.dirlist.MultiSelectManager; import com.android.documentsui.dirlist.MultiSelectManager.Selection; import com.android.documentsui.manager.ActionHandler.Addons; +import com.android.documentsui.roots.RootsAccess; import com.android.documentsui.services.FileOperation; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperations; @@ -49,6 +55,7 @@ import com.android.documentsui.ui.DialogController; import java.io.IOException; import java.util.List; +import java.util.concurrent.Executor; import javax.annotation.Nullable; @@ -60,7 +67,6 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa private static final String TAG = "ManagerActionHandler"; private final DialogController mDialogs; - private final State mState; private final FragmentTuner mTuner; private final DocumentClipper mClipper; private final ClipStore mClipStore; @@ -69,15 +75,17 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa ActionHandler( T activity, - DialogController dialogs, State state, + RootsAccess roots, + Lookup<String, Executor> executors, + DialogController dialogs, FragmentTuner tuner, DocumentClipper clipper, ClipStore clipStore) { - super(activity); + + super(activity, state, roots, executors); mDialogs = dialogs; - mState = state; mTuner = tuner; mClipper = clipper; mClipStore = clipStore; @@ -201,6 +209,87 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa mDialogs.confirmDelete(docs, result); } + @Override + public void initLocation(Intent intent) { + assert(intent != null); + + if (mState.restored) { + if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData()); + return; + } + + if (launchToStackLocation(mState.stack)) { + if (DEBUG) Log.d(TAG, "Launched to location from stack."); + 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; + } + + if (DEBUG) Log.d(TAG, "Launching directly into Home directory."); + loadHomeDir(); + } + + // If a non-empty stack is present in our state, it was read (presumably) + // from EXTRA_STACK intent extra. In this case, we'll skip other means of + // loading or restoring the stack (like URI). + // + // When restoring from a stack, if a URI is present, it should only ever be: + // -- a launch URI: Launch URIs support sensible activity management, + // but don't specify a real content target) + // -- a fake Uri from notifications. These URIs have no authority (TODO: details). + // + // Any other URI is *sorta* unexpected...except when browsing an archive + // in downloads. + private boolean launchToStackLocation(DocumentStack stack) { + if (stack == null || stack.root == null) { + return false; + } + + if (mState.stack.isEmpty()) { + mActivity.onRootPicked(mState.stack.root); + } else { + mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); + } + + 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); + new OpenUriForViewTask<>(mActivity, mState).executeOnExecutor( + ProviderExecutor.forAuthority(uri.getAuthority()), uri); + return true; + } + + return false; + } + + private boolean launchToRoot(Intent intent) { + if (DocumentsContract.ACTION_BROWSE.equals(intent.getAction())) { + Uri uri = intent.getData(); + if (DocumentsContract.isRootUri(mActivity, uri)) { + if (DEBUG) Log.d(TAG, "Launching with root URI."); + // If we've got a specific root to display, restore that root using a dedicated + // authority. That way a misbehaving provider won't result in an ANR. + loadRoot(uri); + return true; + } + } + return false; + } + ActionHandler<T> reset(Model model, MultiSelectManager selectionMgr) { mConfig.reset(model, selectionMgr); return this; diff --git a/src/com/android/documentsui/manager/ManageActivity.java b/src/com/android/documentsui/manager/ManageActivity.java index e9ab04afb..1aea5cf1b 100644 --- a/src/com/android/documentsui/manager/ManageActivity.java +++ b/src/com/android/documentsui/manager/ManageActivity.java @@ -38,32 +38,30 @@ import com.android.documentsui.BaseActivity; import com.android.documentsui.DocumentsApplication; import com.android.documentsui.FocusManager; import com.android.documentsui.MenuManager.DirectoryDetails; +import com.android.documentsui.ProviderExecutor; import com.android.documentsui.OperationDialogFragment; import com.android.documentsui.OperationDialogFragment.DialogType; import com.android.documentsui.ProviderExecutor; import com.android.documentsui.R; 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.base.State; import com.android.documentsui.clipping.DocumentClipper; import com.android.documentsui.dirlist.AnimationView; +import com.android.documentsui.dirlist.AnimationView.AnimationType; import com.android.documentsui.dirlist.DirectoryFragment; import com.android.documentsui.dirlist.FragmentTuner; import com.android.documentsui.dirlist.Model; import com.android.documentsui.dirlist.MultiSelectManager; -import com.android.documentsui.roots.RootsCache; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.sidebar.RootsFragment; import com.android.documentsui.ui.DialogController; import com.android.documentsui.ui.Snackbars; -import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.List; /** @@ -76,7 +74,7 @@ public class ManageActivity extends BaseActivity implements ActionHandler.Addons private Tuner mTuner; private MenuManager mMenuManager; private FocusManager mFocusManager; - private ActionHandler<ManageActivity> mActionHandler; + private ActionHandler<ManageActivity> mActions; private DialogController mDialogs; private DocumentClipper mClipper; @@ -103,10 +101,12 @@ public class ManageActivity extends BaseActivity implements ActionHandler.Addons // Make sure this is done after the RecyclerView and the Model are set up. mFocusManager = new FocusManager(getColor(R.color.accent_dark)); mDialogs = DialogController.create(this); - mActionHandler = new ActionHandler<>( + mActions = new ActionHandler<>( this, - mDialogs, mState, + mRoots, + ProviderExecutor::forAuthority, + mDialogs, mTuner, mClipper, DocumentsApplication.getClipStore(this)); @@ -114,52 +114,12 @@ public class ManageActivity extends BaseActivity implements ActionHandler.Addons RootsFragment.show(getFragmentManager(), null); final Intent intent = getIntent(); - final Uri uri = intent.getData(); - - if (mState.restored) { - if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData()); - } else if (mState.stack.root != null) { - // If a non-empty stack is present in our state, it was read (presumably) - // from EXTRA_STACK intent extra. In this case, we'll skip other means of - // loading or restoring the stack (like URI). - // - // When restoring from a stack, if a URI is present, it should only ever be: - // -- a launch URI: Launch URIs support sensible activity management, - // but don't specify a real content target) - // -- a fake Uri from notifications. These URIs have no authority (TODO: details). - // - // Any other URI is *sorta* unexpected...except when browsing an archive - // in downloads. - if (DEBUG) { - if (uri != null - && uri.getAuthority() != null - && !uri.equals(mState.stack.peek()) - && !LauncherActivity.isLaunchUri(uri)) { - Log.w(TAG, "Launching with non-empty stack. Ignoring unexpected uri: " + uri); - } else { - Log.d(TAG, "Launching with non-empty stack."); - } - } - if (!mState.stack.isEmpty()) { - refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); - } else { - onRootPicked(mState.stack.root); - } - } else if (Intent.ACTION_VIEW.equals(intent.getAction())) { - assert(uri != null); - new OpenUriForViewTask(this).executeOnExecutor( - ProviderExecutor.forAuthority(uri.getAuthority()), uri); - } else if (DocumentsContract.isRootUri(this, uri)) { - if (DEBUG) Log.d(TAG, "Launching with root URI."); - // If we've got a specific root to display, restore that root using a dedicated - // authority. That way a misbehaving provider won't result in an ANR. - loadRoot(uri); - } else { - if (DEBUG) Log.d(TAG, "All other means skipped. Launching into default directory."); - loadRoot(getDefaultRoot()); - } + mActions.initLocation(intent); + presentFileErrors(icicle, intent); + } + private void presentFileErrors(Bundle icicle, final Intent intent) { final @DialogType int dialogType = intent.getIntExtra( FileOperationService.EXTRA_DIALOG_TYPE, DIALOG_TYPE_UNKNOWN); // DialogFragment takes care of restoring the dialog on configuration change. @@ -249,7 +209,7 @@ public class ManageActivity extends BaseActivity implements ActionHandler.Addons showCreateDirectoryDialog(); break; case R.id.menu_new_window: - mActionHandler.openInNewWindow(mState.stack); + mActions.openInNewWindow(mState.stack); break; case R.id.menu_paste_from_clipboard: DirectoryFragment dir = getDirectoryFragment(); @@ -258,7 +218,7 @@ public class ManageActivity extends BaseActivity implements ActionHandler.Addons } break; case R.id.menu_settings: - mActionHandler.openSettings(getCurrentRoot()); + mActions.openSettings(getCurrentRoot()); break; default: return super.onOptionsItemSelected(item); @@ -267,7 +227,7 @@ public class ManageActivity extends BaseActivity implements ActionHandler.Addons } @Override - public void refreshDirectory(int anim) { + public void refreshDirectory(@AnimationType int anim) { final FragmentManager fm = getFragmentManager(); final RootInfo root = getCurrentRoot(); final DocumentInfo cwd = getCurrentDirectory(); @@ -534,57 +494,13 @@ public class ManageActivity extends BaseActivity implements ActionHandler.Addons if (model == null || selectionMgr == null) { assert(model == null); assert(selectionMgr == null); - return mActionHandler; + return mActions; } - return mActionHandler.reset(model, selectionMgr); + return mActions.reset(model, selectionMgr); } @Override public DialogController getDialogController() { return mDialogs; } - - /** - * 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. - */ - private static final class OpenUriForViewTask extends PairedTask<ManageActivity, Uri, Void> { - - private final State mState; - public OpenUriForViewTask(ManageActivity activity) { - super(activity); - mState = activity.mState; - } - - @Override - public Void run(Uri... params) { - final Uri uri = params[0]; - - final RootsCache rootsCache = DocumentsApplication.getRootsCache(mOwner); - final String authority = uri.getAuthority(); - - final Collection<RootInfo> roots = - rootsCache.getRootsForAuthorityBlocking(authority); - if (roots.isEmpty()) { - Log.e(TAG, "Failed to find root for the requested Uri: " + uri); - return null; - } - - final RootInfo root = roots.iterator().next(); - mState.stack.root = root; - try { - mState.stack.add(DocumentInfo.fromUri(mOwner.getContentResolver(), uri)); - } catch (FileNotFoundException e) { - Log.e(TAG, "Failed to resolve DocumentInfo from Uri: " + uri); - } - mState.stack.add(root.getRootDocumentBlocking(mOwner)); - return null; - } - - @Override - public void finish(Void result) { - mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); - } - } } diff --git a/src/com/android/documentsui/manager/OpenUriForViewTask.java b/src/com/android/documentsui/manager/OpenUriForViewTask.java new file mode 100644 index 000000000..6fa02f875 --- /dev/null +++ b/src/com/android/documentsui/manager/OpenUriForViewTask.java @@ -0,0 +1,78 @@ +/* + * 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.manager; + +import android.app.Activity; +import android.net.Uri; +import android.util.Log; + +import com.android.documentsui.AbstractActionHandler.CommonAddons; +import com.android.documentsui.DocumentsApplication; +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.io.FileNotFoundException; +import java.util.Collection; + +/** + * Builds a stack for the specific Uris. Multi roots are not supported, as it's impossible + * to know which root to select. Also, the stack doesn't contain intermediate directories. + * It's primarly used for opening ZIP archives from Downloads app. + */ +final class OpenUriForViewTask<T extends Activity & CommonAddons> + extends PairedTask<T, Uri, Void> { + + private final State mState; + public OpenUriForViewTask(T activity, State state) { + super(activity); + mState = state; + } + + @Override + public Void run(Uri... params) { + final Uri uri = params[0]; + + final RootsAccess rootsCache = DocumentsApplication.getRootsCache(mOwner); + final String authority = uri.getAuthority(); + + final Collection<RootInfo> roots = + rootsCache.getRootsForAuthorityBlocking(authority); + if (roots.isEmpty()) { + Log.e(ManageActivity.TAG, "Failed to find root for the requested Uri: " + uri); + return null; + } + + final RootInfo root = roots.iterator().next(); + mState.stack.root = root; + mState.stack.add(root.getRootDocumentBlocking(mOwner)); + try { + mState.stack.add(DocumentInfo.fromUri(mOwner.getContentResolver(), uri)); + } catch (FileNotFoundException e) { + Log.e(ManageActivity.TAG, "Failed to resolve DocumentInfo from Uri: " + uri); + } + + return null; + } + + @Override + public void finish(Void result) { + mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); + } +}
\ No newline at end of file diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java index 0b9744ada..59d0b4861 100644 --- a/src/com/android/documentsui/picker/ActionHandler.java +++ b/src/com/android/documentsui/picker/ActionHandler.java @@ -16,6 +16,9 @@ package com.android.documentsui.picker; +import static com.android.documentsui.base.Shared.DEBUG; +import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION; + import android.app.Activity; import android.content.Intent; import android.content.pm.ResolveInfo; @@ -27,12 +30,17 @@ import com.android.documentsui.AbstractActionHandler; import com.android.documentsui.Metrics; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; +import com.android.documentsui.base.Lookup; import com.android.documentsui.base.RootInfo; +import com.android.documentsui.base.State; import com.android.documentsui.dirlist.DocumentDetails; import com.android.documentsui.dirlist.FragmentTuner; import com.android.documentsui.dirlist.Model; import com.android.documentsui.dirlist.MultiSelectManager; import com.android.documentsui.picker.ActionHandler.Addons; +import com.android.documentsui.roots.RootsAccess; + +import java.util.concurrent.Executor; import javax.annotation.Nullable; @@ -46,13 +54,44 @@ class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T private final FragmentTuner mTuner; private final Config mConfig; - ActionHandler(T activity, FragmentTuner tuner) { - super(activity); + ActionHandler( + T activity, + State state, + RootsAccess roots, + Lookup<String, Executor> executors, + FragmentTuner tuner) { + + super(activity, state, roots, executors); + mTuner = tuner; mConfig = new Config(); } @Override + public void initLocation(Intent intent) { + if (mState.restored) { + if (DEBUG) Log.d(TAG, "Stack already resolved"); + } else { + // We set the activity title in AsyncTask.onPostExecute(). + // To prevent talkback from reading aloud the default title, we clear it here. + mActivity.setTitle(""); + + // As a matter of policy we don't load the last used stack for the copy + // destination picker (user is already in Files app). + // Concensus was that the experice was too confusing. + // In all other cases, where the user is visiting us from another app + // we restore the stack as last used from that app. + if (mState.action == ACTION_PICK_COPY_DESTINATION) { + if (DEBUG) Log.d(TAG, "Launching directly into Home directory."); + loadHomeDir(); + } else { + 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); intent.setData(Uri.fromParts("package", info.activityInfo.packageName, null)); diff --git a/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java b/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java index a3e693864..3dadca468 100644 --- a/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java +++ b/src/com/android/documentsui/picker/LoadLastAccessedStackTask.java @@ -18,18 +18,20 @@ package com.android.documentsui.picker; import static com.android.documentsui.base.Shared.DEBUG; +import android.app.Activity; import android.database.Cursor; import android.net.Uri; import android.util.Log; -import com.android.documentsui.DocumentsApplication; +import com.android.documentsui.AbstractActionHandler.CommonAddons; import com.android.documentsui.base.DurableUtils; import com.android.documentsui.base.PairedTask; 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.picker.LastAccessedProvider.Columns; -import com.android.documentsui.roots.RootsCache; +import com.android.documentsui.roots.RootsAccess; import libcore.io.IoUtils; @@ -43,17 +45,19 @@ import java.util.Collection; * path for an app like Gmail can be different than the last path * for an app like DropBox. */ -final class LoadLastAccessedStackTask - extends PairedTask<PickActivity, Void, Void> { +final class LoadLastAccessedStackTask<T extends Activity & CommonAddons> + extends PairedTask<T, Void, Void> { private static final String TAG = "LoadLastAccessedStackTask"; private volatile boolean mRestoredStack; private volatile boolean mExternal; private final State mState; + private RootsAccess mRoots; - public LoadLastAccessedStackTask(PickActivity activity, State state) { + public LoadLastAccessedStackTask(T activity, State state, RootsAccess roots) { super(activity); mState = state; + mRoots = roots; } @Override @@ -61,10 +65,9 @@ final class LoadLastAccessedStackTask if (DEBUG && !mState.stack.isEmpty()) { Log.w(TAG, "Overwriting existing stack."); } - RootsCache roots = DocumentsApplication.getRootsCache(mOwner); - - String packageName = mOwner.getCallingPackageMaybeExtra(); - Uri resumeUri = LastAccessedProvider.buildLastAccessed(packageName); + String callingPackage = Shared.getCallingPackageName(mOwner); + Uri resumeUri = LastAccessedProvider.buildLastAccessed( + callingPackage); Cursor cursor = mOwner.getContentResolver().query(resumeUri, null, null, null, null); try { if (cursor.moveToFirst()) { @@ -82,12 +85,12 @@ final class LoadLastAccessedStackTask if (mRestoredStack) { // Update the restored stack to ensure we have freshest data - final Collection<RootInfo> matchingRoots = roots.getMatchingRootsBlocking(mState); + final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState); try { mState.stack.updateRoot(matchingRoots); mState.stack.updateDocuments(mOwner.getContentResolver()); } catch (FileNotFoundException e) { - Log.w(TAG, "Failed to restore stack for package: " + packageName + Log.w(TAG, "Failed to restore stack for package: " + callingPackage + " because of error: "+ e); mState.stack.reset(); mRestoredStack = false; diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java index 7b03a4d2a..88837e9bc 100644 --- a/src/com/android/documentsui/picker/PickActivity.java +++ b/src/com/android/documentsui/picker/PickActivity.java @@ -46,6 +46,7 @@ import com.android.documentsui.BaseActivity; import com.android.documentsui.DocumentsApplication; import com.android.documentsui.FocusManager; import com.android.documentsui.MenuManager.DirectoryDetails; +import com.android.documentsui.ProviderExecutor; import com.android.documentsui.R; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.MimePredicate; @@ -86,11 +87,23 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { mTuner = new Tuner(this, mState); mFocusManager = new FocusManager(getColor(R.color.accent_dark)); mMenuManager = new MenuManager(mSearchManager, mState, new DirectoryDetails(this)); - mActionHandler = new ActionHandler<>(this, mTuner); + mActionHandler = new ActionHandler<>( + this, + mState, + DocumentsApplication.getRootsCache(this), + ProviderExecutor::forAuthority, + mTuner); + Intent intent = getIntent(); + + setupLayout(intent); + mActionHandler.initLocation(intent); + } + + private void setupLayout(Intent intent) { if (mState.action == ACTION_CREATE) { - final String mimeType = getIntent().getType(); - final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE); + final String mimeType = intent.getType(); + final String title = intent.getStringExtra(Intent.EXTRA_TITLE); SaveFragment.show(getFragmentManager(), mimeType, title); } else if (mState.action == ACTION_OPEN_TREE || mState.action == ACTION_PICK_COPY_DESTINATION) { @@ -98,7 +111,7 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { } if (mState.action == ACTION_GET_CONTENT) { - final Intent moreApps = new Intent(getIntent()); + final Intent moreApps = new Intent(intent); moreApps.setComponent(null); moreApps.setPackage(null); RootsFragment.show(getFragmentManager(), moreApps); @@ -108,27 +121,6 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { mState.action == ACTION_PICK_COPY_DESTINATION) { RootsFragment.show(getFragmentManager(), (Intent) null); } - - if (mState.restored) { - if (DEBUG) Log.d(TAG, "Stack already resolved"); - } else { - // We set the activity title in AsyncTask.onPostExecute(). - // To prevent talkback from reading aloud the default title, we clear it here. - setTitle(""); - - // As a matter of policy we don't load the last used stack for the copy - // destination picker (user is already in Files app). - // Concensus was that the experice was too confusing. - // In all other cases, where the user is visiting us from another app - // we restore the stack as last used from that app. - if (mState.action == ACTION_PICK_COPY_DESTINATION) { - if (DEBUG) Log.d(TAG, "Launching directly into Home directory."); - loadRoot(getDefaultRoot()); - } else { - if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package."); - new LoadLastAccessedStackTask(this, mState).execute(); - } - } } @Override @@ -187,7 +179,7 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) { // Remember that we last picked via external app - final String packageName = getCallingPackageMaybeExtra(); + final String packageName = Shared.getCallingPackageName(this); final ContentValues values = new ContentValues(); values.put(Columns.EXTERNAL, 1); getContentResolver().insert(LastAccessedProvider.buildLastAccessed(packageName), values); @@ -252,7 +244,7 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { // No directory means recents if (mState.action == ACTION_CREATE || mState.action == ACTION_PICK_COPY_DESTINATION) { - loadRoot(getDefaultRoot()); + mActionHandler.loadRoot(Shared.getDefaultRootUri(this)); } else { DirectoryFragment.showRecentsOpen(fm, anim); @@ -354,7 +346,7 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { void updateLastAccessed() { LastAccessedProvider.setLastAccessed( - getContentResolver(), getCallingPackageMaybeExtra(), mState.stack); + getContentResolver(), Shared.getCallingPackageName(this), mState.stack); } @Override diff --git a/src/com/android/documentsui/roots/LoadRootTask.java b/src/com/android/documentsui/roots/LoadRootTask.java index aaf9d83c7..7bc6d0a6e 100644 --- a/src/com/android/documentsui/roots/LoadRootTask.java +++ b/src/com/android/documentsui/roots/LoadRootTask.java @@ -16,24 +16,28 @@ package com.android.documentsui.roots; +import static com.android.documentsui.base.Shared.DEBUG; + +import android.app.Activity; import android.net.Uri; import android.provider.DocumentsContract; import android.util.Log; -import com.android.documentsui.BaseActivity; +import com.android.documentsui.AbstractActionHandler.CommonAddons; import com.android.documentsui.base.PairedTask; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; -public final class LoadRootTask extends PairedTask<BaseActivity, Void, RootInfo> { +public final class LoadRootTask<T extends Activity & CommonAddons> + extends PairedTask<T, Void, RootInfo> { private static final String TAG = "LoadRootTask"; private final State mState; - private final RootsCache mRoots; + private final RootsAccess mRoots; private final Uri mRootUri; - public LoadRootTask(BaseActivity activity, RootsCache roots, State state, Uri rootUri) { + public LoadRootTask(T activity, RootsAccess roots, State state, Uri rootUri) { super(activity); mState = state; mRoots = roots; @@ -42,6 +46,8 @@ public final class LoadRootTask extends PairedTask<BaseActivity, Void, RootInfo> @Override protected RootInfo run(Void... params) { + if (DEBUG) Log.d(TAG, "Loading root: " + mRootUri); + String rootId = DocumentsContract.getRootId(mRootUri); return mRoots.getRootOneshot(mRootUri.getAuthority(), rootId); } @@ -51,6 +57,7 @@ public final class LoadRootTask extends PairedTask<BaseActivity, Void, RootInfo> mState.restored = true; if (root != null) { + if (DEBUG) Log.d(TAG, "Loaded root: " + root); mOwner.onRootPicked(root); } else { Log.w(TAG, "Failed to find root: " + mRootUri); diff --git a/src/com/android/documentsui/roots/RootsAccess.java b/src/com/android/documentsui/roots/RootsAccess.java new file mode 100644 index 000000000..ee5e52284 --- /dev/null +++ b/src/com/android/documentsui/roots/RootsAccess.java @@ -0,0 +1,123 @@ +/* + * 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.roots; + +import static com.android.documentsui.base.Shared.DEBUG; + +import android.util.Log; + +import com.android.documentsui.base.MimePredicate; +import com.android.documentsui.base.RootInfo; +import com.android.documentsui.base.State; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Provides testable access to key {@link RootsCache} methods. + */ +public interface RootsAccess { + + /** + * Return the requested {@link RootInfo}, but only loading the roots for the + * requested authority. This is useful when we want to load fast without + * waiting for all the other roots to come back. + */ + RootInfo getRootOneshot(String authority, String rootId); + + Collection<RootInfo> getMatchingRootsBlocking(State state); + + /** + * Returns a list of roots for the specified authority. If not found, then + * an empty list is returned. + */ + Collection<RootInfo> getRootsForAuthorityBlocking(String authority); + + public static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) { + + final String tag = "RootsAccess"; + + final List<RootInfo> matching = new ArrayList<>(); + for (RootInfo root : roots) { + + if (state.action == State.ACTION_CREATE && !root.supportsCreate()) { + if (DEBUG) Log.v(tag, "Excluding read-only root because: ACTION_CREATE."); + continue; + } + + if (state.action == State.ACTION_PICK_COPY_DESTINATION + && !root.supportsCreate()) { + if (DEBUG) Log.v( + tag, "Excluding read-only root because: ACTION_PICK_COPY_DESTINATION."); + continue; + } + + if (state.action == State.ACTION_OPEN_TREE && !root.supportsChildren()) { + if (DEBUG) Log.v( + tag, "Excluding root !supportsChildren because: ACTION_OPEN_TREE."); + continue; + } + + if (!state.showAdvanced && root.isAdvanced()) { + if (DEBUG) Log.v(tag, "Excluding root because: unwanted advanced device."); + continue; + } + + if (state.localOnly && !root.isLocalOnly()) { + if (DEBUG) Log.v(tag, "Excluding root because: unwanted non-local device."); + continue; + } + + if (state.directoryCopy && root.isDownloads()) { + if (DEBUG) Log.v( + tag, "Excluding downloads root because: unsupported directory copy."); + continue; + } + + if (state.action == State.ACTION_OPEN && root.isEmpty()) { + if (DEBUG) Log.v(tag, "Excluding empty root because: ACTION_OPEN."); + continue; + } + + if (state.action == State.ACTION_GET_CONTENT && root.isEmpty()) { + if (DEBUG) Log.v(tag, "Excluding empty root because: ACTION_GET_CONTENT."); + continue; + } + + final boolean overlap = + MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) || + MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes); + if (!overlap) { + if (DEBUG) Log.v( + tag, "Excluding root because: unsupported content types > " + + state.acceptMimes); + continue; + } + + if (state.excludedAuthorities.contains(root.authority)) { + if (DEBUG) Log.v(tag, "Excluding root because: owned by calling package."); + continue; + } + + matching.add(root); + } + + if (DEBUG) Log.d(tag, "Matched roots: " + matching); + return matching; + } +} diff --git a/src/com/android/documentsui/roots/RootsCache.java b/src/com/android/documentsui/roots/RootsCache.java index 4fa9cb080..2fda191ff 100644 --- a/src/com/android/documentsui/roots/RootsCache.java +++ b/src/com/android/documentsui/roots/RootsCache.java @@ -16,10 +16,9 @@ package com.android.documentsui.roots; -import android.content.BroadcastReceiver.PendingResult; - import static com.android.documentsui.base.Shared.DEBUG; +import android.content.BroadcastReceiver.PendingResult; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; @@ -37,24 +36,19 @@ import android.os.Handler; import android.os.SystemClock; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Root; -import android.provider.DocumentsProvider; -import android.support.annotation.VisibleForTesting; import android.util.Log; import com.android.documentsui.DocumentsApplication; import com.android.documentsui.R; -import com.android.documentsui.R.drawable; -import com.android.documentsui.R.string; -import com.android.documentsui.base.MimePredicate; import com.android.documentsui.base.RootInfo; 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; @@ -67,7 +61,7 @@ import java.util.concurrent.TimeUnit; /** * Cache of known storage backends and their roots. */ -public class RootsCache { +public class RootsCache implements RootsAccess { public static final Uri sNotificationUri = Uri.parse( "content://com.android.documentsui.roots/"); @@ -125,9 +119,6 @@ public class RootsCache { } } - /** - * Gather roots from all known storage providers. - */ public void updateAsync(boolean forceRefreshAll) { // NOTE: This method is called when the UI language changes. @@ -147,16 +138,10 @@ public class RootsCache { .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - /** - * Gather roots from storage providers belonging to given package name. - */ public void updatePackageAsync(String packageName) { new UpdateTask(false, packageName).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } - /** - * Gather roots from storage providers belonging to given authority. - */ public void updateAuthorityAsync(String authority) { final ProviderInfo info = mContext.getPackageManager().resolveContentProvider(authority, 0); if (info != null) { @@ -164,7 +149,7 @@ public class RootsCache { } } - public void setBootCompletedResult(PendingResult result) { + void setBootCompletedResult(PendingResult result) { synchronized (mLock) { // Quickly check if we've already finished loading, otherwise hang // out until first pass is finished. @@ -224,75 +209,6 @@ public class RootsCache { } } - private class UpdateTask extends AsyncTask<Void, Void, Void> { - private final boolean mForceRefreshAll; - private final String mForceRefreshPackage; - - private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create(); - private final HashSet<String> mTaskStoppedAuthorities = new HashSet<>(); - - /** - * Create task to update roots cache. - * - * @param forceRefreshAll when true, all previously cached values for - * all packages should be ignored. - * @param forceRefreshPackage when non-null, all previously cached - * values for this specific package should be ignored. - */ - public UpdateTask(boolean forceRefreshAll, String forceRefreshPackage) { - mForceRefreshAll = forceRefreshAll; - mForceRefreshPackage = forceRefreshPackage; - } - - @Override - protected Void doInBackground(Void... params) { - final long start = SystemClock.elapsedRealtime(); - - mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot); - - final ContentResolver resolver = mContext.getContentResolver(); - final PackageManager pm = mContext.getPackageManager(); - - // Pick up provider with action string - final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); - final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0); - for (ResolveInfo info : providers) { - handleDocumentsProvider(info.providerInfo); - } - - final long delta = SystemClock.elapsedRealtime() - start; - if (DEBUG) Log.v(TAG, - "Update found " + mTaskRoots.size() + " roots in " + delta + "ms"); - synchronized (mLock) { - mFirstLoadDone = true; - if (mBootCompletedResult != null) { - mBootCompletedResult.finish(); - mBootCompletedResult = null; - } - mRoots = mTaskRoots; - mStoppedAuthorities = mTaskStoppedAuthorities; - } - mFirstLoad.countDown(); - resolver.notifyChange(sNotificationUri, null, false); - return null; - } - - private void handleDocumentsProvider(ProviderInfo info) { - // Ignore stopped packages for now; we might query them - // later during UI interaction. - if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) { - if (DEBUG) Log.v(TAG, "Ignoring stopped authority " + info.authority); - mTaskStoppedAuthorities.add(info.authority); - return; - } - - final boolean forceRefresh = mForceRefreshAll - || Objects.equals(info.packageName, mForceRefreshPackage); - mTaskRoots.putAll(info.authority, loadRootsForAuthority(mContext.getContentResolver(), - info.authority, forceRefresh)); - } - } - /** * Bring up requested provider and query for all active roots. */ @@ -346,20 +262,14 @@ public class RootsCache { return 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 - * waiting for all the other roots to come back. + /* (non-Javadoc) + * @see com.android.documentsui.roots.RootsCache#getRootOneshot(java.lang.String, java.lang.String) */ + @Override public RootInfo getRootOneshot(String authority, String rootId) { return getRootOneshot(authority, rootId, false); } - /** - * Return the requested {@link RootInfo}, but only loading the roots of the requested authority. - * It always fetches from {@link DocumentsProvider} if forceRefresh is true, which is used to - * get the most up-to-date free space before starting copy operations. - */ public RootInfo getRootOneshot(String authority, String rootId, boolean forceRefresh) { synchronized (mLock) { RootInfo root = forceRefresh ? null : getRootLocked(authority, rootId); @@ -389,24 +299,6 @@ public class RootsCache { return null; } - public boolean isIconUniqueBlocking(RootInfo root) { - waitForFirstLoad(); - loadStoppedAuthorities(); - synchronized (mLock) { - final int rootIcon = root.derivedIcon != 0 ? root.derivedIcon : root.icon; - for (RootInfo test : mRoots.get(root.authority)) { - if (Objects.equals(test.rootId, root.rootId)) { - continue; - } - final int testIcon = test.derivedIcon != 0 ? test.derivedIcon : test.icon; - if (testIcon == rootIcon) { - return false; - } - } - return true; - } - } - public RootInfo getRecentsRoot() { return mRecentsRoot; } @@ -423,18 +315,16 @@ public class RootsCache { } } + @Override public Collection<RootInfo> getMatchingRootsBlocking(State state) { waitForFirstLoad(); loadStoppedAuthorities(); synchronized (mLock) { - return getMatchingRoots(mRoots.values(), state); + return RootsAccess.getMatchingRoots(mRoots.values(), state); } } - /** - * Returns a list of roots for the specified authority. If not found, then - * an empty list is returned. - */ + @Override public Collection<RootInfo> getRootsForAuthorityBlocking(String authority) { waitForFirstLoad(); loadStoppedAuthority(authority); @@ -444,11 +334,8 @@ public class RootsCache { } } - /** - * Returns the default root for the specified state. - */ public RootInfo getDefaultRootBlocking(State state) { - for (RootInfo root : getMatchingRoots(getRootsBlocking(), state)) { + for (RootInfo root : RootsAccess.getMatchingRoots(getRootsBlocking(), state)) { if (root.isDownloads()) { return root; } @@ -456,74 +343,72 @@ public class RootsCache { return mRecentsRoot; } - @VisibleForTesting - static List<RootInfo> getMatchingRoots(Collection<RootInfo> roots, State state) { - final List<RootInfo> matching = new ArrayList<>(); - for (RootInfo root : roots) { - - if (state.action == State.ACTION_CREATE && !root.supportsCreate()) { - if (DEBUG) Log.v(TAG, "Excluding read-only root because: ACTION_CREATE."); - continue; - } - - if (state.action == State.ACTION_PICK_COPY_DESTINATION - && !root.supportsCreate()) { - if (DEBUG) Log.v( - TAG, "Excluding read-only root because: ACTION_PICK_COPY_DESTINATION."); - continue; - } + private class UpdateTask extends AsyncTask<Void, Void, Void> { + private final boolean mForceRefreshAll; + private final String mForceRefreshPackage; - if (state.action == State.ACTION_OPEN_TREE && !root.supportsChildren()) { - if (DEBUG) Log.v( - TAG, "Excluding root !supportsChildren because: ACTION_OPEN_TREE."); - continue; - } + private final Multimap<String, RootInfo> mTaskRoots = ArrayListMultimap.create(); + private final HashSet<String> mTaskStoppedAuthorities = new HashSet<>(); - if (!state.showAdvanced && root.isAdvanced()) { - if (DEBUG) Log.v(TAG, "Excluding root because: unwanted advanced device."); - continue; - } + /** + * Create task to update roots cache. + * + * @param forceRefreshAll when true, all previously cached values for + * all packages should be ignored. + * @param forceRefreshPackage when non-null, all previously cached + * values for this specific package should be ignored. + */ + public UpdateTask(boolean forceRefreshAll, String forceRefreshPackage) { + mForceRefreshAll = forceRefreshAll; + mForceRefreshPackage = forceRefreshPackage; + } - if (state.localOnly && !root.isLocalOnly()) { - if (DEBUG) Log.v(TAG, "Excluding root because: unwanted non-local device."); - continue; - } + @Override + protected Void doInBackground(Void... params) { + final long start = SystemClock.elapsedRealtime(); - if (state.directoryCopy && root.isDownloads()) { - if (DEBUG) Log.v( - TAG, "Excluding downloads root because: unsupported directory copy."); - continue; - } + mTaskRoots.put(mRecentsRoot.authority, mRecentsRoot); - if (state.action == State.ACTION_OPEN && root.isEmpty()) { - if (DEBUG) Log.v(TAG, "Excluding empty root because: ACTION_OPEN."); - continue; - } + final ContentResolver resolver = mContext.getContentResolver(); + final PackageManager pm = mContext.getPackageManager(); - if (state.action == State.ACTION_GET_CONTENT && root.isEmpty()) { - if (DEBUG) Log.v(TAG, "Excluding empty root because: ACTION_GET_CONTENT."); - continue; + // Pick up provider with action string + final Intent intent = new Intent(DocumentsContract.PROVIDER_INTERFACE); + final List<ResolveInfo> providers = pm.queryIntentContentProviders(intent, 0); + for (ResolveInfo info : providers) { + handleDocumentsProvider(info.providerInfo); } - final boolean overlap = - MimePredicate.mimeMatches(root.derivedMimeTypes, state.acceptMimes) || - MimePredicate.mimeMatches(state.acceptMimes, root.derivedMimeTypes); - if (!overlap) { - if (DEBUG) Log.v( - TAG, "Excluding root because: unsupported content types > " - + state.acceptMimes); - continue; + final long delta = SystemClock.elapsedRealtime() - start; + if (DEBUG) Log.v(TAG, + "Update found " + mTaskRoots.size() + " roots in " + delta + "ms"); + synchronized (mLock) { + mFirstLoadDone = true; + if (mBootCompletedResult != null) { + mBootCompletedResult.finish(); + mBootCompletedResult = null; + } + mRoots = mTaskRoots; + mStoppedAuthorities = mTaskStoppedAuthorities; } + mFirstLoad.countDown(); + resolver.notifyChange(sNotificationUri, null, false); + return null; + } - if (state.excludedAuthorities.contains(root.authority)) { - if (DEBUG) Log.v(TAG, "Excluding root because: owned by calling package."); - continue; + private void handleDocumentsProvider(ProviderInfo info) { + // Ignore stopped packages for now; we might query them + // later during UI interaction. + if ((info.applicationInfo.flags & ApplicationInfo.FLAG_STOPPED) != 0) { + if (DEBUG) Log.v(TAG, "Ignoring stopped authority " + info.authority); + mTaskStoppedAuthorities.add(info.authority); + return; } - matching.add(root); + final boolean forceRefresh = mForceRefreshAll + || Objects.equals(info.packageName, mForceRefreshPackage); + mTaskRoots.putAll(info.authority, loadRootsForAuthority(mContext.getContentResolver(), + info.authority, forceRefresh)); } - - if (DEBUG) Log.d(TAG, "Matched roots: " + matching); - return matching; } } diff --git a/src/com/android/documentsui/roots/RootsLoader.java b/src/com/android/documentsui/roots/RootsLoader.java index 12b107c77..7e55be41a 100644 --- a/src/com/android/documentsui/roots/RootsLoader.java +++ b/src/com/android/documentsui/roots/RootsLoader.java @@ -37,8 +37,8 @@ public class RootsLoader extends AsyncTaskLoader<Collection<RootInfo>> { mRoots = roots; mState = state; - getContext().getContentResolver() - .registerContentObserver(RootsCache.sNotificationUri, false, mObserver); + context.getContentResolver().registerContentObserver( + RootsCache.sNotificationUri, false, mObserver); } @Override diff --git a/src/com/android/documentsui/services/CopyJob.java b/src/com/android/documentsui/services/CopyJob.java index eea740462..1551bade9 100644 --- a/src/com/android/documentsui/services/CopyJob.java +++ b/src/com/android/documentsui/services/CopyJob.java @@ -21,7 +21,6 @@ import static android.provider.DocumentsContract.buildChildDocumentsUri; import static android.provider.DocumentsContract.buildDocumentUri; import static android.provider.DocumentsContract.getDocumentId; import static android.provider.DocumentsContract.isChildDocument; - import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_CONVERTED; import static com.android.documentsui.base.DocumentInfo.getCursorLong; import static com.android.documentsui.base.DocumentInfo.getCursorString; diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java index 7b9c99c2b..c39be7d5e 100644 --- a/src/com/android/documentsui/sidebar/RootsFragment.java +++ b/src/com/android/documentsui/sidebar/RootsFragment.java @@ -29,9 +29,7 @@ import android.content.Intent; import android.content.Loader; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.os.AsyncTask; import android.os.Bundle; -import android.os.Handler; import android.util.Log; import android.view.ContextMenu; import android.view.DragEvent; diff --git a/tests/common/com/android/documentsui/TestActivity.java b/tests/common/com/android/documentsui/TestActivity.java new file mode 100644 index 000000000..54ebf3e5d --- /dev/null +++ b/tests/common/com/android/documentsui/TestActivity.java @@ -0,0 +1,93 @@ +/* + * 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.ComponentName; +import android.content.Intent; +import android.content.res.Resources; + +import com.android.documentsui.AbstractActionHandler.CommonAddons; +import com.android.documentsui.base.RootInfo; +import com.android.documentsui.testing.TestEventListener; +import com.android.documentsui.testing.android.TestResources; + +import org.mockito.Mockito; + +/** + * Abstract to avoid having to implement unnecessary Activity stuff. + * Instances are created using {@link #create()}. + */ +public abstract class TestActivity extends AbstractBase { + + public TestResources resources; + public Intent intent; + + public TestEventListener<Intent> startActivity; + public TestEventListener<Intent> startService; + public TestEventListener<RootInfo> rootPicked; + + public static TestActivity create() { + TestActivity activity = Mockito.mock(TestActivity.class, Mockito.CALLS_REAL_METHODS); + activity.init(); + return activity; + } + + public void init() { + resources = TestResources.create(); + intent = new Intent(); + + startActivity = new TestEventListener<>(); + startService = new TestEventListener<>(); + rootPicked = new TestEventListener<>(); + } + + @Override + public final String getPackageName() { + return "Banarama"; + } + + @Override + public final void startActivity(Intent intent) { + startActivity.accept(intent); + } + + @Override + public final ComponentName startService(Intent intent) { + startService.accept(intent); + return null; + } + + @Override + public final Intent getIntent() { + return intent; + } + + @Override + public final Resources getResources() { + return resources; + } + + @Override + public final void onRootPicked(RootInfo root) { + rootPicked.accept(root); + } +} + +// Trick Mockito into finding our Addons methods correctly. W/o this +// hack, Mockito thinks Addons methods are not implemented. +abstract class AbstractBase extends Activity implements CommonAddons {} diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java index 102222de1..4581a5c8e 100644 --- a/tests/common/com/android/documentsui/testing/TestActionHandler.java +++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java @@ -16,7 +16,10 @@ package com.android.documentsui.testing; +import android.content.Intent; + import com.android.documentsui.AbstractActionHandler; +import com.android.documentsui.TestActivity; import com.android.documentsui.base.RootInfo; import com.android.documentsui.dirlist.DocumentDetails; @@ -27,7 +30,11 @@ public class TestActionHandler extends AbstractActionHandler<TestActivity> { public final TestEventHandler<DocumentDetails> preview = new TestEventHandler<>(); public TestActionHandler() { - super(TestActivity.create()); + this(TestEnv.create()); + } + + public TestActionHandler(TestEnv env) { + super(TestActivity.create(), env.state, env.roots, (String authority) -> null); } @Override @@ -49,4 +56,9 @@ public class TestActionHandler extends AbstractActionHandler<TestActivity> { public void openRoot(RootInfo root) { throw new UnsupportedOperationException(); } + + @Override + public void initLocation(Intent intent) { + throw new UnsupportedOperationException(); + } } diff --git a/tests/common/com/android/documentsui/testing/TestActivity.java b/tests/common/com/android/documentsui/testing/TestActivity.java deleted file mode 100644 index f725a3563..000000000 --- a/tests/common/com/android/documentsui/testing/TestActivity.java +++ /dev/null @@ -1,61 +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.testing; - -import static junit.framework.Assert.assertEquals; - -import android.app.Activity; -import android.content.Intent; - -import com.android.documentsui.AbstractActionHandler.CommonAddons; -import com.android.documentsui.base.RootInfo; - -import org.mockito.Mockito; - -import javax.annotation.Nullable; - -/** - * Abstract to avoid having to implement unnecessary Activity stuff. - * Instances are created using {@link #create()}. - */ -public abstract class TestActivity extends Activity implements CommonAddons { - - private @Nullable Intent mLastStarted; - - public static TestActivity create() { - return Mockito.mock(TestActivity.class); - } - - @Override - public String getPackageName() { - return "TestActivity"; - } - - @Override - public void startActivity(Intent intent) { - mLastStarted = intent; - } - - public void assertStarted(Intent expected) { - assertEquals(expected, mLastStarted); - } - - @Override - public void onRootPicked(RootInfo root) { - throw new UnsupportedOperationException(); - } -} diff --git a/tests/common/com/android/documentsui/testing/TestEnv.java b/tests/common/com/android/documentsui/testing/TestEnv.java new file mode 100644 index 000000000..f898e6b0f --- /dev/null +++ b/tests/common/com/android/documentsui/testing/TestEnv.java @@ -0,0 +1,68 @@ +/* + * 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.os.Handler; +import android.os.Looper; + +import com.android.documentsui.base.State; +import com.android.documentsui.dirlist.TestModel; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; + +public class TestEnv { + + public static final String AUTHORITY = "hullabaloo"; + + public final TestScheduledExecutorService mExecutor; + public final State state = new State(); + public final TestRootsAccess roots = new TestRootsAccess(); + public final TestModel model = new TestModel(AUTHORITY); + + private TestEnv() { + mExecutor = new TestScheduledExecutorService(); + } + + public static TestEnv create() { + TestEnv env = new TestEnv(); + env.reset(); + return env; + } + + public void reset() { + model.update("a", "b", "c", "x", "y", "z"); + state.stack.push(model.getDocument("1")); + } + + public void beforeAsserts() throws Exception { + // We need to wait on all AsyncTasks to finish AND to post results back. + // *** Results are posted on main thread ***, but tests run in their own + // thread. So even with our test executor we still have races. + // + // To work around this issue post our own runnable to the main thread + // which we presume will be the *last* runnable (after any from AsyncTasks) + // and then wait for our runnable to be called. + CountDownLatch latch = new CountDownLatch(1); + mExecutor.runAll(); + new Handler(Looper.getMainLooper()).post(latch::countDown); + latch.await(); + } + + public Executor lookupExecutor(String authority) { + return mExecutor; + } +} diff --git a/tests/common/com/android/documentsui/testing/TestEventHandler.java b/tests/common/com/android/documentsui/testing/TestEventHandler.java index 1322b7a92..7b204705a 100644 --- a/tests/common/com/android/documentsui/testing/TestEventHandler.java +++ b/tests/common/com/android/documentsui/testing/TestEventHandler.java @@ -16,16 +16,48 @@ package com.android.documentsui.testing; +import static junit.framework.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import com.android.documentsui.base.EventHandler; +import javax.annotation.Nullable; + /** * Test {@link EventHandler} that can be used to spy on, control responses from, * and make assertions against values tested. */ -public class TestEventHandler<T> extends TestPredicate<T> implements EventHandler<T> { +public class TestEventHandler<T> implements EventHandler<T> { + + private @Nullable T lastValue; + private boolean nextReturnValue; + private boolean called; @Override public boolean accept(T event) { - return test(event); + called = true; + lastValue = event; + return nextReturnValue; + } + + public void assertLastArgument(@Nullable T expected) { + assertEquals(expected, lastValue); + } + + public void assertCalled() { + assertTrue(called); + } + + public void assertNotCalled() { + assertFalse(called); + } + + public void nextReturn(boolean value) { + nextReturnValue = value; + } + + public @Nullable T getLastValue() { + return lastValue; } } diff --git a/tests/common/com/android/documentsui/testing/TestEventListener.java b/tests/common/com/android/documentsui/testing/TestEventListener.java index 14a131cd4..420e16a2d 100644 --- a/tests/common/com/android/documentsui/testing/TestEventListener.java +++ b/tests/common/com/android/documentsui/testing/TestEventListener.java @@ -16,17 +16,43 @@ package com.android.documentsui.testing; +import static junit.framework.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + import com.android.documentsui.base.EventHandler; import com.android.documentsui.base.EventListener; +import javax.annotation.Nullable; + /** * Test {@link EventHandler} that can be used to spy on, control responses from, * and make assertions against values tested. */ -public class TestEventListener<T> extends TestPredicate<T> implements EventListener<T> { +public class TestEventListener<T> implements EventListener<T> { + + private @Nullable T lastValue; + private boolean called; @Override public void accept(T event) { - test(event); + called = true; + lastValue = event; + } + + public void assertLastArgument(@Nullable T expected) { + assertEquals(expected, lastValue); + } + + public void assertCalled() { + assertTrue(called); + } + + public void assertNotCalled() { + assertFalse(called); + } + + public T getLastValue() { + return lastValue; } } diff --git a/tests/common/com/android/documentsui/testing/TestRootsAccess.java b/tests/common/com/android/documentsui/testing/TestRootsAccess.java new file mode 100644 index 000000000..641753fc3 --- /dev/null +++ b/tests/common/com/android/documentsui/testing/TestRootsAccess.java @@ -0,0 +1,85 @@ +/* + * 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 com.android.documentsui.base.RootInfo; +import com.android.documentsui.base.State; +import com.android.documentsui.roots.RootsAccess; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +public class TestRootsAccess implements RootsAccess { + + public static final RootInfo DOWNLOADS; + public static final RootInfo HOME; + + static { + DOWNLOADS = new RootInfo(); + DOWNLOADS.authority = "com.android.providers.downloads.documents"; + DOWNLOADS.rootId = "downloads"; + + HOME = new RootInfo(); + HOME.authority = "com.android.externalstorage.documents"; + HOME.rootId = "home"; + } + + public final Map<String, Collection<RootInfo>> roots = new HashMap<>(); + private @Nullable RootInfo nextRoot; + + public TestRootsAccess() { + add(DOWNLOADS); + add(HOME); + } + + public void add(RootInfo root) { + if (!roots.containsKey(root.authority)) { + roots.put(root.authority, new ArrayList<>()); + } + roots.get(root.authority).add(root); + } + + @Override + public RootInfo getRootOneshot(String authority, String rootId) { + if (roots.containsKey(authority)) { + for (RootInfo root : roots.get(authority)) { + if (rootId.equals(root.rootId)) { + return root; + } + } + } + return null; + } + + @Override + public Collection<RootInfo> getMatchingRootsBlocking(State state) { + List<RootInfo> allRoots = new ArrayList<>(); + for (String authority : roots.keySet()) { + allRoots.addAll(roots.get(authority)); + } + return RootsAccess.getMatchingRoots(allRoots, state); + } + + @Override + public Collection<RootInfo> getRootsForAuthorityBlocking(String authority) { + return roots.get(authority); + } +} diff --git a/tests/common/com/android/documentsui/testing/android/TestResources.java b/tests/common/com/android/documentsui/testing/android/TestResources.java new file mode 100644 index 000000000..d72aee1fa --- /dev/null +++ b/tests/common/com/android/documentsui/testing/android/TestResources.java @@ -0,0 +1,47 @@ +/* + * 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.android; + +import android.content.res.Resources; +import android.util.SparseBooleanArray; + +import org.mockito.Mockito; + +/** + * Abstract to avoid having to implement unnecessary Activity stuff. + * Instances are created using {@link #create()}. + */ +public abstract class TestResources extends Resources { + + public SparseBooleanArray bools; + + public TestResources() { + super(ClassLoader.getSystemClassLoader()); + } + + public static TestResources create() { + TestResources resources = Mockito.mock( + TestResources.class, Mockito.CALLS_REAL_METHODS); + resources.bools = new SparseBooleanArray(); + return resources; + } + + @Override + public boolean getBoolean(int id) throws NotFoundException { + return bools.get(id); + } +} diff --git a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java index 023c5eb78..0a04e43bb 100644 --- a/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/AbstractActionHandlerTest.java @@ -16,9 +16,11 @@ package com.android.documentsui; +import static org.junit.Assert.assertEquals; + import android.content.Intent; import android.os.Parcelable; -import android.support.test.filters.SmallTest; +import android.support.test.filters.MediumTest; import android.support.test.runner.AndroidJUnit4; import com.android.documentsui.base.DocumentStack; @@ -27,7 +29,7 @@ import com.android.documentsui.base.Shared; import com.android.documentsui.dirlist.DocumentDetails; import com.android.documentsui.manager.LauncherActivity; import com.android.documentsui.testing.Roots; -import com.android.documentsui.testing.TestActivity; +import com.android.documentsui.testing.TestEnv; import org.junit.Before; import org.junit.Test; @@ -37,16 +39,22 @@ import org.junit.runner.RunWith; * A unit test *for* AbstractActionHandler, not an abstract test baseclass. */ @RunWith(AndroidJUnit4.class) -@SmallTest +@MediumTest public class AbstractActionHandlerTest { private TestActivity mActivity; + private TestEnv mEnv; private AbstractActionHandler<TestActivity> mHandler; @Before public void setUp() { mActivity = TestActivity.create(); - mHandler = new AbstractActionHandler<TestActivity>(mActivity) { + mEnv = TestEnv.create(); + mHandler = new AbstractActionHandler<TestActivity>( + mActivity, + mEnv.state, + mEnv.roots, + mEnv::lookupExecutor) { @Override public void openRoot(RootInfo root) { @@ -57,6 +65,11 @@ public class AbstractActionHandlerTest { public boolean openDocument(DocumentDetails doc) { throw new UnsupportedOperationException(); } + + @Override + public void initLocation(Intent intent) { + throw new UnsupportedOperationException(); + } }; } @@ -67,6 +80,7 @@ public class AbstractActionHandlerTest { Intent expected = LauncherActivity.createLaunchIntent(mActivity); expected.putExtra(Shared.EXTRA_STACK, (Parcelable) path); - mActivity.assertStarted(expected); + Intent actual = mActivity.startActivity.getLastValue(); + assertEquals(expected.toString(), actual.toString()); } } diff --git a/tests/unit/com/android/documentsui/manager/ActionHandlerTest.java b/tests/unit/com/android/documentsui/manager/ActionHandlerTest.java index 2f4748eb9..250b4731f 100644 --- a/tests/unit/com/android/documentsui/manager/ActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/manager/ActionHandlerTest.java @@ -16,16 +16,21 @@ package com.android.documentsui.manager; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.net.Uri; import android.support.test.filters.MediumTest; import android.support.test.runner.AndroidJUnit4; -import com.android.documentsui.base.State; +import com.android.documentsui.R; +import com.android.documentsui.base.RootInfo; +import com.android.documentsui.base.Shared; import com.android.documentsui.dirlist.MultiSelectManager.Selection; -import com.android.documentsui.dirlist.TestModel; import com.android.documentsui.testing.TestConfirmationCallback; +import com.android.documentsui.testing.TestEnv; import com.android.documentsui.ui.TestDialogController; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -34,39 +39,32 @@ import org.junit.runner.RunWith; @MediumTest public class ActionHandlerTest { - private static final String AUTHORITY = "voltron"; - + private TestEnv mEnv; private TestActivity mActivity; private TestDialogController mDialogs; - private State mState; - private TestModel mModel; private TestConfirmationCallback mCallback; - private ActionHandler<TestActivity> mHandler; - private Selection mSelection; @Before public void setUp() { + mEnv = TestEnv.create(); mActivity = TestActivity.create(); - Assert.assertNotNull(mActivity); - mState = new State(); - mModel = new TestModel(AUTHORITY); - mCallback = new TestConfirmationCallback(); mDialogs = new TestDialogController(); + mCallback = new TestConfirmationCallback(); mHandler = new ActionHandler<>( mActivity, + mEnv.state, + mEnv.roots, + mEnv::lookupExecutor, mDialogs, - mState, null, // tuner, not currently used. null, // clipper, only used in drag/drop null // clip storage, not utilized unless we venture into *jumbo* clip terratory. ); - mModel.update("a", "b"); mDialogs.confirmNext(); - mState.stack.push(mModel.getDocument("1")); mSelection = new Selection(); mSelection.add("1"); @@ -74,18 +72,35 @@ public class ActionHandlerTest { @Test public void testDeleteDocuments() { - mHandler.deleteDocuments(mModel, mSelection, mCallback); + mHandler.deleteDocuments(mEnv.model, mSelection, mCallback); mDialogs.assertNoFileFailures(); - mActivity.assertSomethingStarted(); + mActivity.startService.assertCalled(); mCallback.assertConfirmed(); } @Test public void testDeleteDocuments_Cancelable() { mDialogs.rejectNext(); - mHandler.deleteDocuments(mModel, mSelection, mCallback); + mHandler.deleteDocuments(mEnv.model, mSelection, mCallback); mDialogs.assertNoFileFailures(); - mActivity.assertNothingStarted(); + mActivity.startService.assertNotCalled(); mCallback.assertRejected(); } + + @Test + public void testInitLocation_DefaultsToHome() throws Exception { + mActivity.resources.bools.put(R.bool.productivity_device, true); + + mHandler.initLocation(mActivity.getIntent()); + assertRootPicked(Shared.getDefaultRootUri(mActivity)); + } + + private void assertRootPicked(Uri expectedUri) throws Exception { + mEnv.beforeAsserts(); + + mActivity.rootPicked.assertCalled(); + RootInfo root = mActivity.rootPicked.getLastValue(); + assertNotNull(root); + assertEquals(expectedUri, root.getUri()); + } } diff --git a/tests/unit/com/android/documentsui/manager/TestActivity.java b/tests/unit/com/android/documentsui/manager/TestActivity.java index d9cb62f48..2e0804be0 100644 --- a/tests/unit/com/android/documentsui/manager/TestActivity.java +++ b/tests/unit/com/android/documentsui/manager/TestActivity.java @@ -16,73 +16,18 @@ package com.android.documentsui.manager; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; - -import android.content.Intent; - -import com.android.documentsui.base.DocumentInfo; -import com.android.documentsui.base.RootInfo; -import com.android.documentsui.dirlist.Model; - import org.mockito.Mockito; -import java.util.List; - -import javax.annotation.Nullable; - -public abstract class TestActivity extends com.android.documentsui.testing.TestActivity - implements ActionHandler.Addons { - - private @Nullable Intent mLastStarted; +public abstract class TestActivity extends AbstractBase { public static TestActivity create() { - return Mockito.mock(TestActivity.class); - } - - @Override - public String getPackageName() { - return "TestActivity"; - } - - @Override - public void startActivity(Intent intent) { - mLastStarted = intent; - } - - public void assertStarted(Intent expected) { - assertEquals(expected, mLastStarted); - } - - public void assertSomethingStarted() { - assertNotNull(mLastStarted); - } - - public void assertNothingStarted() { - assertNull(mLastStarted); - } - - @Override - public void onRootPicked(RootInfo root) { - throw new UnsupportedOperationException(); - } - - @Override - public void onDocumentPicked(DocumentInfo doc, Model model) { - } - - @Override - public void onDocumentsPicked(List<DocumentInfo> docs) { - } - - @Override - public boolean viewDocument(DocumentInfo doc) { - return false; - } - - @Override - public boolean previewDocument(DocumentInfo doc, Model model) { - return false; + TestActivity activity = Mockito.mock(TestActivity.class, Mockito.CALLS_REAL_METHODS); + activity.init(); + return activity; } } + +// Trick Mockito into finding our Addons methods correctly. W/o this +// hack, Mockito thinks Addons methods are not implemented. +abstract class AbstractBase extends com.android.documentsui.TestActivity + implements ActionHandler.Addons {} diff --git a/tests/unit/com/android/documentsui/roots/RootsCacheTest.java b/tests/unit/com/android/documentsui/roots/RootsCacheTest.java index 6adead8c0..ff7277b93 100644 --- a/tests/unit/com/android/documentsui/roots/RootsCacheTest.java +++ b/tests/unit/com/android/documentsui/roots/RootsCacheTest.java @@ -16,7 +16,6 @@ package com.android.documentsui.roots; -import static com.android.documentsui.roots.RootsCache.getMatchingRoots; import static com.google.common.collect.Lists.newArrayList; import android.test.AndroidTestCase; @@ -64,7 +63,7 @@ public class RootsCacheTest extends AndroidTestCase { mState.acceptMimes = new String[] { "*/*" }; assertContainsExactly( newArrayList(mNull, mWild, mImages, mAudio, mDocs, mMalformed1, mMalformed2), - getMatchingRoots(mRoots, mState)); + RootsAccess.getMatchingRoots(mRoots, mState)); } public void testMatchingRoots_DirectoryCopy() throws Exception { @@ -78,56 +77,56 @@ public class RootsCacheTest extends AndroidTestCase { // basically we're asserting that the results don't contain downloads assertContainsExactly( newArrayList(mNull, mWild, mImages, mAudio, mDocs, mMalformed1, mMalformed2), - getMatchingRoots(mRoots, mState)); + RootsAccess.getMatchingRoots(mRoots, mState)); } public void testMatchingRoots_PngOrWild() throws Exception { mState.acceptMimes = new String[] { "image/png", "*/*" }; assertContainsExactly( newArrayList(mNull, mWild, mImages, mAudio, mDocs, mMalformed1, mMalformed2), - getMatchingRoots(mRoots, mState)); + RootsAccess.getMatchingRoots(mRoots, mState)); } public void testMatchingRoots_AudioWild() throws Exception { mState.acceptMimes = new String[] { "audio/*" }; assertContainsExactly( newArrayList(mNull, mWild, mAudio), - getMatchingRoots(mRoots, mState)); + RootsAccess.getMatchingRoots(mRoots, mState)); } public void testMatchingRoots_AudioWildOrImageWild() throws Exception { mState.acceptMimes = new String[] { "audio/*", "image/*" }; assertContainsExactly( newArrayList(mNull, mWild, mAudio, mImages), - getMatchingRoots(mRoots, mState)); + RootsAccess.getMatchingRoots(mRoots, mState)); } public void testMatchingRoots_AudioSpecific() throws Exception { mState.acceptMimes = new String[] { "audio/mpeg" }; assertContainsExactly( newArrayList(mNull, mWild, mAudio), - getMatchingRoots(mRoots, mState)); + RootsAccess.getMatchingRoots(mRoots, mState)); } public void testMatchingRoots_Document() throws Exception { mState.acceptMimes = new String[] { "application/msword" }; assertContainsExactly( newArrayList(mNull, mWild, mDocs), - getMatchingRoots(mRoots, mState)); + RootsAccess.getMatchingRoots(mRoots, mState)); } public void testMatchingRoots_Application() throws Exception { mState.acceptMimes = new String[] { "application/*" }; assertContainsExactly( newArrayList(mNull, mWild, mAudio, mDocs), - getMatchingRoots(mRoots, mState)); + RootsAccess.getMatchingRoots(mRoots, mState)); } public void testMatchingRoots_FlacOrPng() throws Exception { mState.acceptMimes = new String[] { "application/x-flac", "image/png" }; assertContainsExactly( newArrayList(mNull, mWild, mAudio, mImages), - getMatchingRoots(mRoots, mState)); + RootsAccess.getMatchingRoots(mRoots, mState)); } public void testExcludedAuthorities() throws Exception { @@ -152,7 +151,7 @@ public class RootsCacheTest extends AndroidTestCase { assertContainsExactly( allowedRoots, - getMatchingRoots(roots, mState)); + RootsAccess.getMatchingRoots(roots, mState)); } private static void assertContainsExactly(List<?> expected, List<?> actual) { |