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.Shared.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         // Causes talkback to announce the activity's new title
    429         if (mState.stack.isRecents()) {
    430             setTitle(mProviders.getRecentsRoot().title);
    431         } else {
    432             setTitle(mState.stack.getTitle());
    433         }
    434         invalidateOptionsMenu();
    435     }
    436 
    437     private final List<String> getExcludedAuthorities() {
    438         List<String> authorities = new ArrayList<>();
    439         if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) {
    440             // Exclude roots provided by the calling package.
    441             String packageName = Shared.getCallingPackageName(this);
    442             try {
    443                 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
    444                         PackageManager.GET_PROVIDERS);
    445                 for (ProviderInfo provider: pkgInfo.providers) {
    446                     authorities.add(provider.authority);
    447                 }
    448             } catch (PackageManager.NameNotFoundException e) {
    449                 Log.e(mTag, "Calling package name does not resolve: " + packageName);
    450             }
    451         }
    452         return authorities;
    453     }
    454 
    455     public static BaseActivity get(Fragment fragment) {
    456         return (BaseActivity) fragment.getActivity();
    457     }
    458 
    459     public State getDisplayState() {
    460         return mState;
    461     }
    462 
    463     /**
    464      * Set internal storage visible based on explicit user action.
    465      */
    466     private void onDisplayAdvancedDevices() {
    467         boolean display = !mState.showAdvanced;
    468         Metrics.logUserAction(this,
    469                 display ? Metrics.USER_ACTION_SHOW_ADVANCED : Metrics.USER_ACTION_HIDE_ADVANCED);
    470 
    471         mInjector.prefs.setShowDeviceRoot(display);
    472         updateDisplayAdvancedDevices(display);
    473     }
    474 
    475     private void updateDisplayAdvancedDevices(boolean display) {
    476         mState.showAdvanced = display;
    477         @Nullable RootsFragment fragment = RootsFragment.get(getFragmentManager());
    478         if (fragment != null) {
    479             // This also takes care of updating launcher shortcuts (which are roots :)
    480             fragment.onDisplayStateChanged();
    481         }
    482         invalidateOptionsMenu();
    483     }
    484 
    485     /**
    486      * Set mode based on explicit user action.
    487      */
    488     void setViewMode(@ViewMode int mode) {
    489         if (mode == State.MODE_GRID) {
    490             Metrics.logUserAction(this, Metrics.USER_ACTION_GRID);
    491         } else if (mode == State.MODE_LIST) {
    492             Metrics.logUserAction(this, Metrics.USER_ACTION_LIST);
    493         }
    494 
    495         LocalPreferences.setViewMode(this, getCurrentRoot(), mode);
    496         mState.derivedMode = mode;
    497 
    498         // view icon needs to be updated, but we *could* do it
    499         // in onOptionsItemSelected, and not do the full invalidation
    500         // But! That's a larger refactoring we'll save for another day.
    501         invalidateOptionsMenu();
    502         DirectoryFragment dir = getDirectoryFragment();
    503         if (dir != null) {
    504             dir.onViewModeChanged();
    505         }
    506 
    507         mSortController.onViewModeChanged(mode);
    508     }
    509 
    510     public void setPending(boolean pending) {
    511         // TODO: Isolate this behavior to PickActivity.
    512     }
    513 
    514     @Override
    515     protected void onSaveInstanceState(Bundle state) {
    516         super.onSaveInstanceState(state);
    517         state.putParcelable(Shared.EXTRA_STATE, mState);
    518         mSearchManager.onSaveInstanceState(state);
    519     }
    520 
    521     @Override
    522     protected void onRestoreInstanceState(Bundle state) {
    523         super.onRestoreInstanceState(state);
    524     }
    525 
    526     /**
    527      * Delegate ths call to the current fragment so it can save selection.
    528      * Feel free to expand on this with other useful state.
    529      */
    530     @Override
    531     public RetainedState onRetainNonConfigurationInstance() {
    532         RetainedState retained = new RetainedState();
    533         DirectoryFragment fragment = DirectoryFragment.get(getFragmentManager());
    534         if (fragment != null) {
    535             fragment.retainState(retained);
    536         }
    537         return retained;
    538     }
    539 
    540     public @Nullable RetainedState getRetainedState() {
    541         return mRetainedState;
    542     }
    543 
    544     @Override
    545     public boolean isSearchExpanded() {
    546         return mSearchManager.isExpanded();
    547     }
    548 
    549     @Override
    550     public RootInfo getCurrentRoot() {
    551         RootInfo root = mState.stack.getRoot();
    552         if (root != null) {
    553             return root;
    554         } else {
    555             return mProviders.getRecentsRoot();
    556         }
    557     }
    558 
    559     @Override
    560     public DocumentInfo getCurrentDirectory() {
    561         return mState.stack.peek();
    562     }
    563 
    564     @VisibleForTesting
    565     public void addEventListener(EventListener listener) {
    566         mEventListeners.add(listener);
    567     }
    568 
    569     @VisibleForTesting
    570     public void removeEventListener(EventListener listener) {
    571         mEventListeners.remove(listener);
    572     }
    573 
    574     @VisibleForTesting
    575     public void notifyDirectoryLoaded(Uri uri) {
    576         for (EventListener listener : mEventListeners) {
    577             listener.onDirectoryLoaded(uri);
    578         }
    579     }
    580 
    581     @VisibleForTesting
    582     @Override
    583     public void notifyDirectoryNavigated(Uri uri) {
    584         for (EventListener listener : mEventListeners) {
    585             listener.onDirectoryNavigated(uri);
    586         }
    587     }
    588 
    589     @Override
    590     public boolean dispatchKeyEvent(KeyEvent event) {
    591         if (event.getAction() == KeyEvent.ACTION_DOWN) {
    592             mInjector.debugHelper.debugCheck(event.getDownTime(), event.getKeyCode());
    593         }
    594 
    595         DocumentsApplication.getDragAndDropManager(this).onKeyEvent(event);
    596 
    597         return super.dispatchKeyEvent(event);
    598     }
    599 
    600     @Override
    601     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    602         mInjector.actions.onActivityResult(requestCode, resultCode, data);
    603     }
    604 
    605     /**
    606      * Pops the top entry off the directory stack, and returns the user to the previous directory.
    607      * If the directory stack only contains one item, this method does nothing.
    608      *
    609      * @return Whether the stack was popped.
    610      */
    611     protected boolean popDir() {
    612         if (mState.stack.size() > 1) {
    613             mState.stack.pop();
    614             refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
    615             return true;
    616         }
    617         return false;
    618     }
    619 
    620     protected boolean focusSidebar() {
    621         RootsFragment rf = RootsFragment.get(getFragmentManager());
    622         assert (rf != null);
    623         return rf.requestFocus();
    624     }
    625 
    626     /**
    627      * Closes the activity when it's idle.
    628      */
    629     private void addListenerForLaunchCompletion() {
    630         addEventListener(new EventListener() {
    631             @Override
    632             public void onDirectoryNavigated(Uri uri) {
    633             }
    634 
    635             @Override
    636             public void onDirectoryLoaded(Uri uri) {
    637                 removeEventListener(this);
    638                 getMainLooper().getQueue().addIdleHandler(new IdleHandler() {
    639                     @Override
    640                     public boolean queueIdle() {
    641                         // If startup benchmark is requested by a whitelisted testing package, then
    642                         // close the activity once idle, and notify the testing activity.
    643                         if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) &&
    644                                 BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) {
    645                             setResult(RESULT_OK);
    646                             finish();
    647                         }
    648 
    649                         Metrics.logStartupMs(
    650                                 BaseActivity.this, (int) (new Date().getTime() - mStartTime));
    651 
    652                         // Remove the idle handler.
    653                         return false;
    654                     }
    655                 });
    656             }
    657         });
    658     }
    659 
    660     public static final class RetainedState {
    661         public @Nullable Selection selection;
    662 
    663         public boolean hasSelection() {
    664             return selection != null;
    665         }
    666     }
    667 
    668     @VisibleForTesting
    669     protected interface EventListener {
    670         /**
    671          * @param uri Uri navigated to. If recents, then null.
    672          */
    673         void onDirectoryNavigated(@Nullable Uri uri);
    674 
    675         /**
    676          * @param uri Uri of the loaded directory. If recents, then null.
    677          */
    678         void onDirectoryLoaded(@Nullable Uri uri);
    679     }
    680 }
    681