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