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