• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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