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 BaseActivity(@ayoutRes int layoutId, String tag)104 public BaseActivity(@LayoutRes int layoutId, String tag) { 105 mLayoutId = layoutId; 106 mTag = tag; 107 } 108 refreshDirectory(int anim)109 protected abstract void refreshDirectory(int anim); 110 /** Allows sub-classes to include information in a newly created State instance. */ includeState(State initialState)111 protected abstract void includeState(State initialState); onDirectoryCreated(DocumentInfo doc)112 protected abstract void onDirectoryCreated(DocumentInfo doc); 113 getInjector()114 public abstract Injector<?> getInjector(); 115 116 @CallSuper 117 @Override onCreate(Bundle icicle)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 onPreferenceChanged(String pref)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 onPostCreate(Bundle savedInstanceState)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 onCreateOptionsMenu(Menu menu)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 onPrepareOptionsMenu(Menu menu)237 public boolean onPrepareOptionsMenu(Menu menu) { 238 super.onPrepareOptionsMenu(menu); 239 mSearchManager.showMenu(mState.stack); 240 return true; 241 } 242 243 @Override onDestroy()244 protected void onDestroy() { 245 mRootsMonitor.stop(); 246 mPreferencesMonitor.stop(); 247 mSortController.destroy(); 248 super.onDestroy(); 249 } 250 getState(@ullable Bundle icicle)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 setRootsDrawerOpen(boolean open)280 public void setRootsDrawerOpen(boolean open) { 281 mNavigator.revealRootsDrawer(open); 282 } 283 284 @Override onRootPicked(RootInfo root)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 onOptionsItemSelected(MenuItem item)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 getDirectoryFragment()361 protected final @Nullable DirectoryFragment getDirectoryFragment() { 362 return DirectoryFragment.get(getFragmentManager()); 363 } 364 showCreateDirectoryDialog()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 */ canCreateDirectory()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 updateNavigator()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 refreshCurrentRootAndDirectory(int anim)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 getExcludedAuthorities()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 get(Fragment fragment)440 public static BaseActivity get(Fragment fragment) { 441 return (BaseActivity) fragment.getActivity(); 442 } 443 getDisplayState()444 public State getDisplayState() { 445 return mState; 446 } 447 getShadowBuilder()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 */ onDisplayAdvancedDevices()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 updateDisplayAdvancedDevices(boolean display)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 */ setViewMode(@iewMode int mode)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 setPending(boolean pending)499 public void setPending(boolean pending) { 500 // TODO: Isolate this behavior to PickActivity. 501 } 502 503 @Override onSaveInstanceState(Bundle state)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 onRestoreInstanceState(Bundle state)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 onRetainNonConfigurationInstance()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 getRetainedState()529 public @Nullable RetainedState getRetainedState() { 530 return mRetainedState; 531 } 532 533 @Override isSearchExpanded()534 public boolean isSearchExpanded() { 535 return mSearchManager.isExpanded(); 536 } 537 538 @Override getCurrentRoot()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 getCurrentDirectory()549 public DocumentInfo getCurrentDirectory() { 550 return mState.stack.peek(); 551 } 552 553 @VisibleForTesting addEventListener(EventListener listener)554 public void addEventListener(EventListener listener) { 555 mEventListeners.add(listener); 556 } 557 558 @VisibleForTesting removeEventListener(EventListener listener)559 public void removeEventListener(EventListener listener) { 560 mEventListeners.remove(listener); 561 } 562 563 @VisibleForTesting notifyDirectoryLoaded(Uri uri)564 public void notifyDirectoryLoaded(Uri uri) { 565 for (EventListener listener : mEventListeners) { 566 listener.onDirectoryLoaded(uri); 567 } 568 } 569 570 @VisibleForTesting 571 @Override notifyDirectoryNavigated(Uri uri)572 public void notifyDirectoryNavigated(Uri uri) { 573 for (EventListener listener : mEventListeners) { 574 listener.onDirectoryNavigated(uri); 575 } 576 } 577 578 @Override dispatchKeyEvent(KeyEvent event)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 onActivityResult(int requestCode, int resultCode, Intent data)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 */ popDir()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 focusSidebar()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 */ addListenerForLaunchCompletion()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 hasSelection()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 */ onDirectoryNavigated(@ullable Uri uri)659 void onDirectoryNavigated(@Nullable Uri uri); 660 661 /** 662 * @param uri Uri of the loaded directory. If recents, then null. 663 */ onDirectoryLoaded(@ullable Uri uri)664 void onDirectoryLoaded(@Nullable Uri uri); 665 } 666 } 667