| /* |
| * Copyright (C) 2013 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.sidebar; |
| |
| import static com.android.documentsui.base.Shared.compareToIgnoreCaseNullable; |
| import static com.android.documentsui.base.SharedMinimal.DEBUG; |
| import static com.android.documentsui.base.SharedMinimal.VERBOSE; |
| |
| import android.app.admin.DevicePolicyManager; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.graphics.Color; |
| import android.graphics.drawable.ColorDrawable; |
| import android.os.Bundle; |
| import android.provider.DocumentsContract; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.ContextMenu; |
| import android.view.DragEvent; |
| import android.view.LayoutInflater; |
| import android.view.MenuItem; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.View.OnDragListener; |
| import android.view.View.OnGenericMotionListener; |
| import android.view.ViewGroup; |
| import android.widget.AdapterView; |
| import android.widget.AdapterView.AdapterContextMenuInfo; |
| import android.widget.AdapterView.OnItemClickListener; |
| import android.widget.AdapterView.OnItemLongClickListener; |
| import android.widget.ListView; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.fragment.app.Fragment; |
| import androidx.fragment.app.FragmentManager; |
| import androidx.fragment.app.FragmentTransaction; |
| import androidx.loader.app.LoaderManager; |
| import androidx.loader.app.LoaderManager.LoaderCallbacks; |
| import androidx.loader.content.Loader; |
| |
| import com.android.documentsui.ActionHandler; |
| import com.android.documentsui.BaseActivity; |
| import com.android.documentsui.DocumentsApplication; |
| import com.android.documentsui.DragHoverListener; |
| import com.android.documentsui.Injector; |
| import com.android.documentsui.Injector.Injected; |
| import com.android.documentsui.ItemDragListener; |
| import com.android.documentsui.R; |
| import com.android.documentsui.UserPackage; |
| import com.android.documentsui.base.BooleanConsumer; |
| import com.android.documentsui.base.DocumentInfo; |
| import com.android.documentsui.base.DocumentStack; |
| import com.android.documentsui.base.Events; |
| import com.android.documentsui.base.Features; |
| import com.android.documentsui.base.Providers; |
| import com.android.documentsui.base.RootInfo; |
| import com.android.documentsui.base.State; |
| import com.android.documentsui.base.UserId; |
| import com.android.documentsui.roots.ProvidersAccess; |
| import com.android.documentsui.roots.ProvidersCache; |
| import com.android.documentsui.roots.RootsLoader; |
| import com.android.documentsui.util.CrossProfileUtils; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| |
| /** |
| * Display list of known storage backend roots. |
| */ |
| public class RootsFragment extends Fragment { |
| |
| private static final String TAG = "RootsFragment"; |
| private static final String EXTRA_INCLUDE_APPS = "includeApps"; |
| private static final String EXTRA_INCLUDE_APPS_INTENT = "includeAppsIntent"; |
| private static final int CONTEXT_MENU_ITEM_TIMEOUT = 500; |
| |
| private final OnItemClickListener mItemListener = new OnItemClickListener() { |
| @Override |
| public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |
| final Item item = mAdapter.getItem(position); |
| item.open(); |
| |
| getBaseActivity().setRootsDrawerOpen(false); |
| } |
| }; |
| |
| private final OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() { |
| @Override |
| public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { |
| final Item item = mAdapter.getItem(position); |
| return item.showAppDetails(); |
| } |
| }; |
| |
| private ListView mList; |
| private RootsAdapter mAdapter; |
| private LoaderCallbacks<Collection<RootInfo>> mCallbacks; |
| private @Nullable OnDragListener mDragListener; |
| |
| @Injected |
| private Injector<?> mInjector; |
| |
| @Injected |
| private ActionHandler mActionHandler; |
| |
| private List<Item> mApplicationItemList; |
| |
| /** |
| * Shows the {@link RootsFragment}. |
| * @param fm the FragmentManager for interacting with fragments associated with this |
| * fragment's activity |
| * @param includeApps if {@code true}, query the intent from the system and include apps in |
| * the {@RootsFragment}. |
| * @param intent the intent to query for package manager |
| */ |
| public static RootsFragment show(FragmentManager fm, boolean includeApps, Intent intent) { |
| final Bundle args = new Bundle(); |
| args.putBoolean(EXTRA_INCLUDE_APPS, includeApps); |
| args.putParcelable(EXTRA_INCLUDE_APPS_INTENT, intent); |
| |
| final RootsFragment fragment = new RootsFragment(); |
| fragment.setArguments(args); |
| |
| final FragmentTransaction ft = fm.beginTransaction(); |
| ft.replace(R.id.container_roots, fragment); |
| ft.commitAllowingStateLoss(); |
| |
| return fragment; |
| } |
| |
| public static RootsFragment get(FragmentManager fm) { |
| return (RootsFragment) fm.findFragmentById(R.id.container_roots); |
| } |
| |
| @Override |
| public View onCreateView( |
| LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { |
| |
| mInjector = getBaseActivity().getInjector(); |
| |
| final View view = inflater.inflate(R.layout.fragment_roots, container, false); |
| mList = (ListView) view.findViewById(R.id.roots_list); |
| mList.setOnItemClickListener(mItemListener); |
| // ListView does not have right-click specific listeners, so we will have a |
| // GenericMotionListener to listen for it. |
| // Currently, right click is viewed the same as long press, so we will have to quickly |
| // register for context menu when we receive a right click event, and quickly unregister |
| // it afterwards to prevent context menus popping up upon long presses. |
| // All other motion events will then get passed to OnItemClickListener. |
| mList.setOnGenericMotionListener( |
| new OnGenericMotionListener() { |
| @Override |
| public boolean onGenericMotion(View v, MotionEvent event) { |
| if (Events.isMouseEvent(event) |
| && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) { |
| int x = (int) event.getX(); |
| int y = (int) event.getY(); |
| return onRightClick(v, x, y, () -> { |
| mInjector.menuManager.showContextMenu( |
| RootsFragment.this, v, x, y); |
| }); |
| } |
| return false; |
| } |
| }); |
| mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE); |
| mList.setSelector(new ColorDrawable(Color.TRANSPARENT)); |
| return view; |
| } |
| |
| private boolean onRightClick(View v, int x, int y, Runnable callback) { |
| final int pos = mList.pointToPosition(x, y); |
| final Item item = mAdapter.getItem(pos); |
| |
| // If a read-only root, no need to see if top level is writable (it's not) |
| if (!(item instanceof RootItem) || !((RootItem) item).root.supportsCreate()) { |
| return false; |
| } |
| |
| final RootItem rootItem = (RootItem) item; |
| getRootDocument(rootItem, (DocumentInfo doc) -> { |
| rootItem.docInfo = doc; |
| callback.run(); |
| }); |
| return true; |
| } |
| |
| @Override |
| public void onActivityCreated(Bundle savedInstanceState) { |
| super.onActivityCreated(savedInstanceState); |
| |
| final BaseActivity activity = getBaseActivity(); |
| final ProvidersCache providers = DocumentsApplication.getProvidersCache(activity); |
| final State state = activity.getDisplayState(); |
| |
| mActionHandler = mInjector.actions; |
| |
| if (mInjector.config.dragAndDropEnabled()) { |
| final DragHost host = new DragHost( |
| activity, |
| DocumentsApplication.getDragAndDropManager(activity), |
| this::getItem, |
| mActionHandler); |
| final ItemDragListener<DragHost> listener = new ItemDragListener<DragHost>(host) { |
| @Override |
| public boolean handleDropEventChecked(View v, DragEvent event) { |
| final Item item = getItem(v); |
| |
| assert (item.isRoot()); |
| |
| return item.dropOn(event); |
| } |
| }; |
| mDragListener = DragHoverListener.create(listener, mList); |
| mList.setOnDragListener(mDragListener); |
| } |
| |
| mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() { |
| @Override |
| public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) { |
| return new RootsLoader(activity, providers, state); |
| } |
| |
| @Override |
| public void onLoadFinished( |
| Loader<Collection<RootInfo>> loader, Collection<RootInfo> roots) { |
| if (!isAdded()) { |
| return; |
| } |
| |
| boolean shouldIncludeHandlerApp = getArguments().getBoolean(EXTRA_INCLUDE_APPS, |
| /* defaultValue= */ false); |
| Intent handlerAppIntent = getArguments().getParcelable(EXTRA_INCLUDE_APPS_INTENT); |
| |
| final Intent intent = activity.getIntent(); |
| final boolean excludeSelf = |
| intent.getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false); |
| final String excludePackage = excludeSelf ? activity.getCallingPackage() : null; |
| final boolean maybeShowBadge = |
| getBaseActivity().getDisplayState().supportsCrossProfile(); |
| |
| // For action which supports cross profile, update the policy value in state if |
| // necessary. |
| ResolveInfo crossProfileResolveInfo = null; |
| if (state.supportsCrossProfile() && handlerAppIntent != null) { |
| crossProfileResolveInfo = CrossProfileUtils.getCrossProfileResolveInfo( |
| getContext().getPackageManager(), handlerAppIntent); |
| updateCrossProfileStateAndMaybeRefresh( |
| /* canShareAcrossProfile= */ crossProfileResolveInfo != null); |
| } |
| |
| List<Item> sortedItems = sortLoadResult( |
| getContext(), |
| state, |
| roots, |
| excludePackage, |
| shouldIncludeHandlerApp ? handlerAppIntent : null, |
| DocumentsApplication.getProvidersCache(getContext()), |
| getBaseActivity().getSelectedUser(), |
| DocumentsApplication.getUserIdManager(getContext()).getUserIds(), |
| maybeShowBadge); |
| |
| // This will be removed when feature flag is removed. |
| if (crossProfileResolveInfo != null && !Features.CROSS_PROFILE_TABS) { |
| // Add profile item if we don't support cross-profile tab. |
| sortedItems.add(new SpacerItem()); |
| sortedItems.add(new ProfileItem(crossProfileResolveInfo, |
| crossProfileResolveInfo.loadLabel( |
| getContext().getPackageManager()).toString(), |
| mActionHandler)); |
| } |
| |
| // Disable drawer if only one root |
| activity.setRootsDrawerLocked(sortedItems.size() <= 1); |
| |
| // Get the first visible position and offset |
| final int firstPosition = mList.getFirstVisiblePosition(); |
| View firstChild = mList.getChildAt(0); |
| final int offset = |
| firstChild != null ? firstChild.getTop() - mList.getPaddingTop() : 0; |
| final int oriItemCount = mAdapter != null ? mAdapter.getCount() : 0; |
| mAdapter = new RootsAdapter(activity, sortedItems, mDragListener); |
| mList.setAdapter(mAdapter); |
| |
| // recover the position. |
| if (oriItemCount == mAdapter.getCount()) { |
| mList.setSelectionFromTop(firstPosition, offset); |
| } |
| |
| mInjector.shortcutsUpdater.accept(roots); |
| mInjector.appsRowManager.updateList(mApplicationItemList); |
| mInjector.appsRowManager.updateView(activity); |
| onCurrentRootChanged(); |
| } |
| |
| @Override |
| public void onLoaderReset(Loader<Collection<RootInfo>> loader) { |
| mAdapter = null; |
| mList.setAdapter(null); |
| } |
| }; |
| } |
| |
| /** |
| * Updates the state values of whether we can share across profiles, if necessary. Also reload |
| * documents stack if the selected user is not the current user. |
| */ |
| private void updateCrossProfileStateAndMaybeRefresh(boolean canShareAcrossProfile) { |
| final State state = getBaseActivity().getDisplayState(); |
| if (state.canShareAcrossProfile != canShareAcrossProfile) { |
| state.canShareAcrossProfile = canShareAcrossProfile; |
| if (!UserId.CURRENT_USER.equals(getBaseActivity().getSelectedUser())) { |
| mActionHandler.loadDocumentsForCurrentStack(); |
| } |
| } |
| } |
| |
| /** |
| * If the package name of other providers or apps capable of handling the original intent |
| * include the preferred root source, it will have higher order than others. |
| * @param excludePackage Exclude activities from this given package |
| * @param handlerAppIntent When not null, apps capable of handling the original intent will |
| * be included in list of roots (in special section at bottom). |
| */ |
| @VisibleForTesting |
| List<Item> sortLoadResult( |
| Context context, |
| State state, |
| Collection<RootInfo> roots, |
| @Nullable String excludePackage, |
| @Nullable Intent handlerAppIntent, |
| ProvidersAccess providersAccess, |
| UserId selectedUser, |
| List<UserId> userIds, |
| boolean maybeShowBadge) { |
| final List<Item> result = new ArrayList<>(); |
| |
| final RootItemListBuilder librariesBuilder = new RootItemListBuilder(selectedUser, userIds); |
| final RootItemListBuilder storageProvidersBuilder = new RootItemListBuilder(selectedUser, |
| userIds); |
| final List<RootItem> otherProviders = new ArrayList<>(); |
| |
| for (final RootInfo root : roots) { |
| final RootItem item; |
| |
| if (root.isExternalStorageHome()) { |
| continue; |
| } else if (root.isLibrary() || root.isDownloads()) { |
| item = new RootItem(root, mActionHandler, maybeShowBadge); |
| librariesBuilder.add(item); |
| } else if (root.isStorage()) { |
| item = new RootItem(root, mActionHandler, maybeShowBadge); |
| storageProvidersBuilder.add(item); |
| } else { |
| item = new RootItem(root, mActionHandler, |
| providersAccess.getPackageName(root.userId, root.authority), |
| maybeShowBadge); |
| otherProviders.add(item); |
| } |
| } |
| |
| final List<RootItem> libraries = librariesBuilder.getList(); |
| final List<RootItem> storageProviders = storageProvidersBuilder.getList(); |
| |
| final RootComparator comp = new RootComparator(); |
| Collections.sort(libraries, comp); |
| Collections.sort(storageProviders, comp); |
| |
| if (VERBOSE) Log.v(TAG, "Adding library roots: " + libraries); |
| result.addAll(libraries); |
| |
| // Only add the spacer if it is actually separating something. |
| if (!result.isEmpty() && !storageProviders.isEmpty()) { |
| result.add(new SpacerItem()); |
| } |
| if (VERBOSE) Log.v(TAG, "Adding storage roots: " + storageProviders); |
| result.addAll(storageProviders); |
| |
| final List<Item> rootList = new ArrayList<>(); |
| final List<Item> rootListOtherUser = new ArrayList<>(); |
| mApplicationItemList = new ArrayList<>(); |
| if (handlerAppIntent != null) { |
| includeHandlerApps(state, handlerAppIntent, excludePackage, rootList, rootListOtherUser, |
| otherProviders, userIds, maybeShowBadge); |
| } else { |
| // Only add providers |
| Collections.sort(otherProviders, comp); |
| for (RootItem item : otherProviders) { |
| if (UserId.CURRENT_USER.equals(item.userId)) { |
| rootList.add(item); |
| } else { |
| rootListOtherUser.add(item); |
| } |
| mApplicationItemList.add(item); |
| } |
| } |
| |
| List<Item> presentableList = new UserItemsCombiner( |
| context.getResources(), context.getSystemService(DevicePolicyManager.class), state) |
| .setRootListForCurrentUser(rootList) |
| .setRootListForOtherUser(rootListOtherUser) |
| .createPresentableList(); |
| addListToResult(result, presentableList); |
| return result; |
| } |
| |
| private void addListToResult(List<Item> result, List<Item> rootList) { |
| if (!result.isEmpty() && !rootList.isEmpty()) { |
| result.add(new SpacerItem()); |
| } |
| result.addAll(rootList); |
| } |
| |
| /** |
| * Adds apps capable of handling the original intent will be included in list of roots. If |
| * the providers and apps are the same package name, combine them as RootAndAppItems. |
| */ |
| private void includeHandlerApps(State state, |
| Intent handlerAppIntent, @Nullable String excludePackage, List<Item> rootList, |
| List<Item> rootListOtherUser, List<RootItem> otherProviders, List<UserId> userIds, |
| boolean maybeShowBadge) { |
| if (VERBOSE) Log.v(TAG, "Adding handler apps for intent: " + handlerAppIntent); |
| |
| Context context = getContext(); |
| final Map<UserPackage, ResolveInfo> appsMapping = new HashMap<>(); |
| final Map<UserPackage, Item> appItems = new HashMap<>(); |
| |
| final String myPackageName = context.getPackageName(); |
| for (UserId userId : userIds) { |
| final PackageManager pm = userId.getPackageManager(context); |
| final List<ResolveInfo> infos = pm.queryIntentActivities( |
| handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY); |
| |
| // Omit ourselves and maybe calling package from the list |
| for (ResolveInfo info : infos) { |
| if (!info.activityInfo.exported) { |
| if (VERBOSE) { |
| Log.v(TAG, "Non exported activity: " + info.activityInfo); |
| } |
| continue; |
| } |
| |
| final String packageName = info.activityInfo.packageName; |
| if (!myPackageName.equals(packageName) |
| && !TextUtils.equals(excludePackage, packageName)) { |
| UserPackage userPackage = new UserPackage(userId, packageName); |
| appsMapping.put(userPackage, info); |
| |
| if (!CrossProfileUtils.isCrossProfileIntentForwarderActivity(info)) { |
| final Item item = new AppItem(info, info.loadLabel(pm).toString(), userId, |
| mActionHandler); |
| appItems.put(userPackage, item); |
| if (VERBOSE) Log.v(TAG, "Adding handler app: " + item); |
| } |
| } |
| } |
| } |
| |
| // If there are some providers and apps has the same package name, combine them as one item. |
| for (RootItem rootItem : otherProviders) { |
| final UserPackage userPackage = new UserPackage(rootItem.userId, |
| rootItem.getPackageName()); |
| final ResolveInfo resolveInfo = appsMapping.get(userPackage); |
| |
| final Item item; |
| if (resolveInfo != null) { |
| item = new RootAndAppItem(rootItem.root, resolveInfo, mActionHandler, |
| maybeShowBadge); |
| appItems.remove(userPackage); |
| } else { |
| item = rootItem; |
| } |
| |
| if (UserId.CURRENT_USER.equals(item.userId)) { |
| if (VERBOSE) Log.v(TAG, "Adding provider : " + item); |
| rootList.add(item); |
| } else { |
| if (VERBOSE) Log.v(TAG, "Adding provider to other users : " + item); |
| rootListOtherUser.add(item); |
| } |
| } |
| |
| for (Item item : appItems.values()) { |
| if (UserId.CURRENT_USER.equals(item.userId)) { |
| rootList.add(item); |
| } else { |
| rootListOtherUser.add(item); |
| } |
| } |
| |
| final String preferredRootPackage = getResources().getString( |
| R.string.preferred_root_package, ""); |
| final ItemComparator comp = new ItemComparator(preferredRootPackage); |
| Collections.sort(rootList, comp); |
| Collections.sort(rootListOtherUser, comp); |
| |
| if (state.supportsCrossProfile() && state.canShareAcrossProfile) { |
| mApplicationItemList.addAll(rootList); |
| mApplicationItemList.addAll(rootListOtherUser); |
| } else { |
| mApplicationItemList.addAll(rootList); |
| } |
| } |
| |
| @Override |
| public void onResume() { |
| super.onResume(); |
| final Context context = getActivity(); |
| // Update the information for Storage's root |
| if (context != null) { |
| DocumentsApplication.getProvidersCache(context).updateAuthorityAsync( |
| ((BaseActivity) context).getSelectedUser(), Providers.AUTHORITY_STORAGE); |
| } |
| onDisplayStateChanged(); |
| } |
| |
| public void onDisplayStateChanged() { |
| final Context context = getActivity(); |
| final State state = ((BaseActivity) context).getDisplayState(); |
| |
| if (state.action == State.ACTION_GET_CONTENT) { |
| mList.setOnItemLongClickListener(mItemLongClickListener); |
| } else { |
| mList.setOnItemLongClickListener(null); |
| mList.setLongClickable(false); |
| } |
| |
| LoaderManager.getInstance(this).restartLoader(2, null, mCallbacks); |
| } |
| |
| public void onCurrentRootChanged() { |
| if (mAdapter == null) { |
| return; |
| } |
| |
| final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot(); |
| for (int i = 0; i < mAdapter.getCount(); i++) { |
| final Object item = mAdapter.getItem(i); |
| if (item instanceof RootItem) { |
| final RootInfo testRoot = ((RootItem) item).root; |
| if (Objects.equals(testRoot, root)) { |
| // b/37358441 should reload all root title after configuration changed |
| root.title = testRoot.title; |
| mList.setItemChecked(i, true); |
| return; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Called when the selected user is changed. It reloads roots with the current user. |
| */ |
| public void onSelectedUserChanged() { |
| LoaderManager.getInstance(this).restartLoader(/* id= */ 2, /* args= */ null, mCallbacks); |
| } |
| |
| /** |
| * Attempts to shift focus back to the navigation drawer. |
| */ |
| public boolean requestFocus() { |
| return mList.requestFocus(); |
| } |
| |
| private BaseActivity getBaseActivity() { |
| return (BaseActivity) getActivity(); |
| } |
| |
| @Override |
| public void onCreateContextMenu( |
| ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { |
| super.onCreateContextMenu(menu, v, menuInfo); |
| AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo; |
| final Item item = mAdapter.getItem(adapterMenuInfo.position); |
| |
| BaseActivity activity = getBaseActivity(); |
| item.createContextMenu(menu, activity.getMenuInflater(), mInjector.menuManager); |
| } |
| |
| @Override |
| public boolean onContextItemSelected(MenuItem item) { |
| AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) item.getMenuInfo(); |
| // There is a possibility that this is called from DirectoryFragment since |
| // all fragments' onContextItemSelected gets called when any menu item is selected |
| // This is to guard against it since DirectoryFragment's RecylerView does not have a |
| // menuInfo |
| if (adapterMenuInfo == null) { |
| return false; |
| } |
| final RootItem rootItem = (RootItem) mAdapter.getItem(adapterMenuInfo.position); |
| final int id = item.getItemId(); |
| if (id == R.id.root_menu_eject_root) { |
| final View ejectIcon = adapterMenuInfo.targetView.findViewById(R.id.action_icon); |
| ejectClicked(ejectIcon, rootItem.root, mActionHandler); |
| return true; |
| } else if (id == R.id.root_menu_open_in_new_window) { |
| mActionHandler.openInNewWindow(new DocumentStack(rootItem.root)); |
| return true; |
| } else if (id == R.id.root_menu_paste_into_folder) { |
| mActionHandler.pasteIntoFolder(rootItem.root); |
| return true; |
| } else if (id == R.id.root_menu_settings) { |
| mActionHandler.openSettings(rootItem.root); |
| return true; |
| } |
| if (DEBUG) { |
| Log.d(TAG, "Unhandled menu item selected: " + item); |
| } |
| return false; |
| } |
| |
| private void getRootDocument(RootItem rootItem, RootUpdater updater) { |
| // We need to start a GetRootDocumentTask so we can know whether items can be directly |
| // pasted into root |
| mActionHandler.getRootDocument( |
| rootItem.root, |
| CONTEXT_MENU_ITEM_TIMEOUT, |
| (DocumentInfo doc) -> { |
| updater.updateDocInfoForRoot(doc); |
| }); |
| } |
| |
| private Item getItem(View v) { |
| final int pos = (Integer) v.getTag(R.id.item_position_tag); |
| return mAdapter.getItem(pos); |
| } |
| |
| static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler) { |
| assert(ejectIcon != null); |
| assert(!root.ejecting); |
| ejectIcon.setEnabled(false); |
| root.ejecting = true; |
| actionHandler.ejectRoot( |
| root, |
| new BooleanConsumer() { |
| @Override |
| public void accept(boolean ejected) { |
| // Event if ejected is false, we should reset, since the op failed. |
| // Either way, we are no longer attempting to eject the device. |
| root.ejecting = false; |
| |
| // If the view is still visible, we update its state. |
| if (ejectIcon.getVisibility() == View.VISIBLE) { |
| ejectIcon.setEnabled(!ejected); |
| } |
| } |
| }); |
| } |
| |
| private static class RootComparator implements Comparator<RootItem> { |
| @Override |
| public int compare(RootItem lhs, RootItem rhs) { |
| return lhs.root.compareTo(rhs.root); |
| } |
| } |
| |
| /** |
| * The comparator of {@link AppItem}, {@link RootItem} and {@link RootAndAppItem}. |
| * Sort by if the item's package name starts with the preferred package name, |
| * then title, then summary. Because the {@link AppItem} doesn't have summary, |
| * it will have lower order than other same title items. |
| */ |
| @VisibleForTesting |
| static class ItemComparator implements Comparator<Item> { |
| private final String mPreferredPackageName; |
| |
| ItemComparator(String preferredPackageName) { |
| mPreferredPackageName = preferredPackageName; |
| } |
| |
| @Override |
| public int compare(Item lhs, Item rhs) { |
| // Sort by whether the item starts with preferred package name |
| if (!mPreferredPackageName.isEmpty()) { |
| if (lhs.getPackageName().startsWith(mPreferredPackageName)) { |
| if (!rhs.getPackageName().startsWith(mPreferredPackageName)) { |
| // lhs starts with it, but rhs doesn't start with it |
| return -1; |
| } |
| } else { |
| if (rhs.getPackageName().startsWith(mPreferredPackageName)) { |
| // lhs doesn't start with it, but rhs starts with it |
| return 1; |
| } |
| } |
| } |
| |
| // Sort by title |
| int score = compareToIgnoreCaseNullable(lhs.title, rhs.title); |
| if (score != 0) { |
| return score; |
| } |
| |
| // Sort by summary. If the item is AppItem, it doesn't have summary. |
| // So, the RootItem or RootAndAppItem will have higher order than AppItem. |
| if (lhs instanceof RootItem) { |
| return rhs instanceof RootItem ? compareToIgnoreCaseNullable( |
| ((RootItem) lhs).root.summary, ((RootItem) rhs).root.summary) : 1; |
| } |
| return rhs instanceof RootItem ? -1 : 0; |
| } |
| } |
| |
| @FunctionalInterface |
| interface RootUpdater { |
| void updateDocInfoForRoot(DocumentInfo doc); |
| } |
| } |