• 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.SharedMinimal.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.Lookup;
48 import com.android.documentsui.base.Providers;
49 import com.android.documentsui.base.RootInfo;
50 import com.android.documentsui.base.Shared;
51 import com.android.documentsui.base.State;
52 import com.android.documentsui.dirlist.AnimationView;
53 import com.android.documentsui.dirlist.AnimationView.AnimationType;
54 import com.android.documentsui.dirlist.FocusHandler;
55 import com.android.documentsui.files.LauncherActivity;
56 import com.android.documentsui.queries.SearchViewManager;
57 import com.android.documentsui.roots.GetRootDocumentTask;
58 import com.android.documentsui.roots.LoadRootTask;
59 import com.android.documentsui.roots.ProvidersAccess;
60 import com.android.documentsui.selection.ContentLock;
61 import com.android.documentsui.selection.MutableSelection;
62 import com.android.documentsui.selection.SelectionHelper;
63 import com.android.documentsui.selection.ItemDetailsLookup.ItemDetails;
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 SelectionHelper 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 ContentLock mContentLock;
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
openItem(ItemDetails doc, @ViewType int type, @ViewType int fallback)230     public boolean openItem(ItemDetails doc, @ViewType int type, @ViewType int fallback) {
231         throw new UnsupportedOperationException("Can't open document.");
232     }
233 
234     @Override
showInspector(DocumentInfo doc)235     public void showInspector(DocumentInfo doc) {
236         throw new UnsupportedOperationException("Can't open properties.");
237     }
238 
239     @Override
springOpenDirectory(DocumentInfo doc)240     public void springOpenDirectory(DocumentInfo doc) {
241         throw new UnsupportedOperationException("Can't spring open directories.");
242     }
243 
244     @Override
openSettings(RootInfo root)245     public void openSettings(RootInfo root) {
246         throw new UnsupportedOperationException("Can't open settings.");
247     }
248 
249     @Override
openRoot(ResolveInfo app)250     public void openRoot(ResolveInfo app) {
251         throw new UnsupportedOperationException("Can't open an app.");
252     }
253 
254     @Override
showAppDetails(ResolveInfo info)255     public void showAppDetails(ResolveInfo info) {
256         throw new UnsupportedOperationException("Can't show app details.");
257     }
258 
259     @Override
dropOn(DragEvent event, RootInfo root)260     public boolean dropOn(DragEvent event, RootInfo root) {
261         throw new UnsupportedOperationException("Can't open an app.");
262     }
263 
264     @Override
pasteIntoFolder(RootInfo root)265     public void pasteIntoFolder(RootInfo root) {
266         throw new UnsupportedOperationException("Can't paste into folder.");
267     }
268 
269     @Override
viewInOwner()270     public void viewInOwner() {
271         throw new UnsupportedOperationException("Can't view in application.");
272     }
273 
274     @Override
selectAllFiles()275     public void selectAllFiles() {
276         Metrics.logUserAction(mActivity, Metrics.USER_ACTION_SELECT_ALL);
277         Model model = mInjector.getModel();
278 
279         // Exclude disabled files
280         List<String> enabled = new ArrayList<>();
281         for (String id : model.getModelIds()) {
282             Cursor cursor = model.getItem(id);
283             if (cursor == null) {
284                 Log.w(TAG, "Skipping selection. Can't obtain cursor for modeId: " + id);
285                 continue;
286             }
287             String docMimeType = getCursorString(
288                     cursor, DocumentsContract.Document.COLUMN_MIME_TYPE);
289             int docFlags = getCursorInt(cursor, DocumentsContract.Document.COLUMN_FLAGS);
290             if (mInjector.config.isDocumentEnabled(docMimeType, docFlags, mState)) {
291                 enabled.add(id);
292             }
293         }
294 
295         // Only select things currently visible in the adapter.
296         boolean changed = mSelectionMgr.setItemsSelected(enabled, true);
297         if (changed) {
298             mDisplayStateChangedListener.run();
299         }
300     }
301 
302     @Override
showCreateDirectoryDialog()303     public void showCreateDirectoryDialog() {
304         Metrics.logUserAction(mActivity, Metrics.USER_ACTION_CREATE_DIR);
305 
306         CreateDirectoryFragment.show(mActivity.getFragmentManager());
307     }
308 
309     @Override
310     @Nullable
renameDocument(String name, DocumentInfo document)311     public DocumentInfo renameDocument(String name, DocumentInfo document) {
312         throw new UnsupportedOperationException("Can't rename documents.");
313     }
314 
315     @Override
showChooserForDoc(DocumentInfo doc)316     public void showChooserForDoc(DocumentInfo doc) {
317         throw new UnsupportedOperationException("Show chooser for doc not supported!");
318     }
319 
320     @Override
openRootDocument(@ullable DocumentInfo rootDoc)321     public void openRootDocument(@Nullable DocumentInfo rootDoc) {
322         if (rootDoc == null) {
323             // There are 2 cases where rootDoc is null -- 1) loading recents; 2) failed to load root
324             // document. Either case we should call refreshCurrentRootAndDirectory() to let
325             // DirectoryFragment update UI.
326             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
327         } else {
328             openContainerDocument(rootDoc);
329         }
330     }
331 
332     @Override
openContainerDocument(DocumentInfo doc)333     public void openContainerDocument(DocumentInfo doc) {
334         assert(doc.isContainer());
335 
336         if (mSearchMgr.isSearching()) {
337             loadDocument(
338                     doc.derivedUri,
339                     (@Nullable DocumentStack stack) -> openFolderInSearchResult(stack, doc));
340         } else {
341             openChildContainer(doc);
342         }
343     }
344 
openFolderInSearchResult(@ullable DocumentStack stack, DocumentInfo doc)345     private void openFolderInSearchResult(@Nullable DocumentStack stack, DocumentInfo doc) {
346         if (stack == null) {
347             mState.stack.popToRootDocument();
348 
349             // Update navigator to give horizontal breadcrumb a chance to update documents. It
350             // doesn't update its content if the size of document stack doesn't change.
351             // TODO: update breadcrumb to take range update.
352             mActivity.updateNavigator();
353 
354             mState.stack.push(doc);
355         } else {
356             if (!Objects.equals(mState.stack.getRoot(), stack.getRoot())) {
357                 Log.w(TAG, "Provider returns " + stack.getRoot() + " rather than expected "
358                         + mState.stack.getRoot());
359             }
360 
361             final DocumentInfo top = stack.peek();
362             if (top.isArchive()) {
363                 // Swap the zip file in original provider and the one provided by ArchiveProvider.
364                 stack.pop();
365                 stack.push(mDocs.getArchiveDocument(top.derivedUri));
366             }
367 
368             mState.stack.reset();
369             // Update navigator to give horizontal breadcrumb a chance to update documents. It
370             // doesn't update its content if the size of document stack doesn't change.
371             // TODO: update breadcrumb to take range update.
372             mActivity.updateNavigator();
373 
374             mState.stack.reset(stack);
375         }
376 
377         // Show an opening animation only if pressing "back" would get us back to the
378         // previous directory. Especially after opening a root document, pressing
379         // back, wouldn't go to the previous root, but close the activity.
380         final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1)
381                 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
382         mActivity.refreshCurrentRootAndDirectory(anim);
383     }
384 
openChildContainer(DocumentInfo doc)385     private void openChildContainer(DocumentInfo doc) {
386         DocumentInfo currentDoc = null;
387 
388         if (doc.isDirectory()) {
389             // Regular directory.
390             currentDoc = doc;
391         } else if (doc.isArchive()) {
392             // Archive.
393             currentDoc = mDocs.getArchiveDocument(doc.derivedUri);
394         }
395 
396         assert(currentDoc != null);
397         mActivity.notifyDirectoryNavigated(currentDoc.derivedUri);
398 
399         mState.stack.push(currentDoc);
400         // Show an opening animation only if pressing "back" would get us back to the
401         // previous directory. Especially after opening a root document, pressing
402         // back, wouldn't go to the previous root, but close the activity.
403         final int anim = (mState.stack.hasLocationChanged() && mState.stack.size() > 1)
404                 ? AnimationView.ANIM_ENTER : AnimationView.ANIM_NONE;
405         mActivity.refreshCurrentRootAndDirectory(anim);
406     }
407 
408     @Override
setDebugMode(boolean enabled)409     public void setDebugMode(boolean enabled) {
410         if (!mInjector.features.isDebugSupportEnabled()) {
411             return;
412         }
413 
414         mState.debugMode = enabled;
415         mInjector.features.forceFeature(R.bool.feature_command_interceptor, enabled);
416         mInjector.features.forceFeature(R.bool.feature_inspector, enabled);
417         mActivity.invalidateOptionsMenu();
418 
419         if (enabled) {
420             showDebugMessage();
421         } else {
422             mActivity.getActionBar().setBackgroundDrawable(new ColorDrawable(
423                     mActivity.getResources().getColor(R.color.primary)));
424             mActivity.getWindow().setStatusBarColor(
425                     mActivity.getResources().getColor(R.color.primary_dark));
426         }
427     }
428 
429     @Override
showDebugMessage()430     public void showDebugMessage() {
431         assert (mInjector.features.isDebugSupportEnabled());
432 
433         int[] colors = mInjector.debugHelper.getNextColors();
434         Pair<String, Integer> messagePair = mInjector.debugHelper.getNextMessage();
435 
436         Snackbars.showCustomTextWithImage(mActivity, messagePair.first, messagePair.second);
437 
438         mActivity.getActionBar().setBackgroundDrawable(new ColorDrawable(colors[0]));
439         mActivity.getWindow().setStatusBarColor(colors[1]);
440     }
441 
442     @Override
cutToClipboard()443     public void cutToClipboard() {
444         throw new UnsupportedOperationException("Cut not supported!");
445     }
446 
447     @Override
copyToClipboard()448     public void copyToClipboard() {
449         throw new UnsupportedOperationException("Copy not supported!");
450     }
451 
452     @Override
deleteSelectedDocuments()453     public void deleteSelectedDocuments() {
454         throw new UnsupportedOperationException("Delete not supported!");
455     }
456 
457     @Override
shareSelectedDocuments()458     public void shareSelectedDocuments() {
459         throw new UnsupportedOperationException("Share not supported!");
460     }
461 
loadDocument(Uri uri, LoadDocStackCallback callback)462     protected final void loadDocument(Uri uri, LoadDocStackCallback callback) {
463         new LoadDocStackTask(
464                 mActivity,
465                 mProviders,
466                 mDocs,
467                 callback
468                 ).executeOnExecutor(mExecutors.lookup(uri.getAuthority()), uri);
469     }
470 
471     @Override
loadRoot(Uri uri)472     public final void loadRoot(Uri uri) {
473         new LoadRootTask<>(mActivity, mProviders, mState, uri)
474                 .executeOnExecutor(mExecutors.lookup(uri.getAuthority()));
475     }
476 
477     @Override
loadDocumentsForCurrentStack()478     public void loadDocumentsForCurrentStack() {
479         DocumentStack stack = mState.stack;
480         if (!stack.isRecents() && stack.isEmpty()) {
481             DirectoryResult result = new DirectoryResult();
482 
483             // TODO (b/35996595): Consider plumbing through the actual exception, though it might
484             // not be very useful (always pointing to DatabaseUtils#readExceptionFromParcel()).
485             result.exception = new IllegalStateException("Failed to load root document.");
486             mInjector.getModel().update(result);
487             return;
488         }
489 
490         mActivity.getLoaderManager().restartLoader(LOADER_ID, null, mBindings);
491     }
492 
launchToDocument(Uri uri)493     protected final boolean launchToDocument(Uri uri) {
494         // We don't support launching to a document in an archive.
495         if (!Providers.isArchiveUri(uri)) {
496             loadDocument(uri, this::onStackLoaded);
497             return true;
498         }
499 
500         return false;
501     }
502 
onStackLoaded(@ullable DocumentStack stack)503     private void onStackLoaded(@Nullable DocumentStack stack) {
504         if (stack != null) {
505             if (!stack.peek().isDirectory()) {
506                 // Requested document is not a directory. Pop it so that we can launch into its
507                 // parent.
508                 stack.pop();
509             }
510             mState.stack.reset(stack);
511             mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
512 
513             Metrics.logLaunchAtLocation(mActivity, mState, stack.getRoot().getUri());
514         } else {
515             Log.w(TAG, "Failed to launch into the given uri. Launch to default location.");
516             launchToDefaultLocation();
517 
518             Metrics.logLaunchAtLocation(mActivity, mState, null);
519         }
520     }
521 
launchToDefaultLocation()522     protected abstract void launchToDefaultLocation();
523 
restoreRootAndDirectory()524     protected void restoreRootAndDirectory() {
525         if (!mState.stack.getRoot().isRecents() && mState.stack.isEmpty()) {
526             mActivity.onRootPicked(mState.stack.getRoot());
527         } else {
528             mActivity.restoreRootAndDirectory();
529         }
530     }
531 
loadHomeDir()532     protected final void loadHomeDir() {
533         loadRoot(Shared.getDefaultRootUri(mActivity));
534     }
535 
getStableSelection()536     protected MutableSelection getStableSelection() {
537         MutableSelection selection = new MutableSelection();
538         mSelectionMgr.copySelection(selection);
539         return selection;
540     }
541 
542     @Override
reset(ContentLock reloadLock)543     public ActionHandler reset(ContentLock reloadLock) {
544         mContentLock = reloadLock;
545         mActivity.getLoaderManager().destroyLoader(LOADER_ID);
546         return this;
547     }
548 
549     private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> {
550 
551         @Override
onCreateLoader(int id, Bundle args)552         public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) {
553             Context context = mActivity;
554 
555             if (mState.stack.isRecents()) {
556 
557                 if (DEBUG) Log.d(TAG, "Creating new loader recents.");
558                 return new RecentsLoader(
559                         context,
560                         mProviders,
561                         mState,
562                         mInjector.features,
563                         mExecutors,
564                         mInjector.fileTypeLookup);
565             } else {
566 
567                 Uri contentsUri = mSearchMgr.isSearching()
568                         ? DocumentsContract.buildSearchDocumentsUri(
569                             mState.stack.getRoot().authority,
570                             mState.stack.getRoot().rootId,
571                             mSearchMgr.getCurrentSearch())
572                         : DocumentsContract.buildChildDocumentsUri(
573                                 mState.stack.peek().authority,
574                                 mState.stack.peek().documentId);
575 
576                 if (mInjector.config.managedModeEnabled(mState.stack)) {
577                     contentsUri = DocumentsContract.setManageMode(contentsUri);
578                 }
579 
580                 if (DEBUG) Log.d(TAG,
581                         "Creating new directory loader for: "
582                                 + DocumentInfo.debugString(mState.stack.peek()));
583 
584                 return new DirectoryLoader(
585                         mInjector.features,
586                         context,
587                         mState.stack.getRoot(),
588                         mState.stack.peek(),
589                         contentsUri,
590                         mState.sortModel,
591                         mInjector.fileTypeLookup,
592                         mContentLock,
593                         mSearchMgr.isSearching());
594             }
595         }
596 
597         @Override
onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result)598         public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) {
599             if (DEBUG) Log.d(TAG, "Loader has finished for: "
600                     + DocumentInfo.debugString(mState.stack.peek()));
601             assert(result != null);
602 
603             mInjector.getModel().update(result);
604         }
605 
606         @Override
onLoaderReset(Loader<DirectoryResult> loader)607         public void onLoaderReset(Loader<DirectoryResult> loader) {}
608     }
609     /**
610      * A class primarily for the support of isolating our tests
611      * from our concrete activity implementations.
612      */
613     public interface CommonAddons {
restoreRootAndDirectory()614         void restoreRootAndDirectory();
refreshCurrentRootAndDirectory(@nimationType int anim)615         void refreshCurrentRootAndDirectory(@AnimationType int anim);
onRootPicked(RootInfo root)616         void onRootPicked(RootInfo root);
617         // TODO: Move this to PickAddons as multi-document picking is exclusive to that activity.
onDocumentsPicked(List<DocumentInfo> docs)618         void onDocumentsPicked(List<DocumentInfo> docs);
onDocumentPicked(DocumentInfo doc)619         void onDocumentPicked(DocumentInfo doc);
getCurrentRoot()620         RootInfo getCurrentRoot();
getCurrentDirectory()621         DocumentInfo getCurrentDirectory();
setRootsDrawerOpen(boolean open)622         void setRootsDrawerOpen(boolean open);
623 
624         // TODO: Let navigator listens to State
updateNavigator()625         void updateNavigator();
626 
627         @VisibleForTesting
notifyDirectoryNavigated(Uri docUri)628         void notifyDirectoryNavigated(Uri docUri);
629     }
630 }
631