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.app.admin.DevicePolicyManager; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.content.pm.ResolveInfo; 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 getContext(), 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( Context context, 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 Context context, 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( 422 context.getResources(), context.getSystemService(DevicePolicyManager.class), state) 423 .setRootListForCurrentUser(rootList) 424 .setRootListForOtherUser(rootListOtherUser) 425 .createPresentableList(); 426 addListToResult(result, presentableList); 427 return result; 428 } 429 addListToResult(List<Item> result, List<Item> rootList)430 private void addListToResult(List<Item> result, List<Item> rootList) { 431 if (!result.isEmpty() && !rootList.isEmpty()) { 432 result.add(new SpacerItem()); 433 } 434 result.addAll(rootList); 435 } 436 437 /** 438 * Adds apps capable of handling the original intent will be included in list of roots. If 439 * the providers and apps are the same package name, combine them as RootAndAppItems. 440 */ includeHandlerApps(State state, Intent handlerAppIntent, @Nullable String excludePackage, List<Item> rootList, List<Item> rootListOtherUser, List<RootItem> otherProviders, List<UserId> userIds, boolean maybeShowBadge)441 private void includeHandlerApps(State state, 442 Intent handlerAppIntent, @Nullable String excludePackage, List<Item> rootList, 443 List<Item> rootListOtherUser, List<RootItem> otherProviders, List<UserId> userIds, 444 boolean maybeShowBadge) { 445 if (VERBOSE) Log.v(TAG, "Adding handler apps for intent: " + handlerAppIntent); 446 447 Context context = getContext(); 448 final Map<UserPackage, ResolveInfo> appsMapping = new HashMap<>(); 449 final Map<UserPackage, Item> appItems = new HashMap<>(); 450 451 final String myPackageName = context.getPackageName(); 452 for (UserId userId : userIds) { 453 final PackageManager pm = userId.getPackageManager(context); 454 final List<ResolveInfo> infos = pm.queryIntentActivities( 455 handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY); 456 457 // Omit ourselves and maybe calling package from the list 458 for (ResolveInfo info : infos) { 459 if (!info.activityInfo.exported) { 460 if (VERBOSE) { 461 Log.v(TAG, "Non exported activity: " + info.activityInfo); 462 } 463 continue; 464 } 465 466 final String packageName = info.activityInfo.packageName; 467 if (!myPackageName.equals(packageName) 468 && !TextUtils.equals(excludePackage, packageName)) { 469 UserPackage userPackage = new UserPackage(userId, packageName); 470 appsMapping.put(userPackage, info); 471 472 if (!CrossProfileUtils.isCrossProfileIntentForwarderActivity(info)) { 473 final Item item = new AppItem(info, info.loadLabel(pm).toString(), userId, 474 mActionHandler); 475 appItems.put(userPackage, item); 476 if (VERBOSE) Log.v(TAG, "Adding handler app: " + item); 477 } 478 } 479 } 480 } 481 482 // If there are some providers and apps has the same package name, combine them as one item. 483 for (RootItem rootItem : otherProviders) { 484 final UserPackage userPackage = new UserPackage(rootItem.userId, 485 rootItem.getPackageName()); 486 final ResolveInfo resolveInfo = appsMapping.get(userPackage); 487 488 final Item item; 489 if (resolveInfo != null) { 490 item = new RootAndAppItem(rootItem.root, resolveInfo, mActionHandler, 491 maybeShowBadge); 492 appItems.remove(userPackage); 493 } else { 494 item = rootItem; 495 } 496 497 if (UserId.CURRENT_USER.equals(item.userId)) { 498 if (VERBOSE) Log.v(TAG, "Adding provider : " + item); 499 rootList.add(item); 500 } else { 501 if (VERBOSE) Log.v(TAG, "Adding provider to other users : " + item); 502 rootListOtherUser.add(item); 503 } 504 } 505 506 for (Item item : appItems.values()) { 507 if (UserId.CURRENT_USER.equals(item.userId)) { 508 rootList.add(item); 509 } else { 510 rootListOtherUser.add(item); 511 } 512 } 513 514 final String preferredRootPackage = getResources().getString( 515 R.string.preferred_root_package, ""); 516 final ItemComparator comp = new ItemComparator(preferredRootPackage); 517 Collections.sort(rootList, comp); 518 Collections.sort(rootListOtherUser, comp); 519 520 if (state.supportsCrossProfile() && state.canShareAcrossProfile) { 521 mApplicationItemList.addAll(rootList); 522 mApplicationItemList.addAll(rootListOtherUser); 523 } else { 524 mApplicationItemList.addAll(rootList); 525 } 526 } 527 528 @Override onResume()529 public void onResume() { 530 super.onResume(); 531 final Context context = getActivity(); 532 // Update the information for Storage's root 533 if (context != null) { 534 DocumentsApplication.getProvidersCache(context).updateAuthorityAsync( 535 ((BaseActivity) context).getSelectedUser(), Providers.AUTHORITY_STORAGE); 536 } 537 onDisplayStateChanged(); 538 } 539 onDisplayStateChanged()540 public void onDisplayStateChanged() { 541 final Context context = getActivity(); 542 final State state = ((BaseActivity) context).getDisplayState(); 543 544 if (state.action == State.ACTION_GET_CONTENT) { 545 mList.setOnItemLongClickListener(mItemLongClickListener); 546 } else { 547 mList.setOnItemLongClickListener(null); 548 mList.setLongClickable(false); 549 } 550 551 LoaderManager.getInstance(this).restartLoader(2, null, mCallbacks); 552 } 553 onCurrentRootChanged()554 public void onCurrentRootChanged() { 555 if (mAdapter == null) { 556 return; 557 } 558 559 final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot(); 560 for (int i = 0; i < mAdapter.getCount(); i++) { 561 final Object item = mAdapter.getItem(i); 562 if (item instanceof RootItem) { 563 final RootInfo testRoot = ((RootItem) item).root; 564 if (Objects.equals(testRoot, root)) { 565 // b/37358441 should reload all root title after configuration changed 566 root.title = testRoot.title; 567 mList.setItemChecked(i, true); 568 return; 569 } 570 } 571 } 572 } 573 574 /** 575 * Called when the selected user is changed. It reloads roots with the current user. 576 */ onSelectedUserChanged()577 public void onSelectedUserChanged() { 578 LoaderManager.getInstance(this).restartLoader(/* id= */ 2, /* args= */ null, mCallbacks); 579 } 580 581 /** 582 * Attempts to shift focus back to the navigation drawer. 583 */ requestFocus()584 public boolean requestFocus() { 585 return mList.requestFocus(); 586 } 587 getBaseActivity()588 private BaseActivity getBaseActivity() { 589 return (BaseActivity) getActivity(); 590 } 591 592 @Override onCreateContextMenu( ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)593 public void onCreateContextMenu( 594 ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { 595 super.onCreateContextMenu(menu, v, menuInfo); 596 AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo; 597 final Item item = mAdapter.getItem(adapterMenuInfo.position); 598 599 BaseActivity activity = getBaseActivity(); 600 item.createContextMenu(menu, activity.getMenuInflater(), mInjector.menuManager); 601 } 602 603 @Override onContextItemSelected(MenuItem item)604 public boolean onContextItemSelected(MenuItem item) { 605 AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) item.getMenuInfo(); 606 // There is a possibility that this is called from DirectoryFragment since 607 // all fragments' onContextItemSelected gets called when any menu item is selected 608 // This is to guard against it since DirectoryFragment's RecylerView does not have a 609 // menuInfo 610 if (adapterMenuInfo == null) { 611 return false; 612 } 613 final RootItem rootItem = (RootItem) mAdapter.getItem(adapterMenuInfo.position); 614 switch (item.getItemId()) { 615 case R.id.root_menu_eject_root: 616 final View ejectIcon = adapterMenuInfo.targetView.findViewById(R.id.action_icon); 617 ejectClicked(ejectIcon, rootItem.root, mActionHandler); 618 return true; 619 case R.id.root_menu_open_in_new_window: 620 mActionHandler.openInNewWindow(new DocumentStack(rootItem.root)); 621 return true; 622 case R.id.root_menu_paste_into_folder: 623 mActionHandler.pasteIntoFolder(rootItem.root); 624 return true; 625 case R.id.root_menu_settings: 626 mActionHandler.openSettings(rootItem.root); 627 return true; 628 default: 629 if (DEBUG) { 630 Log.d(TAG, "Unhandled menu item selected: " + item); 631 } 632 return false; 633 } 634 } 635 getRootDocument(RootItem rootItem, RootUpdater updater)636 private void getRootDocument(RootItem rootItem, RootUpdater updater) { 637 // We need to start a GetRootDocumentTask so we can know whether items can be directly 638 // pasted into root 639 mActionHandler.getRootDocument( 640 rootItem.root, 641 CONTEXT_MENU_ITEM_TIMEOUT, 642 (DocumentInfo doc) -> { 643 updater.updateDocInfoForRoot(doc); 644 }); 645 } 646 getItem(View v)647 private Item getItem(View v) { 648 final int pos = (Integer) v.getTag(R.id.item_position_tag); 649 return mAdapter.getItem(pos); 650 } 651 ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler)652 static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler) { 653 assert(ejectIcon != null); 654 assert(!root.ejecting); 655 ejectIcon.setEnabled(false); 656 root.ejecting = true; 657 actionHandler.ejectRoot( 658 root, 659 new BooleanConsumer() { 660 @Override 661 public void accept(boolean ejected) { 662 // Event if ejected is false, we should reset, since the op failed. 663 // Either way, we are no longer attempting to eject the device. 664 root.ejecting = false; 665 666 // If the view is still visible, we update its state. 667 if (ejectIcon.getVisibility() == View.VISIBLE) { 668 ejectIcon.setEnabled(!ejected); 669 } 670 } 671 }); 672 } 673 674 private static class RootComparator implements Comparator<RootItem> { 675 @Override compare(RootItem lhs, RootItem rhs)676 public int compare(RootItem lhs, RootItem rhs) { 677 return lhs.root.compareTo(rhs.root); 678 } 679 } 680 681 /** 682 * The comparator of {@link AppItem}, {@link RootItem} and {@link RootAndAppItem}. 683 * Sort by if the item's package name starts with the preferred package name, 684 * then title, then summary. Because the {@link AppItem} doesn't have summary, 685 * it will have lower order than other same title items. 686 */ 687 @VisibleForTesting 688 static class ItemComparator implements Comparator<Item> { 689 private final String mPreferredPackageName; 690 ItemComparator(String preferredPackageName)691 ItemComparator(String preferredPackageName) { 692 mPreferredPackageName = preferredPackageName; 693 } 694 695 @Override compare(Item lhs, Item rhs)696 public int compare(Item lhs, Item rhs) { 697 // Sort by whether the item starts with preferred package name 698 if (!mPreferredPackageName.isEmpty()) { 699 if (lhs.getPackageName().startsWith(mPreferredPackageName)) { 700 if (!rhs.getPackageName().startsWith(mPreferredPackageName)) { 701 // lhs starts with it, but rhs doesn't start with it 702 return -1; 703 } 704 } else { 705 if (rhs.getPackageName().startsWith(mPreferredPackageName)) { 706 // lhs doesn't start with it, but rhs starts with it 707 return 1; 708 } 709 } 710 } 711 712 // Sort by title 713 int score = compareToIgnoreCaseNullable(lhs.title, rhs.title); 714 if (score != 0) { 715 return score; 716 } 717 718 // Sort by summary. If the item is AppItem, it doesn't have summary. 719 // So, the RootItem or RootAndAppItem will have higher order than AppItem. 720 if (lhs instanceof RootItem) { 721 return rhs instanceof RootItem ? compareToIgnoreCaseNullable( 722 ((RootItem) lhs).root.summary, ((RootItem) rhs).root.summary) : 1; 723 } 724 return rhs instanceof RootItem ? -1 : 0; 725 } 726 } 727 728 @FunctionalInterface 729 interface RootUpdater { updateDocInfoForRoot(DocumentInfo doc)730 void updateDocInfoForRoot(DocumentInfo doc); 731 } 732 } 733