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