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