blob: 9954e9d0ee831d429856d0ca6b8ab0a465884285 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui;
import static com.android.documentsui.base.Shared.EXTRA_BENCHMARK;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
import static com.android.documentsui.base.State.MODE_GRID;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.MessageQueue.IdleHandler;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Checkable;
import android.widget.TextView;
import androidx.annotation.CallSuper;
import androidx.annotation.LayoutRes;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.ActionMenuView;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import com.android.documentsui.AbstractActionHandler.CommonAddons;
import com.android.documentsui.Injector.Injected;
import com.android.documentsui.NavigationViewManager.Breadcrumb;
import com.android.documentsui.R;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
import com.android.documentsui.base.State.ViewMode;
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.dirlist.AppsRowManager;
import com.android.documentsui.dirlist.DirectoryFragment;
import com.android.documentsui.prefs.LocalPreferences;
import com.android.documentsui.prefs.Preferences;
import com.android.documentsui.prefs.PreferencesMonitor;
import com.android.documentsui.prefs.ScopedPreferences;
import com.android.documentsui.queries.CommandInterceptor;
import com.android.documentsui.queries.SearchChipData;
import com.android.documentsui.queries.SearchFragment;
import com.android.documentsui.queries.SearchViewManager;
import com.android.documentsui.queries.SearchViewManager.SearchManagerListener;
import com.android.documentsui.roots.ProvidersCache;
import com.android.documentsui.sidebar.RootsFragment;
import com.android.documentsui.sorting.SortController;
import com.android.documentsui.sorting.SortModel;
import com.google.android.material.appbar.AppBarLayout;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.annotation.Nullable;
public abstract class BaseActivity
extends AppCompatActivity implements CommonAddons, NavigationViewManager.Environment {
private static final String BENCHMARK_TESTING_PACKAGE = "com.android.documentsui.appperftests";
protected SearchViewManager mSearchManager;
protected AppsRowManager mAppsRowManager;
protected State mState;
@Injected
protected Injector<?> mInjector;
protected ProvidersCache mProviders;
protected DocumentsAccess mDocs;
protected DrawerController mDrawer;
protected NavigationViewManager mNavigator;
protected SortController mSortController;
private final List<EventListener> mEventListeners = new ArrayList<>();
private final String mTag;
@LayoutRes
private int mLayoutId;
private RootsMonitor<BaseActivity> mRootsMonitor;
private long mStartTime;
private boolean mHasQueryContentFromIntent;
private PreferencesMonitor mPreferencesMonitor;
public BaseActivity(@LayoutRes int layoutId, String tag) {
mLayoutId = layoutId;
mTag = tag;
}
protected abstract void refreshDirectory(int anim);
/** Allows sub-classes to include information in a newly created State instance. */
protected abstract void includeState(State initialState);
protected abstract void onDirectoryCreated(DocumentInfo doc);
public abstract Injector<?> getInjector();
@CallSuper
@Override
public void onCreate(Bundle icicle) {
// Record the time when onCreate is invoked for metric.
mStartTime = new Date().getTime();
// ToDo Create tool to check resource version before applyStyle for the theme
// If version code is not match, we should reset overlay package to default,
// in case Activity continueusly encounter resource not found exception
getTheme().applyStyle(R.style.DocumentsDefaultTheme, false);
super.onCreate(icicle);
final Intent intent = getIntent();
addListenerForLaunchCompletion();
setContentView(mLayoutId);
setContainer();
mInjector = getInjector();
mState = getState(icicle);
mDrawer = DrawerController.create(this, mInjector.config);
Metrics.logActivityLaunch(mState, intent);
mProviders = DocumentsApplication.getProvidersCache(this);
mDocs = DocumentsAccess.create(this);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
Breadcrumb breadcrumb =
Shared.findView(this, R.id.dropdown_breadcrumb, R.id.horizontal_breadcrumb);
assert(breadcrumb != null);
mNavigator = new NavigationViewManager(this, mDrawer, mState, this, breadcrumb);
SearchManagerListener searchListener = new SearchManagerListener() {
/**
* Called when search results changed. Refreshes the content of the directory. It
* doesn't refresh elements on the action bar. e.g. The current directory name displayed
* on the action bar won't get updated.
*/
@Override
public void onSearchChanged(@Nullable String query) {
if (mSearchManager.isSearching()) {
Metrics.logSearchMode(query != null, mSearchManager.hasCheckedChip());
if (mInjector.pickResult != null) {
mInjector.pickResult.increaseActionCount();
}
}
mInjector.actions.loadDocumentsForCurrentStack();
expandAppBar();
DirectoryFragment dir = getDirectoryFragment();
if (dir != null) {
dir.scrollToTop();
}
}
@Override
public void onSearchFinished() {
// Restores menu icons state
invalidateOptionsMenu();
}
@Override
public void onSearchViewChanged(boolean opened) {
mNavigator.update();
}
@Override
public void onSearchChipStateChanged(View v) {
final Checkable chip = (Checkable) v;
if (chip.isChecked()) {
final SearchChipData item = (SearchChipData) v.getTag();
Metrics.logUserAction(MetricConsts.USER_ACTION_SEARCH_CHIP);
Metrics.logSearchType(item.getChipType());
}
}
@Override
public void onSearchViewFocusChanged(boolean hasFocus) {
final boolean isInitailSearch
= !TextUtils.isEmpty(mSearchManager.getCurrentSearch())
&& TextUtils.isEmpty(mSearchManager.getSearchViewText());
if (hasFocus && (SearchFragment.get(getSupportFragmentManager()) == null)
&& !isInitailSearch) {
SearchFragment.showFragment(getSupportFragmentManager(),
mSearchManager.getSearchViewText());
}
}
@Override
public void onSearchViewClearClicked() {
if (SearchFragment.get(getSupportFragmentManager()) == null) {
SearchFragment.showFragment(getSupportFragmentManager(),
mSearchManager.getSearchViewText());
}
}
};
// "Commands" are meta input for controlling system behavior.
// We piggy back on search input as it is the only text input
// area in the app. But the functionality is independent
// of "regular" search query processing.
final CommandInterceptor cmdInterceptor = new CommandInterceptor(mInjector.features);
cmdInterceptor.add(new CommandInterceptor.DumpRootsCacheHandler(this));
// A tiny decorator that adds support for enabling CommandInterceptor
// based on query input. It's sorta like CommandInterceptor, but its metaaahhh.
EventHandler<String> queryInterceptor =
CommandInterceptor.createDebugModeFlipper(
mInjector.features,
mInjector.debugHelper::toggleDebugMode,
cmdInterceptor);
ViewGroup chipGroup = findViewById(R.id.search_chip_group);
mSearchManager = new SearchViewManager(searchListener, queryInterceptor,
chipGroup, icicle);
// initialize the chip sets by accept mime types
mSearchManager.initChipSets(mState.acceptMimes);
// update the chip items by the mime types of the root
mSearchManager.updateChips(getCurrentRoot().derivedMimeTypes);
// parse the query content from intent when launch the
// activity at the first time
if (icicle == null) {
mHasQueryContentFromIntent = mSearchManager.parseQueryContentFromIntent(getIntent(),
mState.action);
}
mNavigator.setSearchBarClickListener(v -> {
mSearchManager.onSearchBarClicked();
mNavigator.update();
});
mSortController = SortController.create(this, mState.derivedMode, mState.sortModel);
mPreferencesMonitor = new PreferencesMonitor(
getApplicationContext().getPackageName(),
PreferenceManager.getDefaultSharedPreferences(this),
this::onPreferenceChanged);
mPreferencesMonitor.start();
// Base classes must update result in their onCreate.
setResult(AppCompatActivity.RESULT_CANCELED);
}
public void onPreferenceChanged(String pref) {
// For now, we only work with prefs that we backup. This
// just limits the scope of what we expect to come flowing
// through here until we know we want more and fancier options.
assert(Preferences.shouldBackup(pref));
switch (pref) {
case ScopedPreferences.INCLUDE_DEVICE_ROOT:
updateDisplayAdvancedDevices(mInjector.prefs.getShowDeviceRoot());
}
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
mRootsMonitor = new RootsMonitor<>(
this,
mInjector.actions,
mProviders,
mDocs,
mState,
mSearchManager,
mInjector.actionModeController::finishActionMode);
mRootsMonitor.start();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
boolean showMenu = super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.activity, menu);
mNavigator.update();
boolean fullBarSearch = getResources().getBoolean(R.bool.full_bar_search_view);
boolean showSearchBar = getResources().getBoolean(R.bool.show_search_bar);
mSearchManager.install(menu, fullBarSearch, showSearchBar);
final ActionMenuView subMenuView = findViewById(R.id.sub_menu);
// If size is 0, it means the menu has not inflated and it should only do once.
if (subMenuView.getMenu().size() == 0) {
subMenuView.setOnMenuItemClickListener(this::onOptionsItemSelected);
getMenuInflater().inflate(R.menu.sub_menu, subMenuView.getMenu());
}
return showMenu;
}
@Override
@CallSuper
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
mSearchManager.showMenu(mState.stack);
final ActionMenuView subMenuView = findViewById(R.id.sub_menu);
mInjector.menuManager.updateSubMenu(subMenuView.getMenu());
return true;
}
@Override
protected void onDestroy() {
mRootsMonitor.stop();
mPreferencesMonitor.stop();
mSortController.destroy();
super.onDestroy();
}
private State getState(@Nullable Bundle icicle) {
if (icicle != null) {
State state = icicle.<State>getParcelable(Shared.EXTRA_STATE);
if (DEBUG) {
Log.d(mTag, "Recovered existing state object: " + state);
}
return state;
}
State state = new State();
final Intent intent = getIntent();
state.sortModel = SortModel.createModel();
state.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false);
state.excludedAuthorities = getExcludedAuthorities();
includeState(state);
state.showAdvanced = Shared.mustShowDeviceRoot(intent)
|| mInjector.prefs.getShowDeviceRoot();
// Only show the toggle if advanced isn't forced enabled.
state.showDeviceStorageOption = !Shared.mustShowDeviceRoot(intent);
if (DEBUG) {
Log.d(mTag, "Created new state object: " + state);
}
return state;
}
private void setContainer() {
View root = findViewById(R.id.coordinator_layout);
root.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
root.setOnApplyWindowInsetsListener((v, insets) -> {
root.setPadding(insets.getSystemWindowInsetLeft(),
insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
View saveContainer = findViewById(R.id.container_save);
saveContainer.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
View rootsContainer = findViewById(R.id.container_roots);
rootsContainer.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
return insets.consumeSystemWindowInsets();
});
getWindow().setNavigationBarDividerColor(Color.TRANSPARENT);
if (Build.VERSION.SDK_INT >= 29) {
getWindow().setNavigationBarColor(Color.TRANSPARENT);
getWindow().setNavigationBarContrastEnforced(true);
} else {
getWindow().setNavigationBarColor(getColor(R.color.nav_bar_translucent));
}
}
@Override
public void setRootsDrawerOpen(boolean open) {
mNavigator.revealRootsDrawer(open);
}
@Override
public void onRootPicked(RootInfo root) {
// Clicking on the current root removes search
mSearchManager.cancelSearch();
// Skip refreshing if root nor directory didn't change
if (root.equals(getCurrentRoot()) && mState.stack.size() == 1) {
return;
}
mInjector.actionModeController.finishActionMode();
mSortController.onViewModeChanged(mState.derivedMode);
// Set summary header's visibility. Only recents and downloads root may have summary in
// their docs.
mState.sortModel.setDimensionVisibility(
SortModel.SORT_DIMENSION_ID_SUMMARY,
root.isRecents() || root.isDownloads() ? View.VISIBLE : View.INVISIBLE);
// Clear entire backstack and start in new root
mState.stack.changeRoot(root);
// Recents is always in memory, so we just load it directly.
// Otherwise we delegate loading data from disk to a task
// to ensure a responsive ui.
if (mProviders.isRecentsRoot(root)) {
refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
} else {
mInjector.actions.getRootDocument(
root,
TimeoutTask.DEFAULT_TIMEOUT,
doc -> mInjector.actions.openRootDocument(doc));
}
expandAppBar();
updateHeaderTitle();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.option_menu_create_dir:
getInjector().actions.showCreateDirectoryDialog();
return true;
case R.id.option_menu_search:
// SearchViewManager listens for this directly.
return false;
case R.id.option_menu_advanced:
onDisplayAdvancedDevices();
return true;
case R.id.option_menu_select_all:
getInjector().actions.selectAllFiles();
return true;
case R.id.option_menu_debug:
getInjector().actions.showDebugMessage();
return true;
case R.id.option_menu_sort:
getInjector().actions.showSortDialog();
return true;
case R.id.sub_menu_grid:
setViewMode(State.MODE_GRID);
return true;
case R.id.sub_menu_list:
setViewMode(State.MODE_LIST);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
protected final @Nullable DirectoryFragment getDirectoryFragment() {
return DirectoryFragment.get(getSupportFragmentManager());
}
/**
* Returns true if a directory can be created in the current location.
* @return
*/
protected boolean canCreateDirectory() {
final RootInfo root = getCurrentRoot();
final DocumentInfo cwd = getCurrentDirectory();
return cwd != null
&& cwd.isCreateSupported()
&& !mSearchManager.isSearching()
&& !root.isRecents();
}
/**
* Returns true if a directory can be inspected.
*/
protected boolean canInspectDirectory() {
return false;
}
// TODO: make navigator listen to state
@Override
public final void updateNavigator() {
mNavigator.update();
}
@Override
public void restoreRootAndDirectory() {
// We're trying to restore stuff in document stack from saved instance. If we didn't have a
// chance to spawn a fragment before we need to do it now. However if we spawned a fragment
// already, system will automatically restore the fragment for us so we don't need to do
// that manually this time.
if (DirectoryFragment.get(getSupportFragmentManager()) == null) {
refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
}
}
/**
* Refreshes the content of the director and the menu/action bar.
* The current directory name and selection will get updated.
* @param anim
*/
@Override
public final void refreshCurrentRootAndDirectory(int anim) {
// The following call will crash if it's called before onCreateOptionMenu() is called in
// which we install menu item to search view manager, and there is a search query we need to
// restore. This happens when we're still initializing our UI so we shouldn't cancel the
// search which will be restored later in onCreateOptionMenu(). Try finding a way to guard
// refreshCurrentRootAndDirectory() from being called while we're restoring the state of UI
// from the saved state passed in onCreate().
mSearchManager.cancelSearch();
// only set the query content in the first launch
if (mHasQueryContentFromIntent) {
mHasQueryContentFromIntent = false;
mSearchManager.setCurrentSearch(mSearchManager.getQueryContentFromIntent());
}
mState.derivedMode = LocalPreferences.getViewMode(this, mState.stack.getRoot(), MODE_GRID);
mNavigator.update();
refreshDirectory(anim);
final RootsFragment roots = RootsFragment.get(getSupportFragmentManager());
if (roots != null) {
roots.onCurrentRootChanged();
}
// Causes talkback to announce the activity's new title
setTitle(mState.stack.getTitle());
invalidateOptionsMenu();
mSortController.onViewModeChanged(mState.derivedMode);
mSearchManager.updateChips(getCurrentRoot().derivedMimeTypes);
mAppsRowManager.updateView(this);
}
private final List<String> getExcludedAuthorities() {
List<String> authorities = new ArrayList<>();
if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) {
// Exclude roots provided by the calling package.
String packageName = Shared.getCallingPackageName(this);
try {
PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName,
PackageManager.GET_PROVIDERS);
for (ProviderInfo provider: pkgInfo.providers) {
authorities.add(provider.authority);
}
} catch (PackageManager.NameNotFoundException e) {
Log.e(mTag, "Calling package name does not resolve: " + packageName);
}
}
return authorities;
}
public static BaseActivity get(Fragment fragment) {
return (BaseActivity) fragment.getActivity();
}
public State getDisplayState() {
return mState;
}
/**
* Set internal storage visible based on explicit user action.
*/
private void onDisplayAdvancedDevices() {
boolean display = !mState.showAdvanced;
Metrics.logUserAction(display
? MetricConsts.USER_ACTION_SHOW_ADVANCED : MetricConsts.USER_ACTION_HIDE_ADVANCED);
mInjector.prefs.setShowDeviceRoot(display);
updateDisplayAdvancedDevices(display);
}
private void updateDisplayAdvancedDevices(boolean display) {
mState.showAdvanced = display;
@Nullable RootsFragment fragment = RootsFragment.get(getSupportFragmentManager());
if (fragment != null) {
// This also takes care of updating launcher shortcuts (which are roots :)
fragment.onDisplayStateChanged();
}
invalidateOptionsMenu();
}
/**
* Set mode based on explicit user action.
*/
void setViewMode(@ViewMode int mode) {
if (mode == State.MODE_GRID) {
Metrics.logUserAction(MetricConsts.USER_ACTION_GRID);
} else if (mode == State.MODE_LIST) {
Metrics.logUserAction(MetricConsts.USER_ACTION_LIST);
}
LocalPreferences.setViewMode(this, getCurrentRoot(), mode);
mState.derivedMode = mode;
final ActionMenuView subMenuView = findViewById(R.id.sub_menu);
mInjector.menuManager.updateSubMenu(subMenuView.getMenu());
DirectoryFragment dir = getDirectoryFragment();
if (dir != null) {
dir.onViewModeChanged();
}
mSortController.onViewModeChanged(mode);
}
public void setPending(boolean pending) {
// TODO: Isolate this behavior to PickActivity.
}
public void expandAppBar() {
final AppBarLayout appBarLayout = findViewById(R.id.app_bar);
if (appBarLayout != null) {
appBarLayout.setExpanded(true);
}
}
public void updateHeaderTitle() {
if (!mState.stack.isInitialized()) {
//stack has not initialized, the header will update after the stack finishes loading
return;
}
final RootInfo root = mState.stack.getRoot();
final String rootTitle = root.title;
String result;
switch (root.derivedType) {
case RootInfo.TYPE_RECENTS:
result = getHeaderRecentTitle();
break;
case RootInfo.TYPE_IMAGES:
case RootInfo.TYPE_VIDEO:
case RootInfo.TYPE_AUDIO:
result = getString(R.string.root_info_header_media, rootTitle);
break;
case RootInfo.TYPE_DOWNLOADS:
case RootInfo.TYPE_LOCAL:
case RootInfo.TYPE_MTP:
case RootInfo.TYPE_SD:
case RootInfo.TYPE_USB:
result = getHeaderStorageTitle(rootTitle);
break;
default:
final String summary = root.summary;
result = getHeaderDefaultTitle(rootTitle, summary);
break;
}
TextView headerTitle = findViewById(R.id.header_title);
headerTitle.setText(result);
}
private String getHeaderRecentTitle() {
// If stack size larger than 1, it means user global search than enter a folder, but search
// is not expanded on that time.
boolean isGlobalSearch = mSearchManager.isSearching() || mState.stack.size() > 1;
if (mState.isPhotoPicking()) {
final int resId = isGlobalSearch
? R.string.root_info_header_image_global_search
: R.string.root_info_header_image_recent;
return getString(resId);
} else {
final int resId = isGlobalSearch
? R.string.root_info_header_global_search
: R.string.root_info_header_recent;
return getString(resId);
}
}
private String getHeaderStorageTitle(String rootTitle) {
final int resId = mState.isPhotoPicking()
? R.string.root_info_header_image_storage : R.string.root_info_header_storage;
return getString(resId, rootTitle);
}
private String getHeaderDefaultTitle(String rootTitle, String summary) {
if (TextUtils.isEmpty(summary)) {
final int resId = mState.isPhotoPicking()
? R.string.root_info_header_image_app : R.string.root_info_header_app;
return getString(resId, rootTitle);
} else {
final int resId = mState.isPhotoPicking()
? R.string.root_info_header_image_app_with_summary
: R.string.root_info_header_app_with_summary;
return getString(resId, rootTitle, summary);
}
}
/**
* Get title string equal to the string action bar displayed.
* @return current directory title name
*/
public String getCurrentTitle() {
if (!mState.stack.isInitialized()) {
return null;
}
if (mState.stack.size() > 1) {
return getCurrentDirectory().displayName;
} else {
return getCurrentRoot().title;
}
}
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
state.putParcelable(Shared.EXTRA_STATE, mState);
mSearchManager.onSaveInstanceState(state);
}
@Override
public boolean isSearchExpanded() {
return mSearchManager.isExpanded();
}
@Override
public RootInfo getCurrentRoot() {
RootInfo root = mState.stack.getRoot();
if (root != null) {
return root;
} else {
return mProviders.getRecentsRoot();
}
}
@Override
public DocumentInfo getCurrentDirectory() {
return mState.stack.peek();
}
@Override
public boolean isInRecents() {
return mState.stack.isRecents();
}
@VisibleForTesting
public void addEventListener(EventListener listener) {
mEventListeners.add(listener);
}
@VisibleForTesting
public void removeEventListener(EventListener listener) {
mEventListeners.remove(listener);
}
@VisibleForTesting
public void notifyDirectoryLoaded(Uri uri) {
for (EventListener listener : mEventListeners) {
listener.onDirectoryLoaded(uri);
}
}
@VisibleForTesting
@Override
public void notifyDirectoryNavigated(Uri uri) {
for (EventListener listener : mEventListeners) {
listener.onDirectoryNavigated(uri);
}
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
mInjector.debugHelper.debugCheck(event.getDownTime(), event.getKeyCode());
}
DocumentsApplication.getDragAndDropManager(this).onKeyEvent(event);
return super.dispatchKeyEvent(event);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mInjector.actions.onActivityResult(requestCode, resultCode, data);
}
/**
* Pops the top entry off the directory stack, and returns the user to the previous directory.
* If the directory stack only contains one item, this method does nothing.
*
* @return Whether the stack was popped.
*/
protected boolean popDir() {
if (mState.stack.size() > 1) {
final DirectoryFragment fragment = getDirectoryFragment();
if (fragment != null) {
fragment.stopScroll();
}
mState.stack.pop();
refreshCurrentRootAndDirectory(AnimationView.ANIM_LEAVE);
return true;
}
return false;
}
protected boolean focusSidebar() {
RootsFragment rf = RootsFragment.get(getSupportFragmentManager());
assert (rf != null);
return rf.requestFocus();
}
/**
* Closes the activity when it's idle.
*/
private void addListenerForLaunchCompletion() {
addEventListener(new EventListener() {
@Override
public void onDirectoryNavigated(Uri uri) {
}
@Override
public void onDirectoryLoaded(Uri uri) {
removeEventListener(this);
getMainLooper().getQueue().addIdleHandler(new IdleHandler() {
@Override
public boolean queueIdle() {
// If startup benchmark is requested by a whitelisted testing package, then
// close the activity once idle, and notify the testing activity.
if (getIntent().getBooleanExtra(EXTRA_BENCHMARK, false) &&
BENCHMARK_TESTING_PACKAGE.equals(getCallingPackage())) {
setResult(RESULT_OK);
finish();
}
Metrics.logStartupMs((int) (new Date().getTime() - mStartTime));
// Remove the idle handler.
return false;
}
});
}
});
}
@VisibleForTesting
protected interface EventListener {
/**
* @param uri Uri navigated to. If recents, then null.
*/
void onDirectoryNavigated(@Nullable Uri uri);
/**
* @param uri Uri of the loaded directory. If recents, then null.
*/
void onDirectoryLoaded(@Nullable Uri uri);
}
}