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