• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 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.SharedMinimal.DEBUG;
20 import static com.android.documentsui.base.Shared.EXTRA_BENCHMARK;
21 import static com.android.documentsui.base.State.MODE_GRID;
22 
23 import android.app.Activity;
24 import android.app.Fragment;
25 import android.content.Intent;
26 import android.content.pm.PackageInfo;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ProviderInfo;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.MessageQueue.IdleHandler;
32 import android.preference.PreferenceManager;
33 import android.provider.DocumentsContract;
34 import android.support.annotation.CallSuper;
35 import android.support.annotation.LayoutRes;
36 import android.support.annotation.VisibleForTesting;
37 import android.util.Log;
38 import android.view.KeyEvent;
39 import android.view.Menu;
40 import android.view.MenuItem;
41 import android.view.View;
42 import android.widget.Toolbar;
43 
44 import com.android.documentsui.AbstractActionHandler.CommonAddons;
45 import com.android.documentsui.Injector.Injected;
46 import com.android.documentsui.NavigationViewManager.Breadcrumb;
47 import com.android.documentsui.base.DocumentInfo;
48 import com.android.documentsui.base.EventHandler;
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.base.State.ViewMode;
53 import com.android.documentsui.dirlist.AnimationView;
54 import com.android.documentsui.dirlist.DirectoryFragment;
55 import com.android.documentsui.prefs.LocalPreferences;
56 import com.android.documentsui.prefs.Preferences;
57 import com.android.documentsui.prefs.PreferencesMonitor;
58 import com.android.documentsui.prefs.ScopedPreferences;
59 import com.android.documentsui.queries.CommandInterceptor;
60 import com.android.documentsui.queries.SearchViewManager;
61 import com.android.documentsui.queries.SearchViewManager.SearchManagerListener;
62 import com.android.documentsui.roots.ProvidersCache;
63 import com.android.documentsui.selection.Selection;
64 import com.android.documentsui.sidebar.RootsFragment;
65 import com.android.documentsui.sorting.SortController;
66 import com.android.documentsui.sorting.SortModel;
67 
68 import java.util.ArrayList;
69 import java.util.Date;
70 import java.util.List;
71 
72 import javax.annotation.Nullable;
73 
74 public abstract class BaseActivity
75         extends Activity implements CommonAddons, NavigationViewManager.Environment {
76 
77     private static final String BENCHMARK_TESTING_PACKAGE = "com.android.documentsui.appperftests";
78 
79     protected SearchViewManager mSearchManager;
80     protected State mState;
81 
82     @Injected
83     protected Injector<?> mInjector;
84 
85     protected @Nullable RetainedState mRetainedState;
86     protected ProvidersCache mProviders;
87     protected DocumentsAccess mDocs;
88     protected DrawerController mDrawer;
89 
90     protected NavigationViewManager mNavigator;
91     protected SortController mSortController;
92 
93     private final List<EventListener> mEventListeners = new ArrayList<>();
94     private final String mTag;
95 
96     @LayoutRes
97     private int mLayoutId;
98 
99     private RootsMonitor<BaseActivity> mRootsMonitor;
100 
101     private long mStartTime;
102 
103     private PreferencesMonitor mPreferencesMonitor;
104 
BaseActivity(@ayoutRes int layoutId, String tag)105     public BaseActivity(@LayoutRes int layoutId, String tag) {
106         mLayoutId = layoutId;
107         mTag = tag;
108     }
109 
refreshDirectory(int anim)110     protected abstract void refreshDirectory(int anim);
111     /** Allows sub-classes to include information in a newly created State instance. */
includeState(State initialState)112     protected abstract void includeState(State initialState);
onDirectoryCreated(DocumentInfo doc)113     protected abstract void onDirectoryCreated(DocumentInfo doc);
114 
getInjector()115     public abstract Injector<?> getInjector();
116 
117     @CallSuper
118     @Override
onCreate(Bundle icicle)119     public void onCreate(Bundle icicle) {
120         // Record the time when onCreate is invoked for metric.
121         mStartTime = new Date().getTime();
122 
123         super.onCreate(icicle);
124 
125         final Intent intent = getIntent();
126 
127         addListenerForLaunchCompletion();
128 
129         setContentView(mLayoutId);
130 
131         mInjector = getInjector();
132         mState = getState(icicle);
133         mDrawer = DrawerController.create(this, mInjector.config);
134         Metrics.logActivityLaunch(this, mState, intent);
135 
136         // we're really interested in retainining state in our very complex
137         // DirectoryFragment. So we do a little code yoga to extend
138         // support to that fragment.
139         mRetainedState = (RetainedState) getLastNonConfigurationInstance();
140         mProviders = DocumentsApplication.getProvidersCache(this);
141         mDocs = DocumentsAccess.create(this);
142 
143         Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
144         setActionBar(toolbar);
145 
146         Breadcrumb breadcrumb =
147                 Shared.findView(this, R.id.dropdown_breadcrumb, R.id.horizontal_breadcrumb);
148         assert(breadcrumb != null);
149 
150         mNavigator = new NavigationViewManager(mDrawer, toolbar, mState, this, breadcrumb);
151         SearchManagerListener searchListener = new SearchManagerListener() {
152             /**
153              * Called when search results changed. Refreshes the content of the directory. It
154              * doesn't refresh elements on the action bar. e.g. The current directory name displayed
155              * on the action bar won't get updated.
156              */
157             @Override
158             public void onSearchChanged(@Nullable String query) {
159                 if (query != null) {
160                     Metrics.logUserAction(BaseActivity.this, Metrics.USER_ACTION_SEARCH);
161                 }
162 
163                 mInjector.actions.loadDocumentsForCurrentStack();
164             }
165 
166             @Override
167             public void onSearchFinished() {
168                 // Restores menu icons state
169                 invalidateOptionsMenu();
170             }
171 
172             @Override
173             public void onSearchViewChanged(boolean opened) {
174                 mNavigator.update();
175             }
176         };
177 
178         // "Commands" are meta input for controlling system behavior.
179         // We piggy back on search input as it is the only text input
180         // area in the app. But the functionality is independent
181         // of "regular" search query processing.
182         final CommandInterceptor cmdInterceptor = new CommandInterceptor(mInjector.features);
183         cmdInterceptor.add(new CommandInterceptor.DumpRootsCacheHandler(this));
184 
185         // A tiny decorator that adds support for enabling CommandInterceptor
186         // based on query input. It's sorta like CommandInterceptor, but its metaaahhh.
187         EventHandler<String> queryInterceptor =
188                 CommandInterceptor.createDebugModeFlipper(
189                         mInjector.features,
190                         mInjector.debugHelper::toggleDebugMode,
191                         cmdInterceptor);
192         mSearchManager = new SearchViewManager(searchListener, queryInterceptor, icicle);
193         mSortController = SortController.create(this, mState.derivedMode, mState.sortModel);
194 
195         mPreferencesMonitor = new PreferencesMonitor(
196                 getApplicationContext().getPackageName(),
197                 PreferenceManager.getDefaultSharedPreferences(this),
198                 this::onPreferenceChanged);
199         mPreferencesMonitor.start();
200 
201         // Base classes must update result in their onCreate.
202         setResult(Activity.RESULT_CANCELED);
203     }
204 
onPreferenceChanged(String pref)205     public void onPreferenceChanged(String pref) {
206         // For now, we only work with prefs that we backup. This
207         // just limits the scope of what we expect to come flowing
208         // through here until we know we want more and fancier options.
209         assert(Preferences.shouldBackup(pref));
210 
211         switch (pref) {
212             case ScopedPreferences.INCLUDE_DEVICE_ROOT:
213                 updateDisplayAdvancedDevices(mInjector.prefs.getShowDeviceRoot());
214         }
215     }
216 
217     @Override
onPostCreate(Bundle savedInstanceState)218     protected void onPostCreate(Bundle savedInstanceState) {
219         super.onPostCreate(savedInstanceState);
220 
221         mRootsMonitor = new RootsMonitor<>(
222                 this,
223                 mInjector.actions,
224                 mProviders,
225                 mDocs,
226                 mState,
227                 mSearchManager,
228                 mInjector.actionModeController::finishActionMode);
229         mRootsMonitor.start();
230     }
231 
232     @Override
onCreateOptionsMenu(Menu menu)233     public boolean onCreateOptionsMenu(Menu menu) {
234         boolean showMenu = super.onCreateOptionsMenu(menu);
235 
236         getMenuInflater().inflate(R.menu.activity, menu);
237         mNavigator.update();
238         boolean fullBarSearch = getResources().getBoolean(R.bool.full_bar_search_view);
239         mSearchManager.install(menu, fullBarSearch);
240 
241         return showMenu;
242     }
243 
244     @Override
245     @CallSuper
onPrepareOptionsMenu(Menu menu)246     public boolean onPrepareOptionsMenu(Menu menu) {
247         super.onPrepareOptionsMenu(menu);
248         mSearchManager.showMenu(mState.stack);
249         return true;
250     }
251 
252     @Override
onDestroy()253     protected void onDestroy() {
254         mRootsMonitor.stop();
255         mPreferencesMonitor.stop();
256         mSortController.destroy();
257         super.onDestroy();
258     }
259 
getState(@ullable Bundle icicle)260     private State getState(@Nullable Bundle icicle) {
261         if (icicle != null) {
262             State state = icicle.<State>getParcelable(Shared.EXTRA_STATE);
263             if (DEBUG) Log.d(mTag, "Recovered existing state object: " + state);
264             return state;
265         }
266 
267         State state = new State();
268 
269         final Intent intent = getIntent();
270 
271         state.sortModel = SortModel.createModel();
272         state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
273         state.excludedAuthorities = getExcludedAuthorities();
274 
275         includeState(state);
276 
277         state.showAdvanced = Shared.mustShowDeviceRoot(intent)
278                 || mInjector.prefs.getShowDeviceRoot();
279 
280         // Only show the toggle if advanced isn't forced enabled.
281         state.showDeviceStorageOption = !Shared.mustShowDeviceRoot(intent);
282 
283         if (DEBUG) Log.d(mTag, "Created new state object: " + state);
284 
285         return state;
286     }
287 
288     @Override
setRootsDrawerOpen(boolean open)289     public void setRootsDrawerOpen(boolean open) {
290         mNavigator.revealRootsDrawer(open);
291     }
292 
293     @Override
onRootPicked(RootInfo root)294     public void onRootPicked(RootInfo root) {
295         // Clicking on the current root removes search
296         mSearchManager.cancelSearch();
297 
298         // Skip refreshing if root nor directory didn't change
299         if (root.equals(getCurrentRoot()) && mState.stack.size() == 1) {
300             return;
301         }
302 
303         mInjector.actionModeController.finishActionMode();
304         mSortController.onViewModeChanged(mState.derivedMode);
305 
306         // Set summary header's visibility. Only recents and downloads root may have summary in
307         // their docs.
308         mState.sortModel.setDimensionVisibility(
309                 SortModel.SORT_DIMENSION_ID_SUMMARY,
310                 root.isRecents() || root.isDownloads() ? View.VISIBLE : View.INVISIBLE);
311 
312         // Clear entire backstack and start in new root
313         mState.stack.changeRoot(root);
314 
315         // Recents is always in memory, so we just load it directly.
316         // Otherwise we delegate loading data from disk to a task
317         // to ensure a responsive ui.
318         if (mProviders.isRecentsRoot(root)) {
319             refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
320         } else {
321             mInjector.actions.getRootDocument(
322                     root,
323                     TimeoutTask.DEFAULT_TIMEOUT,
324                     doc -> mInjector.actions.openRootDocument(doc));
325         }
326     }
327 
328     @Override
onOptionsItemSelected(MenuItem item)329     public boolean onOptionsItemSelected(MenuItem item) {
330 
331         switch (item.getItemId()) {
332             case android.R.id.home:
333                 onBackPressed();
334                 return true;
335 
336             case R.id.option_menu_create_dir:
337                 getInjector().actions.showCreateDirectoryDialog();
338                 return true;
339 
340             case R.id.option_menu_search:
341                 // SearchViewManager listens for this directly.
342                 return false;
343 
344             case R.id.option_menu_grid:
345                 setViewMode(State.MODE_GRID);
346                 return true;
347 
348             case R.id.option_menu_list:
349                 setViewMode(State.MODE_LIST);
350                 return true;
351 
352             case R.id.option_menu_advanced:
353                 onDisplayAdvancedDevices();
354                 return true;
355 
356             case R.id.option_menu_select_all:
357                 getInjector().actions.selectAllFiles();
358                 return true;
359 
360             case R.id.option_menu_debug:
361                 getInjector().actions.showDebugMessage();
362                 return true;
363 
364             default:
365                 return super.onOptionsItemSelected(item);
366         }
367     }
368 
getDirectoryFragment()369     protected final @Nullable DirectoryFragment getDirectoryFragment() {
370         return DirectoryFragment.get(getFragmentManager());
371     }
372 
373     /**
374      * Returns true if a directory can be created in the current location.
375      * @return
376      */
canCreateDirectory()377     protected boolean canCreateDirectory() {
378         final RootInfo root = getCurrentRoot();
379         final DocumentInfo cwd = getCurrentDirectory();
380         return cwd != null
381                 && cwd.isCreateSupported()
382                 && !mSearchManager.isSearching()
383                 && !root.isRecents();
384     }
385 
386     // TODO: make navigator listen to state
387     @Override
updateNavigator()388     public final void updateNavigator() {
389         mNavigator.update();
390     }
391 
392     @Override
restoreRootAndDirectory()393     public void restoreRootAndDirectory() {
394         // We're trying to restore stuff in document stack from saved instance. If we didn't have a
395         // chance to spawn a fragment before we need to do it now. However if we spawned a fragment
396         // already, system will automatically restore the fragment for us so we don't need to do
397         // that manually this time.
398         if (DirectoryFragment.get(getFragmentManager()) == null) {
399             refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
400         }
401     }
402 
403     /**
404      * Refreshes the content of the director and the menu/action bar.
405      * The current directory name and selection will get updated.
406      * @param anim
407      */
408     @Override
refreshCurrentRootAndDirectory(int anim)409     public final void refreshCurrentRootAndDirectory(int anim) {
410         // The following call will crash if it's called before onCreateOptionMenu() is called in
411         // which we install menu item to search view manager, and there is a search query we need to
412         // restore. This happens when we're still initializing our UI so we shouldn't cancel the
413         // search which will be restored later in onCreateOptionMenu(). Try finding a way to guard
414         // refreshCurrentRootAndDirectory() from being called while we're restoring the state of UI
415         // from the saved state passed in onCreate().
416         mSearchManager.cancelSearch();
417 
418         mState.derivedMode = LocalPreferences.getViewMode(this, mState.stack.getRoot(), MODE_GRID);
419 
420         refreshDirectory(anim);
421 
422         final RootsFragment roots = RootsFragment.get(getFragmentManager());
423         if (roots != null) {
424             roots.onCurrentRootChanged();
425         }
426 
427         mNavigator.update();
428 
429         // Causes talkback to announce the activity's new title
430         setTitle(mState.stack.getTitle());
431 
432         invalidateOptionsMenu();
433     }
434 
getExcludedAuthorities()435     private final List<String> getExcludedAuthorities() {
436         List<String> authorities = new ArrayList<>();
437         if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) {
438             // Exclude roots provided by the calling package.
439             String packageName = Shared.getCallingPackageName(this);
440             try {
441                 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
442                         PackageManager.GET_PROVIDERS);
443                 for (ProviderInfo provider: pkgInfo.providers) {
444                     authorities.add(provider.authority);
445                 }
446             } catch (PackageManager.NameNotFoundException e) {
447                 Log.e(mTag, "Calling package name does not resolve: " + packageName);
448             }
449         }
450         return authorities;
451     }
452 
get(Fragment fragment)453     public static BaseActivity get(Fragment fragment) {
454         return (BaseActivity) fragment.getActivity();
455     }
456 
getDisplayState()457     public State getDisplayState() {
458         return mState;
459     }
460 
461     /**
462      * Set internal storage visible based on explicit user action.
463      */
onDisplayAdvancedDevices()464     private void onDisplayAdvancedDevices() {
465         boolean display = !mState.showAdvanced;
466         Metrics.logUserAction(this,
467                 display ? Metrics.USER_ACTION_SHOW_ADVANCED : Metrics.USER_ACTION_HIDE_ADVANCED);
468 
469         mInjector.prefs.setShowDeviceRoot(display);
470         updateDisplayAdvancedDevices(display);
471     }
472 
updateDisplayAdvancedDevices(boolean display)473     private void updateDisplayAdvancedDevices(boolean display) {
474         mState.showAdvanced = display;
475         @Nullable RootsFragment fragment = RootsFragment.get(getFragmentManager());
476         if (fragment != null) {
477             // This also takes care of updating launcher shortcuts (which are roots :)
478             fragment.onDisplayStateChanged();
479         }
480         invalidateOptionsMenu();
481     }
482 
483     /**
484      * Set mode based on explicit user action.
485      */
setViewMode(@iewMode int mode)486     void setViewMode(@ViewMode int mode) {
487         if (mode == State.MODE_GRID) {
488             Metrics.logUserAction(this, Metrics.USER_ACTION_GRID);
489         } else if (mode == State.MODE_LIST) {
490             Metrics.logUserAction(this, Metrics.USER_ACTION_LIST);
491         }
492 
493         LocalPreferences.setViewMode(this, getCurrentRoot(), mode);
494         mState.derivedMode = mode;
495 
496         // view icon needs to be updated, but we *could* do it
497         // in onOptionsItemSelected, and not do the full invalidation
498         // But! That's a larger refactoring we'll save for another day.
499         invalidateOptionsMenu();
500         DirectoryFragment dir = getDirectoryFragment();
501         if (dir != null) {
502             dir.onViewModeChanged();
503         }
504 
505         mSortController.onViewModeChanged(mode);
506     }
507 
setPending(boolean pending)508     public void setPending(boolean pending) {
509         // TODO: Isolate this behavior to PickActivity.
510     }
511 
512     @Override
onSaveInstanceState(Bundle state)513     protected void onSaveInstanceState(Bundle state) {
514         super.onSaveInstanceState(state);
515         state.putParcelable(Shared.EXTRA_STATE, mState);
516         mSearchManager.onSaveInstanceState(state);
517     }
518 
519     @Override
onRestoreInstanceState(Bundle state)520     protected void onRestoreInstanceState(Bundle state) {
521         super.onRestoreInstanceState(state);
522     }
523 
524     /**
525      * Delegate ths call to the current fragment so it can save selection.
526      * Feel free to expand on this with other useful state.
527      */
528     @Override
onRetainNonConfigurationInstance()529     public RetainedState onRetainNonConfigurationInstance() {
530         RetainedState retained = new RetainedState();
531         DirectoryFragment fragment = DirectoryFragment.get(getFragmentManager());
532         if (fragment != null) {
533             fragment.retainState(retained);
534         }
535         return retained;
536     }
537 
getRetainedState()538     public @Nullable RetainedState getRetainedState() {
539         return mRetainedState;
540     }
541 
542     @Override
isSearchExpanded()543     public boolean isSearchExpanded() {
544         return mSearchManager.isExpanded();
545     }
546 
547     @Override
getCurrentRoot()548     public RootInfo getCurrentRoot() {
549         RootInfo root = mState.stack.getRoot();
550         if (root != null) {
551             return root;
552         } else {
553             return mProviders.getRecentsRoot();
554         }
555     }
556 
557     @Override
getCurrentDirectory()558     public DocumentInfo getCurrentDirectory() {
559         return mState.stack.peek();
560     }
561 
562     @VisibleForTesting
addEventListener(EventListener listener)563     public void addEventListener(EventListener listener) {
564         mEventListeners.add(listener);
565     }
566 
567     @VisibleForTesting
removeEventListener(EventListener listener)568     public void removeEventListener(EventListener listener) {
569         mEventListeners.remove(listener);
570     }
571 
572     @VisibleForTesting
notifyDirectoryLoaded(Uri uri)573     public void notifyDirectoryLoaded(Uri uri) {
574         for (EventListener listener : mEventListeners) {
575             listener.onDirectoryLoaded(uri);
576         }
577     }
578 
579     @VisibleForTesting
580     @Override
notifyDirectoryNavigated(Uri uri)581     public void notifyDirectoryNavigated(Uri uri) {
582         for (EventListener listener : mEventListeners) {
583             listener.onDirectoryNavigated(uri);
584         }
585     }
586 
587     @Override
dispatchKeyEvent(KeyEvent event)588     public boolean dispatchKeyEvent(KeyEvent event) {
589         if (event.getAction() == KeyEvent.ACTION_DOWN) {
590             mInjector.debugHelper.debugCheck(event.getDownTime(), event.getKeyCode());
591         }
592 
593         DocumentsApplication.getDragAndDropManager(this).onKeyEvent(event);
594 
595         return super.dispatchKeyEvent(event);
596     }
597 
598     @Override
onActivityResult(int requestCode, int resultCode, Intent data)599     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
600         mInjector.actions.onActivityResult(requestCode, resultCode, data);
601     }
602 
603     /**
604      * Pops the top entry off the directory stack, and returns the user to the previous directory.
605      * If the directory stack only contains one item, this method does nothing.
606      *
607      * @return Whether the stack was popped.
608      */
popDir()609     protected boolean popDir() {
610         if (mState.stack.size() > 1) {
611             mState.stack.pop();
612             refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
613             return true;
614         }
615         return false;
616     }
617 
focusSidebar()618     protected boolean focusSidebar() {
619         RootsFragment rf = RootsFragment.get(getFragmentManager());
620         assert (rf != null);
621         return rf.requestFocus();
622     }
623 
624     /**
625      * Closes the activity when it's idle.
626      */
addListenerForLaunchCompletion()627     private void addListenerForLaunchCompletion() {
628         addEventListener(new EventListener() {
629             @Override
630             public void onDirectoryNavigated(Uri uri) {
631             }
632 
633             @Override
634             public void onDirectoryLoaded(Uri uri) {
635                 removeEventListener(this);
636                 getMainLooper().getQueue().addIdleHandler(new IdleHandler() {
637                     @Override
638                     public boolean queueIdle() {
639                         // If startup benchmark is requested by a whitelisted testing package, then
640                         // close the activity once idle, and notify the testing activity.
641                         if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) &&
642                                 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) {
643                             setResult(RESULT_OK);
644                             finish();
645                         }
646 
647                         Metrics.logStartupMs(
648                                 BaseActivity.this, (int) (new Date().getTime() - mStartTime));
649 
650                         // Remove the idle handler.
651                         return false;
652                     }
653                 });
654             }
655         });
656     }
657 
658     public static final class RetainedState {
659         public @Nullable Selection selection;
660 
hasSelection()661         public boolean hasSelection() {
662             return selection != null;
663         }
664     }
665 
666     @VisibleForTesting
667     protected interface EventListener {
668         /**
669          * @param uri Uri navigated to. If recents, then null.
670          */
onDirectoryNavigated(@ullable Uri uri)671         void onDirectoryNavigated(@Nullable Uri uri);
672 
673         /**
674          * @param uri Uri of the loaded directory. If recents, then null.
675          */
onDirectoryLoaded(@ullable Uri uri)676         void onDirectoryLoaded(@Nullable Uri uri);
677     }
678 }
679