Home | History | Annotate | Download | only in documentsui
      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 
    105     public BaseActivity(@LayoutRes int layoutId, String tag) {
    106         mLayoutId = layoutId;
    107         mTag = tag;
    108     }
    109 
    110     protected abstract void refreshDirectory(int anim);
    111     /** Allows sub-classes to include information in a newly created State instance. */
    112     protected abstract void includeState(State initialState);
    113     protected abstract void onDirectoryCreated(DocumentInfo doc);
    114 
    115     public abstract Injector<?> getInjector();
    116 
    117     @CallSuper
    118     @Override
    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 
    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
    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
    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
    246     public boolean onPrepareOptionsMenu(Menu menu) {
    247         super.onPrepareOptionsMenu(menu);
    248         mSearchManager.showMenu(mState.stack);
    249         return true;
    250     }
    251 
    252     @Override
    253     protected void onDestroy() {
    254         mRootsMonitor.stop();
    255         mPreferencesMonitor.stop();
    256         mSortController.destroy();
    257         super.onDestroy();
    258     }
    259 
    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
    289     public void setRootsDrawerOpen(boolean open) {
    290         mNavigator.revealRootsDrawer(open);
    291     }
    292 
    293     @Override
    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
    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 
    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      */
    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
    388     public final void updateNavigator() {
    389         mNavigator.update();
    390     }
    391 
    392     @Override
    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
    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 
    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 
    453     public static BaseActivity get(Fragment fragment) {
    454         return (BaseActivity) fragment.getActivity();
    455     }
    456 
    457     public State getDisplayState() {
    458         return mState;
    459     }
    460 
    461     /**
    462      * Set internal storage visible based on explicit user action.
    463      */
    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 
    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      */
    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 
    508     public void setPending(boolean pending) {
    509         // TODO: Isolate this behavior to PickActivity.
    510     }
    511 
    512     @Override
    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
    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
    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 
    538     public @Nullable RetainedState getRetainedState() {
    539         return mRetainedState;
    540     }
    541 
    542     @Override
    543     public boolean isSearchExpanded() {
    544         return mSearchManager.isExpanded();
    545     }
    546 
    547     @Override
    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
    558     public DocumentInfo getCurrentDirectory() {
    559         return mState.stack.peek();
    560     }
    561 
    562     @VisibleForTesting
    563     public void addEventListener(EventListener listener) {
    564         mEventListeners.add(listener);
    565     }
    566 
    567     @VisibleForTesting
    568     public void removeEventListener(EventListener listener) {
    569         mEventListeners.remove(listener);
    570     }
    571 
    572     @VisibleForTesting
    573     public void notifyDirectoryLoaded(Uri uri) {
    574         for (EventListener listener : mEventListeners) {
    575             listener.onDirectoryLoaded(uri);
    576         }
    577     }
    578 
    579     @VisibleForTesting
    580     @Override
    581     public void notifyDirectoryNavigated(Uri uri) {
    582         for (EventListener listener : mEventListeners) {
    583             listener.onDirectoryNavigated(uri);
    584         }
    585     }
    586 
    587     @Override
    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
    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      */
    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 
    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      */
    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 
    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          */
    671         void onDirectoryNavigated(@Nullable Uri uri);
    672 
    673         /**
    674          * @param uri Uri of the loaded directory. If recents, then null.
    675          */
    676         void onDirectoryLoaded(@Nullable Uri uri);
    677     }
    678 }
    679