• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui;
18 
19 import static com.android.documentsui.base.DocumentInfo.getCursorInt;
20 import static com.android.documentsui.base.DocumentInfo.getCursorString;
21 import static com.android.documentsui.base.Shared.DEBUG;
22 
23 import android.app.Activity;
24 import android.app.LoaderManager.LoaderCallbacks;
25 import android.app.PendingIntent;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentSender;
29 import android.content.Loader;
30 import android.content.pm.ResolveInfo;
31 import android.database.Cursor;
32 import android.graphics.drawable.ColorDrawable;
33 import android.net.Uri;
34 import android.os.Bundle;
35 import android.os.Parcelable;
36 import android.provider.DocumentsContract;
37 import android.support.annotation.VisibleForTesting;
38 import android.util.Log;
39 import android.util.Pair;
40 import android.view.DragEvent;
41 
42 import com.android.documentsui.AbstractActionHandler.CommonAddons;
43 import com.android.documentsui.LoadDocStackTask.LoadDocStackCallback;
44 import com.android.documentsui.base.BooleanConsumer;
45 import com.android.documentsui.base.DocumentInfo;
46 import com.android.documentsui.base.DocumentStack;
47 import com.android.documentsui.base.Features;
48 import com.android.documentsui.base.Lookup;
49 import com.android.documentsui.base.Providers;
50 import com.android.documentsui.base.RootInfo;
51 import com.android.documentsui.base.Shared;
52 import com.android.documentsui.base.State;
53 import com.android.documentsui.dirlist.AnimationView;
54 import com.android.documentsui.dirlist.AnimationView.AnimationType;
55 import com.android.documentsui.dirlist.DocumentDetails;
56 import com.android.documentsui.dirlist.FocusHandler;
57 import com.android.documentsui.files.LauncherActivity;
58 import com.android.documentsui.queries.SearchViewManager;
59 import com.android.documentsui.roots.GetRootDocumentTask;
60 import com.android.documentsui.roots.LoadRootTask;
61 import com.android.documentsui.roots.ProvidersAccess;
62 import com.android.documentsui.selection.Selection;
63 import com.android.documentsui.selection.SelectionManager;
64 import com.android.documentsui.sidebar.EjectRootTask;
65 import com.android.documentsui.ui.Snackbars;
66 
67 import java.util.ArrayList;
68 import java.util.List;
69 import java.util.Objects;
70 import java.util.concurrent.Executor;
71 import java.util.function.Consumer;
72 
73 import javax.annotation.Nullable;
74 
75 /**
76  * Provides support for specializing the actions (openDocument etc.) to the host activity.
77  */
78 public abstract class AbstractActionHandler<T extends Activity & CommonAddons>
79         implements ActionHandler {
80 
81     @VisibleForTesting
82     public static final int CODE_FORWARD = 42;
83     public static final int CODE_AUTHENTICATION = 43;
84 
85     @VisibleForTesting
86     static final int LOADER_ID = 42;
87 
88     private static final String TAG = "AbstractActionHandler";
89     private static final int REFRESH_SPINNER_TIMEOUT = 500;
90 
91     protected final T mActivity;
92     protected final State mState;
93     protected final ProvidersAccess mProviders;
94     protected final DocumentsAccess mDocs;
95     protected final FocusHandler mFocusHandler;
96     protected final SelectionManager mSelectionMgr;
97     protected final SearchViewManager mSearchMgr;
98     protected final Lookup<String, Executor> mExecutors;
99     protected final Injector<?> mInjector;
100 
101     private final LoaderBindings mBindings;
102 
103     private Runnable mDisplayStateChangedListener;
104 
105     private DirectoryReloadLock mDirectoryReloadLock;
106 
107     @Override
registerDisplayStateChangedListener(Runnable l)108     public void registerDisplayStateChangedListener(Runnable l) {
109         mDisplayStateChangedListener = l;
110     }
111     @Override
unregisterDisplayStateChangedListener(Runnable l)112     public void unregisterDisplayStateChangedListener(Runnable l) {
113         if (mDisplayStateChangedListener == l) {
114             mDisplayStateChangedListener = null;
115         }
116     }
117 
AbstractActionHandler( T activity, State state, ProvidersAccess providers, DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, Injector<?> injector)118     public AbstractActionHandler(
119             T activity,
120             State state,
121             ProvidersAccess providers,
122             DocumentsAccess docs,
123             SearchViewManager searchMgr,
124             Lookup<String, Executor> executors,
125             Injector<?> injector) {
126 
127         assert(activity != null);
128         assert(state != null);
129         assert(providers != null);
130         assert(searchMgr != null);
131         assert(docs != null);
132         assert(injector != null);
133 
134         mActivity = activity;
135         mState = state;
136         mProviders = providers;
137         mDocs = docs;
138         mFocusHandler = injector.focusManager;
139         mSelectionMgr = injector.selectionMgr;
140         mSearchMgr = searchMgr;
141         mExecutors = executors;
142         mInjector = injector;
143 
144         mBindings = new LoaderBindings();
145     }
146 
147     @Override
ejectRoot(RootInfo root, BooleanConsumer listener)148     public void ejectRoot(RootInfo root, BooleanConsumer listener) {
149         new EjectRootTask(
150                 mActivity.getContentResolver(),
151                 root.authority,
152                 root.rootId,
153                 listener).executeOnExecutor(ProviderExecutor.forAuthority(root.authority));
154     }
155 
156     @Override
startAuthentication(PendingIntent intent)157     public void startAuthentication(PendingIntent intent) {
158         try {
159             mActivity.startIntentSenderForResult(intent.getIntentSender(), CODE_AUTHENTICATION,
160                     null, 0, 0, 0);
161         } catch (IntentSender.SendIntentException cancelled) {
162             Log.d(TAG, "Authentication Pending Intent either canceled or ignored.");
163         }
164     }
165 
166     @Override
onActivityResult(int requestCode, int resultCode, Intent data)167     public void onActivityResult(int requestCode, int resultCode, Intent data) {
168         switch (requestCode) {
169             case CODE_AUTHENTICATION:
170                 onAuthenticationResult(resultCode);
171                 break;
172         }
173     }
174 
onAuthenticationResult(int resultCode)175     private void onAuthenticationResult(int resultCode) {
176         if (resultCode == Activity.RESULT_OK) {
177             Log.v(TAG, "Authentication was successful. Refreshing directory now.");
178             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
179         }
180     }
181 
182     @Override
getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback)183     public void getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback) {
184         GetRootDocumentTask task = new GetRootDocumentTask(
185                 root,
186                 mActivity,
187                 timeout,
188                 mDocs,
189                 callback);
190 
191         task.executeOnExecutor(mExecutors.lookup(root.authority));
192     }
193 
194     @Override
refreshDocument(DocumentInfo doc, BooleanConsumer callback)195     public void refreshDocument(DocumentInfo doc, BooleanConsumer callback) {
196         RefreshTask task = new RefreshTask(
197                 mInjector.features,
198                 mState,
199                 doc,
200                 REFRESH_SPINNER_TIMEOUT,
201                 mActivity.getApplicationContext(),
202                 mActivity::isDestroyed,
203                 callback);
204         task.executeOnExecutor(mExecutors.lookup(doc == null ? null : doc.authority));
205     }
206 
207     @Override
openSelectedInNewWindow()208     public void openSelectedInNewWindow() {
209         throw new UnsupportedOperationException("Can't open in new window.");
210     }
211 
212     @Override
openInNewWindow(DocumentStack path)213     public void openInNewWindow(DocumentStack path) {
214         Metrics.logUserAction(mActivity, Metrics.USER_ACTION_NEW_WINDOW);
215 
216         Intent intent = LauncherActivity.createLaunchIntent(mActivity);
217         intent.putExtra(Shared.EXTRA_STACK, (Parcelable) path);
218 
219         // Multi-window necessitates we pick how we are launched.
220         // By default we'd be launched in-place above the existing app.
221         // By setting launch-to-side ActivityManager will open us to side.
222         if (mActivity.isInMultiWindowMode()) {
223             intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT);
224         }
225 
226         mActivity.startActivity(intent);
227     }
228 
229     @Override
openDocument(DocumentDetails doc, @ViewType int type, @ViewType int fallback)230     public boolean openDocument(DocumentDetails doc, @ViewType int type, @ViewType int fallback) {
231         throw new UnsupportedOperationException("Can't open document.");
232     }
233 
234     @Override
springOpenDirectory(DocumentInfo doc)235     public void springOpenDirectory(DocumentInfo doc) {
236         throw new UnsupportedOperationException("Can't spring open directories.");
237     }
238 
239     @Override
openSettings(RootInfo root)240     public void openSettings(RootInfo root) {
241         throw new UnsupportedOperationException("Can't open settings.");
242     }
243 
244     @Override
openRoot(ResolveInfo app)245     public void openRoot(ResolveInfo app) {
246         throw new UnsupportedOperationException("Can't open an app.");
247     }
248 
249     @Override
showAppDetails(ResolveInfo info)250     public void showAppDetails(ResolveInfo info) {
251         throw new UnsupportedOperationException("Can't show app details.");
252     }
253 
254     @Override
dropOn(DragEvent event, RootInfo root)255     public boolean dropOn(DragEvent event, RootInfo root) {
256         throw new UnsupportedOperationException("Can't open an app.");
257     }
258 
259     @Override
pasteIntoFolder(RootInfo root)260     public void pasteIntoFolder(RootInfo root) {
261         throw new UnsupportedOperationException("Can't paste into folder.");
262     }
263 
264     @Override
viewInOwner()265     public void viewInOwner() {
266         throw new UnsupportedOperationException("Can't view in application.");
267     }
268 
269     @Override
selectAllFiles()270     public void selectAllFiles() {
271         Metrics.logUserAction(mActivity, Metrics.USER_ACTION_SELECT_ALL);
272         Model model = mInjector.getModel();
273 
274         // Exclude disabled files
275         List<String> enabled = new ArrayList<>();
276         for (String id : model.getModelIds()) {
277             Cursor cursor = model.getItem(id);
278             if (cursor == null) {
279                 Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id);
280                 continue;
281             }
282             String docMimeType = getCursorString(
283                     cursor, DocumentsContract.Document.COLUMN_MIME_TYPE);
284             int docFlags = getCursorInt(cursor, DocumentsContract.Document.COLUMN_FLAGS);
285             if (mInjector.config.isDocumentEnabled(docMimeType, docFlags, mState)) {
286                 enabled.add(id);
287             }
288         }
289 
290         // Only select things currently visible in the adapter.
291         boolean changed = mSelectionMgr.setItemsSelected(enabled, true);
292         if (changed) {
293             mDisplayStateChangedListener.run();
294         }
295     }
296 
297     @Override
298     @Nullable
renameDocument(String name, DocumentInfo document)299     public DocumentInfo renameDocument(String name, DocumentInfo document) {
300         throw new UnsupportedOperationException("Can't rename documents.");
301     }
302 
303     @Override
showChooserForDoc(DocumentInfo doc)304     public void showChooserForDoc(DocumentInfo doc) {
305         throw new UnsupportedOperationException("Show chooser for doc not supported!");
306     }
307 
308     @Override
openRootDocument(@ullable DocumentInfo rootDoc)309     public void openRootDocument(@Nullable DocumentInfo rootDoc) {
310         if (rootDoc == null) {
311             // There are 2 cases where rootDoc is null -- 1) loading recents; 2) failed to load root
312             // document. Either case we should call refreshCurrentRootAndDirectory() to let
313             // DirectoryFragment update UI.
314             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
315         } else {
316             openContainerDocument(rootDoc);
317         }
318     }
319 
320     @Override
openContainerDocument(DocumentInfo doc)321     public void openContainerDocument(DocumentInfo doc) {
322         assert(doc.isContainer());
323 
324         if (mSearchMgr.isSearching()) {
325             loadDocument(
326                     doc.derivedUri,
327                     (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc));
328         } else {
329             openChildContainer(doc);
330         }
331     }
332 
openFolderInSearchResult(@ullable DocumentStack stack, DocumentInfo doc)333     private void openFolderInSearchResult(@Nullable DocumentStack stack, DocumentInfo doc) {
334         if (stack == null) {
335             mState.stack.popToRootDocument();
336 
337             // Update navigator to give horizontal breadcrumb a chance to update documents. It
338             // doesn't update its content if the size of document stack doesn't change.
339             // TODO: update breadcrumb to take range update.
340             mActivity.updateNavigator();
341 
342             mState.stack.push(doc);
343         } else {
344             if (!Objects.equals(mState.stack.getRoot(), stack.getRoot())) {
345                 Log.w(TAG, "Provider returns " + stack.getRoot() + " rather than expected "
346                         + mState.stack.getRoot());
347             }
348 
349             mState.stack.reset();
350             // Update navigator to give horizontal breadcrumb a chance to update documents. It
351             // doesn't update its content if the size of document stack doesn't change.
352             // TODO: update breadcrumb to take range update.
353             mActivity.updateNavigator();
354 
355             mState.stack.reset(stack);
356         }
357 
358         // Show an opening animation only if pressing "back" would get us back to the
359         // previous directory. Especially after opening a root document, pressing
360         // back, wouldn't go to the previous root, but close the activity.
361         final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1)
362                 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
363         mActivity.refreshCurrentRootAndDirectory(anim);
364     }
365 
openChildContainer(DocumentInfo doc)366     private void openChildContainer(DocumentInfo doc) {
367         DocumentInfo currentDoc = null;
368 
369         if (doc.isDirectory()) {
370             // Regular directory.
371             currentDoc = doc;
372         } else if (doc.isArchive()) {
373             // Archive.
374             currentDoc = mDocs.getArchiveDocument(doc.derivedUri);
375         }
376 
377         assert(currentDoc != null);
378         mActivity.notifyDirectoryNavigated(currentDoc.derivedUri);
379 
380         mState.stack.push(currentDoc);
381         // Show an opening animation only if pressing "back" would get us back to the
382         // previous directory. Especially after opening a root document, pressing
383         // back, wouldn't go to the previous root, but close the activity.
384         final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1)
385                 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
386         mActivity.refreshCurrentRootAndDirectory(anim);
387     }
388 
389     @Override
setDebugMode(boolean enabled)390     public void setDebugMode(boolean enabled) {
391         if (!mInjector.features.isDebugSupportEnabled()) {
392             return;
393         }
394 
395         mState.debugMode = enabled;
396         mInjector.features.forceFeature(R.bool.feature_command_interceptor, enabled);
397         mActivity.invalidateOptionsMenu();
398 
399         if (enabled) {
400             showDebugMessage();
401         } else {
402             mActivity.getActionBar().setBackgroundDrawable(new ColorDrawable(
403                     mActivity.getResources().getColor(R.color.primary)));
404             mActivity.getWindow().setStatusBarColor(
405                     mActivity.getResources().getColor(R.color.primary_dark));
406         }
407     }
408 
409     @Override
showDebugMessage()410     public void showDebugMessage() {
411         assert (mInjector.features.isDebugSupportEnabled());
412 
413         int[] colors = mInjector.debugHelper.getNextColors();
414         Pair<String, Integer> messagePair = mInjector.debugHelper.getNextMessage();
415 
416         Snackbars.showCustomTextWithImage(mActivity, messagePair.first, messagePair.second);
417 
418         mActivity.getActionBar().setBackgroundDrawable(new ColorDrawable(colors[0]));
419         mActivity.getWindow().setStatusBarColor(colors[1]);
420     }
421 
422     @Override
cutToClipboard()423     public void cutToClipboard() {
424         throw new UnsupportedOperationException("Cut not supported!");
425     }
426 
427     @Override
copyToClipboard()428     public void copyToClipboard() {
429         throw new UnsupportedOperationException("Copy not supported!");
430     }
431 
432     @Override
deleteSelectedDocuments()433     public void deleteSelectedDocuments() {
434         throw new UnsupportedOperationException("Delete not supported!");
435     }
436 
437     @Override
shareSelectedDocuments()438     public void shareSelectedDocuments() {
439         throw new UnsupportedOperationException("Share not supported!");
440     }
441 
loadDocument(Uri uri, LoadDocStackCallback callback)442     protected final void loadDocument(Uri uri, LoadDocStackCallback callback) {
443         new LoadDocStackTask(
444                 mActivity,
445                 mProviders,
446                 mDocs,
447                 callback
448                 ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri);
449     }
450 
451     @Override
loadRoot(Uri uri)452     public final void loadRoot(Uri uri) {
453         new LoadRootTask<>(mActivity, mProviders, mState, uri)
454                 .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
455     }
456 
457     @Override
loadDocumentsForCurrentStack()458     public void loadDocumentsForCurrentStack() {
459         DocumentStack stack = mState.stack;
460         if (!stack.isRecents() && stack.isEmpty()) {
461             DirectoryResult result = new DirectoryResult();
462 
463             // TODO (b/35996595): Consider plumbing through the actual exception, though it might
464             // not be very useful (always pointing to DatabaseUtils#readExceptionFromParcel()).
465             result.exception = new IllegalStateException("Failed to load root document.");
466             mInjector.getModel().update(result);
467             return;
468         }
469 
470         mActivity.getLoaderManager().restartLoader(LOADER_ID, null, mBindings);
471     }
472 
launchToDocument(Uri uri)473     protected final boolean launchToDocument(Uri uri) {
474         // We don't support launching to a document in an archive.
475         if (!Providers.isArchiveUri(uri)) {
476             loadDocument(uri, this::onStackLoaded);
477             return true;
478         }
479 
480         return false;
481     }
482 
onStackLoaded(@ullable DocumentStack stack)483     private void onStackLoaded(@Nullable DocumentStack stack) {
484         if (stack != null) {
485             if (!stack.peek().isDirectory()) {
486                 // Requested document is not a directory. Pop it so that we can launch into its
487                 // parent.
488                 stack.pop();
489             }
490             mState.stack.reset(stack);
491             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
492 
493             Metrics.logLaunchAtLocation(mActivity, mState, stack.getRoot().getUri());
494         } else {
495             Log.w(TAG, "Failed to launch into the given uri. Launch to default location.");
496             launchToDefaultLocation();
497 
498             Metrics.logLaunchAtLocation(mActivity, mState, null);
499         }
500     }
501 
launchToDefaultLocation()502     protected abstract void launchToDefaultLocation();
503 
loadHomeDir()504     protected final void loadHomeDir() {
505         loadRoot(Shared.getDefaultRootUri(mActivity));
506     }
507 
getStableSelection()508     protected Selection getStableSelection() {
509         return mSelectionMgr.getSelection(new Selection());
510     }
511 
512     @Override
reset(DirectoryReloadLock reloadLock)513     public ActionHandler reset(DirectoryReloadLock reloadLock) {
514         mDirectoryReloadLock = reloadLock;
515         mActivity.getLoaderManager().destroyLoader(LOADER_ID);
516         return this;
517     }
518 
519     private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> {
520 
521         @Override
onCreateLoader(int id, Bundle args)522         public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
523             Context context = mActivity;
524 
525             if (mState.stack.isRecents()) {
526 
527                 if (DEBUG) Log.d(TAG, "Creating new loader recents.");
528                 return new RecentsLoader(context, mProviders, mState, mInjector.features);
529 
530             } else {
531 
532                 Uri contentsUri = mSearchMgr.isSearching()
533                         ? DocumentsContract.buildSearchDocumentsUri(
534                             mState.stack.getRoot().authority,
535                             mState.stack.getRoot().rootId,
536                             mSearchMgr.getCurrentSearch())
537                         : DocumentsContract.buildChildDocumentsUri(
538                                 mState.stack.peek().authority,
539                                 mState.stack.peek().documentId);
540 
541                 if (mInjector.config.managedModeEnabled(mState.stack)) {
542                     contentsUri = DocumentsContract.setManageMode(contentsUri);
543                 }
544 
545                 if (DEBUG) Log.d(TAG,
546                         "Creating new directory loader for: "
547                                 + DocumentInfo.debugString(mState.stack.peek()));
548 
549                 return new DirectoryLoader(
550                         mInjector.features,
551                         context,
552                         mState.stack.getRoot(),
553                         mState.stack.peek(),
554                         contentsUri,
555                         mState.sortModel,
556                         mDirectoryReloadLock,
557                         mSearchMgr.isSearching());
558             }
559         }
560 
561         @Override
onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result)562         public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
563             if (DEBUG) Log.d(TAG, "Loader has finished for: "
564                     + DocumentInfo.debugString(mState.stack.peek()));
565             assert(result != null);
566 
567             mInjector.getModel().update(result);
568         }
569 
570         @Override
onLoaderReset(Loader<DirectoryResult> loader)571         public void onLoaderReset(Loader<DirectoryResult> loader) {}
572     }
573     /**
574      * A class primarily for the support of isolating our tests
575      * from our concrete activity implementations.
576      */
577     public interface CommonAddons {
refreshCurrentRootAndDirectory(@nimationType int anim)578         void refreshCurrentRootAndDirectory(@AnimationType int anim);
onRootPicked(RootInfo root)579         void onRootPicked(RootInfo root);
580         // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity.
onDocumentsPicked(List<DocumentInfo> docs)581         void onDocumentsPicked(List<DocumentInfo> docs);
onDocumentPicked(DocumentInfo doc)582         void onDocumentPicked(DocumentInfo doc);
getCurrentRoot()583         RootInfo getCurrentRoot();
getCurrentDirectory()584         DocumentInfo getCurrentDirectory();
setRootsDrawerOpen(boolean open)585         void setRootsDrawerOpen(boolean open);
586 
587         // TODO: Let navigator listens to State
updateNavigator()588         void updateNavigator();
589 
590         @VisibleForTesting
notifyDirectoryNavigated(Uri docUri)591         void notifyDirectoryNavigated(Uri docUri);
592     }
593 }
594