1 /* 2 * Copyright (C) 2013 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.sidebar; 18 19 import static com.android.documentsui.base.Shared.compareToIgnoreCaseNullable; 20 import static com.android.documentsui.base.SharedMinimal.DEBUG; 21 import static com.android.documentsui.base.SharedMinimal.VERBOSE; 22 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.PackageManager; 26 import android.content.pm.ResolveInfo; 27 import android.content.res.Resources; 28 import android.graphics.Color; 29 import android.graphics.drawable.ColorDrawable; 30 import android.os.Bundle; 31 import android.provider.DocumentsContract; 32 import android.text.TextUtils; 33 import android.util.Log; 34 import android.view.ContextMenu; 35 import android.view.DragEvent; 36 import android.view.LayoutInflater; 37 import android.view.MenuItem; 38 import android.view.MotionEvent; 39 import android.view.View; 40 import android.view.View.OnDragListener; 41 import android.view.View.OnGenericMotionListener; 42 import android.view.ViewGroup; 43 import android.widget.AdapterView; 44 import android.widget.AdapterView.AdapterContextMenuInfo; 45 import android.widget.AdapterView.OnItemClickListener; 46 import android.widget.AdapterView.OnItemLongClickListener; 47 import android.widget.ListView; 48 49 import androidx.annotation.Nullable; 50 import androidx.annotation.VisibleForTesting; 51 import androidx.fragment.app.Fragment; 52 import androidx.fragment.app.FragmentManager; 53 import androidx.fragment.app.FragmentTransaction; 54 import androidx.loader.app.LoaderManager; 55 import androidx.loader.app.LoaderManager.LoaderCallbacks; 56 import androidx.loader.content.Loader; 57 58 import com.android.documentsui.ActionHandler; 59 import com.android.documentsui.BaseActivity; 60 import com.android.documentsui.DocumentsApplication; 61 import com.android.documentsui.DragHoverListener; 62 import com.android.documentsui.Injector; 63 import com.android.documentsui.Injector.Injected; 64 import com.android.documentsui.ItemDragListener; 65 import com.android.documentsui.R; 66 import com.android.documentsui.UserPackage; 67 import com.android.documentsui.base.BooleanConsumer; 68 import com.android.documentsui.base.DocumentInfo; 69 import com.android.documentsui.base.DocumentStack; 70 import com.android.documentsui.base.Events; 71 import com.android.documentsui.base.Features; 72 import com.android.documentsui.base.Providers; 73 import com.android.documentsui.base.RootInfo; 74 import com.android.documentsui.base.State; 75 import com.android.documentsui.base.UserId; 76 import com.android.documentsui.roots.ProvidersAccess; 77 import com.android.documentsui.roots.ProvidersCache; 78 import com.android.documentsui.roots.RootsLoader; 79 import com.android.documentsui.util.CrossProfileUtils; 80 81 import java.util.ArrayList; 82 import java.util.Collection; 83 import java.util.Collections; 84 import java.util.Comparator; 85 import java.util.HashMap; 86 import java.util.List; 87 import java.util.Map; 88 import java.util.Objects; 89 90 /** 91 * Display list of known storage backend roots. 92 */ 93 public class RootsFragment extends Fragment { 94 95 private static final String TAG = "RootsFragment"; 96 private static final String EXTRA_INCLUDE_APPS = "includeApps"; 97 private static final String EXTRA_INCLUDE_APPS_INTENT = "includeAppsIntent"; 98 private static final int CONTEXT_MENU_ITEM_TIMEOUT = 500; 99 100 private final OnItemClickListener mItemListener = new OnItemClickListener() { 101 @Override 102 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 103 final Item item = mAdapter.getItem(position); 104 item.open(); 105 106 getBaseActivity().setRootsDrawerOpen(false); 107 } 108 }; 109 110 private final OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() { 111 @Override 112 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { 113 final Item item = mAdapter.getItem(position); 114 return item.showAppDetails(); 115 } 116 }; 117 118 private ListView mList; 119 private RootsAdapter mAdapter; 120 private LoaderCallbacks<Collection<RootInfo>> mCallbacks; 121 private @Nullable OnDragListener mDragListener; 122 123 @Injected 124 private Injector<?> mInjector; 125 126 @Injected 127 private ActionHandler mActionHandler; 128 129 private List<Item> mApplicationItemList; 130 131 /** 132 * Shows the {@link RootsFragment}. 133 * @param fm the FragmentManager for interacting with fragments associated with this 134 * fragment's activity 135 * @param includeApps if {@code true}, query the intent from the system and include apps in 136 * the {@RootsFragment}. 137 * @param intent the intent to query for package manager 138 */ show(FragmentManager fm, boolean includeApps, Intent intent)139 public static RootsFragment show(FragmentManager fm, boolean includeApps, Intent intent) { 140 final Bundle args = new Bundle(); 141 args.putBoolean(EXTRA_INCLUDE_APPS, includeApps); 142 args.putParcelable(EXTRA_INCLUDE_APPS_INTENT, intent); 143 144 final RootsFragment fragment = new RootsFragment(); 145 fragment.setArguments(args); 146 147 final FragmentTransaction ft = fm.beginTransaction(); 148 ft.replace(R.id.container_roots, fragment); 149 ft.commitAllowingStateLoss(); 150 151 return fragment; 152 } 153 get(FragmentManager fm)154 public static RootsFragment get(FragmentManager fm) { 155 return (RootsFragment) fm.findFragmentById(R.id.container_roots); 156 } 157 158 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)159 public View onCreateView( 160 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 161 162 mInjector = getBaseActivity().getInjector(); 163 164 final View view = inflater.inflate(R.layout.fragment_roots, container, false); 165 mList = (ListView) view.findViewById(R.id.roots_list); 166 mList.setOnItemClickListener(mItemListener); 167 // ListView does not have right-click specific listeners, so we will have a 168 // GenericMotionListener to listen for it. 169 // Currently, right click is viewed the same as long press, so we will have to quickly 170 // register for context menu when we receive a right click event, and quickly unregister 171 // it afterwards to prevent context menus popping up upon long presses. 172 // All other motion events will then get passed to OnItemClickListener. 173 mList.setOnGenericMotionListener( 174 new OnGenericMotionListener() { 175 @Override 176 public boolean onGenericMotion(View v, MotionEvent event) { 177 if (Events.isMouseEvent(event) 178 && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) { 179 int x = (int) event.getX(); 180 int y = (int) event.getY(); 181 return onRightClick(v, x, y, () -> { 182 mInjector.menuManager.showContextMenu( 183 RootsFragment.this, v, x, y); 184 }); 185 } 186 return false; 187 } 188 }); 189 mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 190 mList.setSelector(new ColorDrawable(Color.TRANSPARENT)); 191 return view; 192 } 193 onRightClick(View v, int x, int y, Runnable callback)194 private boolean onRightClick(View v, int x, int y, Runnable callback) { 195 final int pos = mList.pointToPosition(x, y); 196 final Item item = mAdapter.getItem(pos); 197 198 // If a read-only root, no need to see if top level is writable (it's not) 199 if (!(item instanceof RootItem) || !((RootItem) item).root.supportsCreate()) { 200 return false; 201 } 202 203 final RootItem rootItem = (RootItem) item; 204 getRootDocument(rootItem, (DocumentInfo doc) -> { 205 rootItem.docInfo = doc; 206 callback.run(); 207 }); 208 return true; 209 } 210 211 @Override onActivityCreated(Bundle savedInstanceState)212 public void onActivityCreated(Bundle savedInstanceState) { 213 super.onActivityCreated(savedInstanceState); 214 215 final BaseActivity activity = getBaseActivity(); 216 final ProvidersCache providers = DocumentsApplication.getProvidersCache(activity); 217 final State state = activity.getDisplayState(); 218 219 mActionHandler = mInjector.actions; 220 221 if (mInjector.config.dragAndDropEnabled()) { 222 final DragHost host = new DragHost( 223 activity, 224 DocumentsApplication.getDragAndDropManager(activity), 225 this::getItem, 226 mActionHandler); 227 final ItemDragListener<DragHost> listener = new ItemDragListener<DragHost>(host) { 228 @Override 229 public boolean handleDropEventChecked(View v, DragEvent event) { 230 final Item item = getItem(v); 231 232 assert (item.isRoot()); 233 234 return item.dropOn(event); 235 } 236 }; 237 mDragListener = DragHoverListener.create(listener, mList); 238 mList.setOnDragListener(mDragListener); 239 } 240 241 mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() { 242 @Override 243 public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) { 244 return new RootsLoader(activity, providers, state); 245 } 246 247 @Override 248 public void onLoadFinished( 249 Loader<Collection<RootInfo>> loader, Collection<RootInfo> roots) { 250 if (!isAdded()) { 251 return; 252 } 253 254 boolean shouldIncludeHandlerApp = getArguments().getBoolean(EXTRA_INCLUDE_APPS, 255 /* defaultValue= */ false); 256 Intent handlerAppIntent = getArguments().getParcelable(EXTRA_INCLUDE_APPS_INTENT); 257 258 final Intent intent = activity.getIntent(); 259 final boolean excludeSelf = 260 intent.getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false); 261 final String excludePackage = excludeSelf ? activity.getCallingPackage() : null; 262 final boolean maybeShowBadge = 263 getBaseActivity().getDisplayState().supportsCrossProfile(); 264 265 // For action which supports cross profile, update the policy value in state if 266 // necessary. 267 ResolveInfo crossProfileResolveInfo = null; 268 if (state.supportsCrossProfile() && handlerAppIntent != null) { 269 crossProfileResolveInfo = CrossProfileUtils.getCrossProfileResolveInfo( 270 getContext().getPackageManager(), handlerAppIntent); 271 updateCrossProfileStateAndMaybeRefresh( 272 /* canShareAcrossProfile= */ crossProfileResolveInfo != null); 273 } 274 275 List<Item> sortedItems = sortLoadResult( 276 getResources(), 277 state, 278 roots, 279 excludePackage, 280 shouldIncludeHandlerApp ? handlerAppIntent : null, 281 DocumentsApplication.getProvidersCache(getContext()), 282 getBaseActivity().getSelectedUser(), 283 DocumentsApplication.getUserIdManager(getContext()).getUserIds(), 284 maybeShowBadge); 285 286 // This will be removed when feature flag is removed. 287 if (crossProfileResolveInfo != null && !Features.CROSS_PROFILE_TABS) { 288 // Add profile item if we don't support cross-profile tab. 289 sortedItems.add(new SpacerItem()); 290 sortedItems.add(new ProfileItem(crossProfileResolveInfo, 291 crossProfileResolveInfo.loadLabel( 292 getContext().getPackageManager()).toString(), 293 mActionHandler)); 294 } 295 296 // Disable drawer if only one root 297 activity.setRootsDrawerLocked(sortedItems.size() <= 1); 298 299 // Get the first visible position and offset 300 final int firstPosition = mList.getFirstVisiblePosition(); 301 View firstChild = mList.getChildAt(0); 302 final int offset = 303 firstChild != null ? firstChild.getTop() - mList.getPaddingTop() : 0; 304 final int oriItemCount = mAdapter != null ? mAdapter.getCount() : 0; 305 mAdapter = new RootsAdapter(activity, sortedItems, mDragListener); 306 mList.setAdapter(mAdapter); 307 308 // recover the position. 309 if (oriItemCount == mAdapter.getCount()) { 310 mList.setSelectionFromTop(firstPosition, offset); 311 } 312 313 mInjector.shortcutsUpdater.accept(roots); 314 mInjector.appsRowManager.updateList(mApplicationItemList); 315 mInjector.appsRowManager.updateView(activity); 316 onCurrentRootChanged(); 317 } 318 319 @Override 320 public void onLoaderReset(Loader<Collection<RootInfo>> loader) { 321 mAdapter = null; 322 mList.setAdapter(null); 323 } 324 }; 325 } 326 327 /** 328 * Updates the state values of whether we can share across profiles, if necessary. Also reload 329 * documents stack if the selected user is not the current user. 330 */ updateCrossProfileStateAndMaybeRefresh(boolean canShareAcrossProfile)331 private void updateCrossProfileStateAndMaybeRefresh(boolean canShareAcrossProfile) { 332 final State state = getBaseActivity().getDisplayState(); 333 if (state.canShareAcrossProfile != canShareAcrossProfile) { 334 state.canShareAcrossProfile = canShareAcrossProfile; 335 if (!UserId.CURRENT_USER.equals(getBaseActivity().getSelectedUser())) { 336 mActionHandler.loadDocumentsForCurrentStack(); 337 } 338 } 339 } 340 341 /** 342 * If the package name of other providers or apps capable of handling the original intent 343 * include the preferred root source, it will have higher order than others. 344 * @param excludePackage Exclude activities from this given package 345 * @param handlerAppIntent When not null, apps capable of handling the original intent will 346 * be included in list of roots (in special section at bottom). 347 */ 348 @VisibleForTesting sortLoadResult( Resources resources, State state, Collection<RootInfo> roots, @Nullable String excludePackage, @Nullable Intent handlerAppIntent, ProvidersAccess providersAccess, UserId selectedUser, List<UserId> userIds, boolean maybeShowBadge)349 List<Item> sortLoadResult( 350 Resources resources, 351 State state, 352 Collection<RootInfo> roots, 353 @Nullable String excludePackage, 354 @Nullable Intent handlerAppIntent, 355 ProvidersAccess providersAccess, 356 UserId selectedUser, 357 List<UserId> userIds, 358 boolean maybeShowBadge) { 359 final List<Item> result = new ArrayList<>(); 360 361 final RootItemListBuilder librariesBuilder = new RootItemListBuilder(selectedUser, userIds); 362 final RootItemListBuilder storageProvidersBuilder = new RootItemListBuilder(selectedUser, 363 userIds); 364 final List<RootItem> otherProviders = new ArrayList<>(); 365 366 for (final RootInfo root : roots) { 367 final RootItem item; 368 369 if (root.isExternalStorageHome()) { 370 continue; 371 } else if (root.isLibrary() || root.isDownloads()) { 372 item = new RootItem(root, mActionHandler, maybeShowBadge); 373 librariesBuilder.add(item); 374 } else if (root.isStorage()) { 375 item = new RootItem(root, mActionHandler, maybeShowBadge); 376 storageProvidersBuilder.add(item); 377 } else { 378 item = new RootItem(root, mActionHandler, 379 providersAccess.getPackageName(root.userId, root.authority), 380 maybeShowBadge); 381 otherProviders.add(item); 382 } 383 } 384 385 final List<RootItem> libraries = librariesBuilder.getList(); 386 final List<RootItem> storageProviders = storageProvidersBuilder.getList(); 387 388 final RootComparator comp = new RootComparator(); 389 Collections.sort(libraries, comp); 390 Collections.sort(storageProviders, comp); 391 392 if (VERBOSE) Log.v(TAG, "Adding library roots: " + libraries); 393 result.addAll(libraries); 394 395 // Only add the spacer if it is actually separating something. 396 if (!result.isEmpty() && !storageProviders.isEmpty()) { 397 result.add(new SpacerItem()); 398 } 399 if (VERBOSE) Log.v(TAG, "Adding storage roots: " + storageProviders); 400 result.addAll(storageProviders); 401 402 final List<Item> rootList = new ArrayList<>(); 403 final List<Item> rootListOtherUser = new ArrayList<>(); 404 mApplicationItemList = new ArrayList<>(); 405 if (handlerAppIntent != null) { 406 includeHandlerApps(state, handlerAppIntent, excludePackage, rootList, rootListOtherUser, 407 otherProviders, userIds, maybeShowBadge); 408 } else { 409 // Only add providers 410 Collections.sort(otherProviders, comp); 411 for (RootItem item : otherProviders) { 412 if (UserId.CURRENT_USER.equals(item.userId)) { 413 rootList.add(item); 414 } else { 415 rootListOtherUser.add(item); 416 } 417 mApplicationItemList.add(item); 418 } 419 } 420 421 List<Item> presentableList = new UserItemsCombiner(resources, state) 422 .setRootListForCurrentUser(rootList) 423 .setRootListForOtherUser(rootListOtherUser) 424 .createPresentableList(); 425 addListToResult(result, presentableList); 426 return result; 427 } 428 addListToResult(List<Item> result, List<Item> rootList)429 private void addListToResult(List<Item> result, List<Item> rootList) { 430 if (!result.isEmpty() && !rootList.isEmpty()) { 431 result.add(new SpacerItem()); 432 } 433 result.addAll(rootList); 434 } 435 436 /** 437 * Adds apps capable of handling the original intent will be included in list of roots. If 438 * the providers and apps are the same package name, combine them as RootAndAppItems. 439 */ includeHandlerApps(State state, Intent handlerAppIntent, @Nullable String excludePackage, List<Item> rootList, List<Item> rootListOtherUser, List<RootItem> otherProviders, List<UserId> userIds, boolean maybeShowBadge)440 private void includeHandlerApps(State state, 441 Intent handlerAppIntent, @Nullable String excludePackage, List<Item> rootList, 442 List<Item> rootListOtherUser, List<RootItem> otherProviders, List<UserId> userIds, 443 boolean maybeShowBadge) { 444 if (VERBOSE) Log.v(TAG, "Adding handler apps for intent: " + handlerAppIntent); 445 446 Context context = getContext(); 447 final Map<UserPackage, ResolveInfo> appsMapping = new HashMap<>(); 448 final Map<UserPackage, Item> appItems = new HashMap<>(); 449 450 final String myPackageName = context.getPackageName(); 451 for (UserId userId : userIds) { 452 final PackageManager pm = userId.getPackageManager(context); 453 final List<ResolveInfo> infos = pm.queryIntentActivities( 454 handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY); 455 456 // Omit ourselves and maybe calling package from the list 457 for (ResolveInfo info : infos) { 458 if (!info.activityInfo.exported) { 459 if (VERBOSE) { 460 Log.v(TAG, "Non exported activity: " + info.activityInfo); 461 } 462 continue; 463 } 464 465 final String packageName = info.activityInfo.packageName; 466 if (!myPackageName.equals(packageName) 467 && !TextUtils.equals(excludePackage, packageName)) { 468 UserPackage userPackage = new UserPackage(userId, packageName); 469 appsMapping.put(userPackage, info); 470 471 if (!CrossProfileUtils.isCrossProfileIntentForwarderActivity(info)) { 472 final Item item = new AppItem(info, info.loadLabel(pm).toString(), userId, 473 mActionHandler); 474 appItems.put(userPackage, item); 475 if (VERBOSE) Log.v(TAG, "Adding handler app: " + item); 476 } 477 } 478 } 479 } 480 481 // If there are some providers and apps has the same package name, combine them as one item. 482 for (RootItem rootItem : otherProviders) { 483 final UserPackage userPackage = new UserPackage(rootItem.userId, 484 rootItem.getPackageName()); 485 final ResolveInfo resolveInfo = appsMapping.get(userPackage); 486 487 final Item item; 488 if (resolveInfo != null) { 489 item = new RootAndAppItem(rootItem.root, resolveInfo, mActionHandler, 490 maybeShowBadge); 491 appItems.remove(userPackage); 492 } else { 493 item = rootItem; 494 } 495 496 if (UserId.CURRENT_USER.equals(item.userId)) { 497 if (VERBOSE) Log.v(TAG, "Adding provider : " + item); 498 rootList.add(item); 499 } else { 500 if (VERBOSE) Log.v(TAG, "Adding provider to other users : " + item); 501 rootListOtherUser.add(item); 502 } 503 } 504 505 for (Item item : appItems.values()) { 506 if (UserId.CURRENT_USER.equals(item.userId)) { 507 rootList.add(item); 508 } else { 509 rootListOtherUser.add(item); 510 } 511 } 512 513 final String preferredRootPackage = getResources().getString( 514 R.string.preferred_root_package, ""); 515 final ItemComparator comp = new ItemComparator(preferredRootPackage); 516 Collections.sort(rootList, comp); 517 Collections.sort(rootListOtherUser, comp); 518 519 if (state.supportsCrossProfile() && state.canShareAcrossProfile) { 520 mApplicationItemList.addAll(rootList); 521 mApplicationItemList.addAll(rootListOtherUser); 522 } else { 523 mApplicationItemList.addAll(rootList); 524 } 525 } 526 527 @Override onResume()528 public void onResume() { 529 super.onResume(); 530 final Context context = getActivity(); 531 // Update the information for Storage's root 532 if (context != null) { 533 DocumentsApplication.getProvidersCache(context).updateAuthorityAsync( 534 ((BaseActivity) context).getSelectedUser(), Providers.AUTHORITY_STORAGE); 535 } 536 onDisplayStateChanged(); 537 } 538 onDisplayStateChanged()539 public void onDisplayStateChanged() { 540 final Context context = getActivity(); 541 final State state = ((BaseActivity) context).getDisplayState(); 542 543 if (state.action == State.ACTION_GET_CONTENT) { 544 mList.setOnItemLongClickListener(mItemLongClickListener); 545 } else { 546 mList.setOnItemLongClickListener(null); 547 mList.setLongClickable(false); 548 } 549 550 LoaderManager.getInstance(this).restartLoader(2, null, mCallbacks); 551 } 552 onCurrentRootChanged()553 public void onCurrentRootChanged() { 554 if (mAdapter == null) { 555 return; 556 } 557 558 final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot(); 559 for (int i = 0; i < mAdapter.getCount(); i++) { 560 final Object item = mAdapter.getItem(i); 561 if (item instanceof RootItem) { 562 final RootInfo testRoot = ((RootItem) item).root; 563 if (Objects.equals(testRoot, root)) { 564 // b/37358441 should reload all root title after configuration changed 565 root.title = testRoot.title; 566 mList.setItemChecked(i, true); 567 return; 568 } 569 } 570 } 571 } 572 573 /** 574 * Called when the selected user is changed. It reloads roots with the current user. 575 */ onSelectedUserChanged()576 public void onSelectedUserChanged() { 577 LoaderManager.getInstance(this).restartLoader(/* id= */ 2, /* args= */ null, mCallbacks); 578 } 579 580 /** 581 * Attempts to shift focus back to the navigation drawer. 582 */ requestFocus()583 public boolean requestFocus() { 584 return mList.requestFocus(); 585 } 586 getBaseActivity()587 private BaseActivity getBaseActivity() { 588 return (BaseActivity) getActivity(); 589 } 590 591 @Override onCreateContextMenu( ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)592 public void onCreateContextMenu( 593 ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { 594 super.onCreateContextMenu(menu, v, menuInfo); 595 AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo; 596 final Item item = mAdapter.getItem(adapterMenuInfo.position); 597 598 BaseActivity activity = getBaseActivity(); 599 item.createContextMenu(menu, activity.getMenuInflater(), mInjector.menuManager); 600 } 601 602 @Override onContextItemSelected(MenuItem item)603 public boolean onContextItemSelected(MenuItem item) { 604 AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) item.getMenuInfo(); 605 // There is a possibility that this is called from DirectoryFragment since 606 // all fragments' onContextItemSelected gets called when any menu item is selected 607 // This is to guard against it since DirectoryFragment's RecylerView does not have a 608 // menuInfo 609 if (adapterMenuInfo == null) { 610 return false; 611 } 612 final RootItem rootItem = (RootItem) mAdapter.getItem(adapterMenuInfo.position); 613 switch (item.getItemId()) { 614 case R.id.root_menu_eject_root: 615 final View ejectIcon = adapterMenuInfo.targetView.findViewById(R.id.action_icon); 616 ejectClicked(ejectIcon, rootItem.root, mActionHandler); 617 return true; 618 case R.id.root_menu_open_in_new_window: 619 mActionHandler.openInNewWindow(new DocumentStack(rootItem.root)); 620 return true; 621 case R.id.root_menu_paste_into_folder: 622 mActionHandler.pasteIntoFolder(rootItem.root); 623 return true; 624 case R.id.root_menu_settings: 625 mActionHandler.openSettings(rootItem.root); 626 return true; 627 default: 628 if (DEBUG) { 629 Log.d(TAG, "Unhandled menu item selected: " + item); 630 } 631 return false; 632 } 633 } 634 getRootDocument(RootItem rootItem, RootUpdater updater)635 private void getRootDocument(RootItem rootItem, RootUpdater updater) { 636 // We need to start a GetRootDocumentTask so we can know whether items can be directly 637 // pasted into root 638 mActionHandler.getRootDocument( 639 rootItem.root, 640 CONTEXT_MENU_ITEM_TIMEOUT, 641 (DocumentInfo doc) -> { 642 updater.updateDocInfoForRoot(doc); 643 }); 644 } 645 getItem(View v)646 private Item getItem(View v) { 647 final int pos = (Integer) v.getTag(R.id.item_position_tag); 648 return mAdapter.getItem(pos); 649 } 650 ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler)651 static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler) { 652 assert(ejectIcon != null); 653 assert(!root.ejecting); 654 ejectIcon.setEnabled(false); 655 root.ejecting = true; 656 actionHandler.ejectRoot( 657 root, 658 new BooleanConsumer() { 659 @Override 660 public void accept(boolean ejected) { 661 // Event if ejected is false, we should reset, since the op failed. 662 // Either way, we are no longer attempting to eject the device. 663 root.ejecting = false; 664 665 // If the view is still visible, we update its state. 666 if (ejectIcon.getVisibility() == View.VISIBLE) { 667 ejectIcon.setEnabled(!ejected); 668 } 669 } 670 }); 671 } 672 673 private static class RootComparator implements Comparator<RootItem> { 674 @Override compare(RootItem lhs, RootItem rhs)675 public int compare(RootItem lhs, RootItem rhs) { 676 return lhs.root.compareTo(rhs.root); 677 } 678 } 679 680 /** 681 * The comparator of {@link AppItem}, {@link RootItem} and {@link RootAndAppItem}. 682 * Sort by if the item's package name starts with the preferred package name, 683 * then title, then summary. Because the {@link AppItem} doesn't have summary, 684 * it will have lower order than other same title items. 685 */ 686 @VisibleForTesting 687 static class ItemComparator implements Comparator<Item> { 688 private final String mPreferredPackageName; 689 ItemComparator(String preferredPackageName)690 ItemComparator(String preferredPackageName) { 691 mPreferredPackageName = preferredPackageName; 692 } 693 694 @Override compare(Item lhs, Item rhs)695 public int compare(Item lhs, Item rhs) { 696 // Sort by whether the item starts with preferred package name 697 if (!mPreferredPackageName.isEmpty()) { 698 if (lhs.getPackageName().startsWith(mPreferredPackageName)) { 699 if (!rhs.getPackageName().startsWith(mPreferredPackageName)) { 700 // lhs starts with it, but rhs doesn't start with it 701 return -1; 702 } 703 } else { 704 if (rhs.getPackageName().startsWith(mPreferredPackageName)) { 705 // lhs doesn't start with it, but rhs starts with it 706 return 1; 707 } 708 } 709 } 710 711 // Sort by title 712 int score = compareToIgnoreCaseNullable(lhs.title, rhs.title); 713 if (score != 0) { 714 return score; 715 } 716 717 // Sort by summary. If the item is AppItem, it doesn't have summary. 718 // So, the RootItem or RootAndAppItem will have higher order than AppItem. 719 if (lhs instanceof RootItem) { 720 return rhs instanceof RootItem ? compareToIgnoreCaseNullable( 721 ((RootItem) lhs).root.summary, ((RootItem) rhs).root.summary) : 1; 722 } 723 return rhs instanceof RootItem ? -1 : 0; 724 } 725 } 726 727 @FunctionalInterface 728 interface RootUpdater { updateDocInfoForRoot(DocumentInfo doc)729 void updateDocInfoForRoot(DocumentInfo doc); 730 } 731 } 732