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