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