• 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.content.Context;
24 import android.content.Intent;
25 import android.content.pm.PackageManager;
26 import android.content.pm.ResolveInfo;
27 import android.graphics.Color;
28 import android.graphics.drawable.ColorDrawable;
29 import android.os.Bundle;
30 import android.provider.DocumentsContract;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.view.ContextMenu;
34 import android.view.DragEvent;
35 import android.view.LayoutInflater;
36 import android.view.MenuItem;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.view.View.OnDragListener;
40 import android.view.View.OnGenericMotionListener;
41 import android.view.ViewGroup;
42 import android.widget.AdapterView;
43 import android.widget.AdapterView.AdapterContextMenuInfo;
44 import android.widget.AdapterView.OnItemClickListener;
45 import android.widget.AdapterView.OnItemLongClickListener;
46 import android.widget.ListView;
47 
48 import androidx.annotation.Nullable;
49 import androidx.annotation.VisibleForTesting;
50 import androidx.fragment.app.Fragment;
51 import androidx.fragment.app.FragmentManager;
52 import androidx.fragment.app.FragmentTransaction;
53 import androidx.loader.app.LoaderManager;
54 import androidx.loader.app.LoaderManager.LoaderCallbacks;
55 import androidx.loader.content.Loader;
56 
57 import com.android.documentsui.ActionHandler;
58 import com.android.documentsui.BaseActivity;
59 import com.android.documentsui.DocumentsApplication;
60 import com.android.documentsui.DragHoverListener;
61 import com.android.documentsui.Injector;
62 import com.android.documentsui.Injector.Injected;
63 import com.android.documentsui.ItemDragListener;
64 import com.android.documentsui.R;
65 import com.android.documentsui.base.BooleanConsumer;
66 import com.android.documentsui.base.DocumentInfo;
67 import com.android.documentsui.base.DocumentStack;
68 import com.android.documentsui.base.Events;
69 import com.android.documentsui.base.RootInfo;
70 import com.android.documentsui.base.Shared;
71 import com.android.documentsui.base.State;
72 import com.android.documentsui.roots.ProvidersAccess;
73 import com.android.documentsui.roots.ProvidersCache;
74 import com.android.documentsui.roots.RootsLoader;
75 
76 import java.util.ArrayList;
77 import java.util.Collection;
78 import java.util.Collections;
79 import java.util.Comparator;
80 import java.util.HashMap;
81 import java.util.List;
82 import java.util.Map;
83 import java.util.Objects;
84 
85 /**
86  * Display list of known storage backend roots.
87  */
88 public class RootsFragment extends Fragment {
89 
90     private static final String TAG = "RootsFragment";
91     private static final String EXTRA_INCLUDE_APPS = "includeApps";
92     private static final String PROFILE_TARGET_ACTIVITY =
93             "com.android.internal.app.IntentForwarderActivity";
94     private static final int CONTEXT_MENU_ITEM_TIMEOUT = 500;
95 
96     private final OnItemClickListener mItemListener = new OnItemClickListener() {
97         @Override
98         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
99             final Item item = mAdapter.getItem(position);
100             item.open();
101 
102             getBaseActivity().setRootsDrawerOpen(false);
103         }
104     };
105 
106     private final OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
107         @Override
108         public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
109             final Item item = mAdapter.getItem(position);
110             return item.showAppDetails();
111         }
112     };
113 
114     private ListView mList;
115     private RootsAdapter mAdapter;
116     private LoaderCallbacks<Collection<RootInfo>> mCallbacks;
117     private @Nullable OnDragListener mDragListener;
118 
119     @Injected
120     private Injector<?> mInjector;
121 
122     @Injected
123     private ActionHandler mActionHandler;
124 
125     private List<Item> mApplicationItemList;
126 
show(FragmentManager fm, Intent includeApps)127     public static RootsFragment show(FragmentManager fm, Intent includeApps) {
128         final Bundle args = new Bundle();
129         args.putParcelable(EXTRA_INCLUDE_APPS, includeApps);
130 
131         final RootsFragment fragment = new RootsFragment();
132         fragment.setArguments(args);
133 
134         final FragmentTransaction ft = fm.beginTransaction();
135         ft.replace(R.id.container_roots, fragment);
136         ft.commitAllowingStateLoss();
137 
138         return fragment;
139     }
140 
get(FragmentManager fm)141     public static RootsFragment get(FragmentManager fm) {
142         return (RootsFragment) fm.findFragmentById(R.id.container_roots);
143     }
144 
145     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)146     public View onCreateView(
147             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
148 
149         mInjector = getBaseActivity().getInjector();
150 
151         final View view = inflater.inflate(R.layout.fragment_roots, container, false);
152         mList = (ListView) view.findViewById(R.id.roots_list);
153         mList.setOnItemClickListener(mItemListener);
154         // ListView does not have right-click specific listeners, so we will have a
155         // GenericMotionListener to listen for it.
156         // Currently, right click is viewed the same as long press, so we will have to quickly
157         // register for context menu when we receive a right click event, and quickly unregister
158         // it afterwards to prevent context menus popping up upon long presses.
159         // All other motion events will then get passed to OnItemClickListener.
160         mList.setOnGenericMotionListener(
161                 new OnGenericMotionListener() {
162                     @Override
163                     public boolean onGenericMotion(View v, MotionEvent event) {
164                         if (Events.isMouseEvent(event)
165                                 && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
166                             int x = (int) event.getX();
167                             int y = (int) event.getY();
168                             return onRightClick(v, x, y, () -> {
169                                 mInjector.menuManager.showContextMenu(
170                                         RootsFragment.this, v, x, y);
171                             });
172                         }
173                         return false;
174             }
175         });
176         mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
177         mList.setSelector(new ColorDrawable(Color.TRANSPARENT));
178         return view;
179     }
180 
onRightClick(View v, int x, int y, Runnable callback)181     private boolean onRightClick(View v, int x, int y, Runnable callback) {
182         final int pos = mList.pointToPosition(x, y);
183         final Item item = mAdapter.getItem(pos);
184 
185         // If a read-only root, no need to see if top level is writable (it's not)
186         if (!(item instanceof RootItem) || !((RootItem) item).root.supportsCreate()) {
187             return false;
188         }
189 
190         final RootItem rootItem = (RootItem) item;
191         getRootDocument(rootItem, (DocumentInfo doc) -> {
192             rootItem.docInfo = doc;
193             callback.run();
194         });
195         return true;
196     }
197 
198     @Override
onActivityCreated(Bundle savedInstanceState)199     public void onActivityCreated(Bundle savedInstanceState) {
200         super.onActivityCreated(savedInstanceState);
201 
202         final BaseActivity activity = getBaseActivity();
203         final ProvidersCache providers = DocumentsApplication.getProvidersCache(activity);
204         final State state = activity.getDisplayState();
205 
206         mActionHandler = mInjector.actions;
207 
208         if (mInjector.config.dragAndDropEnabled()) {
209             final DragHost host = new DragHost(
210                     activity,
211                     DocumentsApplication.getDragAndDropManager(activity),
212                     this::getItem,
213                     mActionHandler);
214             final ItemDragListener<DragHost> listener = new ItemDragListener<DragHost>(host) {
215                 @Override
216                 public boolean handleDropEventChecked(View v, DragEvent event) {
217                     final Item item = getItem(v);
218 
219                     assert (item.isRoot());
220 
221                     return item.dropOn(event);
222                 }
223             };
224             mDragListener = DragHoverListener.create(listener, mList);
225             mList.setOnDragListener(mDragListener);
226         }
227 
228         mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() {
229             @Override
230             public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) {
231                 return new RootsLoader(activity, providers, state);
232             }
233 
234             @Override
235             public void onLoadFinished(
236                     Loader<Collection<RootInfo>> loader, Collection<RootInfo> roots) {
237                 if (!isAdded()) {
238                     return;
239                 }
240 
241                 Intent handlerAppIntent = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
242 
243                 final Intent intent = activity.getIntent();
244                 final boolean excludeSelf =
245                         intent.getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false);
246                 final String excludePackage = excludeSelf ? activity.getCallingPackage() : null;
247                 List<Item> sortedItems = sortLoadResult(roots, excludePackage, handlerAppIntent,
248                         DocumentsApplication.getProvidersCache(getContext()));
249                 mAdapter = new RootsAdapter(activity, sortedItems, mDragListener);
250                 mList.setAdapter(mAdapter);
251 
252                 mInjector.shortcutsUpdater.accept(roots);
253                 mInjector.appsRowManager.updateList(mApplicationItemList);
254                 mInjector.appsRowManager.updateView(activity);
255                 onCurrentRootChanged();
256             }
257 
258             @Override
259             public void onLoaderReset(Loader<Collection<RootInfo>> loader) {
260                 mAdapter = null;
261                 mList.setAdapter(null);
262             }
263         };
264     }
265 
266     /**
267      * If the package name of other providers or apps capable of handling the original intent
268      * include the preferred root source, it will have higher order than others.
269      * @param excludePackage Exclude activities from this given package
270      * @param handlerAppIntent When not null, apps capable of handling the original intent will
271      *            be included in list of roots (in special section at bottom).
272      */
273     @VisibleForTesting
sortLoadResult( Collection<RootInfo> roots, @Nullable String excludePackage, @Nullable Intent handlerAppIntent, ProvidersAccess providersAccess)274     List<Item> sortLoadResult(
275             Collection<RootInfo> roots,
276             @Nullable String excludePackage,
277             @Nullable Intent handlerAppIntent,
278             ProvidersAccess providersAccess) {
279         final List<Item> result = new ArrayList<>();
280 
281         final List<RootItem> libraries = new ArrayList<>();
282         final List<RootItem> storageProviders = new ArrayList<>();
283         final List<RootItem> otherProviders = new ArrayList<>();
284 
285         for (final RootInfo root : roots) {
286             final RootItem item;
287 
288             if (root.isExternalStorageHome() && !Shared.shouldShowDocumentsRoot(getContext())) {
289                 continue;
290             } else if (root.isLibrary() || root.isDownloads()) {
291                 item = new RootItem(root, mActionHandler);
292                 libraries.add(item);
293             } else if (root.isStorage()) {
294                 item = new RootItem(root, mActionHandler);
295                 storageProviders.add(item);
296             } else {
297                 item = new RootItem(root, mActionHandler,
298                         providersAccess.getPackageName(root.authority));
299                 otherProviders.add(item);
300             }
301         }
302 
303         final RootComparator comp = new RootComparator();
304         Collections.sort(libraries, comp);
305         Collections.sort(storageProviders, comp);
306 
307         if (VERBOSE) Log.v(TAG, "Adding library roots: " + libraries);
308         result.addAll(libraries);
309 
310         // Only add the spacer if it is actually separating something.
311         if (!result.isEmpty() && !storageProviders.isEmpty()) {
312             result.add(new SpacerItem());
313         }
314         if (VERBOSE) Log.v(TAG, "Adding storage roots: " + storageProviders);
315         result.addAll(storageProviders);
316 
317         // Include apps that can handle this intent too.
318         if (handlerAppIntent != null) {
319             includeHandlerApps(handlerAppIntent, excludePackage, result, otherProviders);
320         } else {
321             // Only add providers
322             Collections.sort(otherProviders, comp);
323             if (!result.isEmpty() && !otherProviders.isEmpty()) {
324                 result.add(new SpacerItem());
325             }
326             if (VERBOSE) Log.v(TAG, "Adding plain roots: " + otherProviders);
327             result.addAll(otherProviders);
328 
329             mApplicationItemList = new ArrayList<>();
330             for (Item item : otherProviders) {
331                 mApplicationItemList.add(item);
332             }
333         }
334 
335         return result;
336     }
337 
338     /**
339      * Adds apps capable of handling the original intent will be included in list of roots. If
340      * the providers and apps are the same package name, combine them as RootAndAppItems.
341      */
includeHandlerApps( Intent handlerAppIntent, @Nullable String excludePackage, List<Item> result, List<RootItem> otherProviders)342     private void includeHandlerApps(
343             Intent handlerAppIntent, @Nullable String excludePackage, List<Item> result,
344             List<RootItem> otherProviders) {
345         if (VERBOSE) Log.v(TAG, "Adding handler apps for intent: " + handlerAppIntent);
346         Context context = getContext();
347         final PackageManager pm = context.getPackageManager();
348         final List<ResolveInfo> infos = pm.queryIntentActivities(
349                 handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY);
350 
351         final List<Item> rootList = new ArrayList<>();
352         final Map<String, ResolveInfo> appsMapping = new HashMap<>();
353         final Map<String, Item> appItems = new HashMap<>();
354         ProfileItem profileItem = null;
355 
356         // Omit ourselves and maybe calling package from the list
357         for (ResolveInfo info : infos) {
358             final String packageName = info.activityInfo.packageName;
359             if (!context.getPackageName().equals(packageName) &&
360                     !TextUtils.equals(excludePackage, packageName)) {
361                 appsMapping.put(packageName, info);
362 
363                 // for change personal profile root.
364                 if (PROFILE_TARGET_ACTIVITY.equals(info.activityInfo.targetActivity)) {
365                     profileItem = new ProfileItem(info, info.loadLabel(pm).toString(),
366                             mActionHandler);
367                 } else {
368                     final Item item = new AppItem(info, info.loadLabel(pm).toString(),
369                             mActionHandler);
370                     appItems.put(packageName, item);
371                     if (VERBOSE) Log.v(TAG, "Adding handler app: " + item);
372                 }
373             }
374         }
375 
376         // If there are some providers and apps has the same package name, combine them as one item.
377         for (RootItem rootItem : otherProviders) {
378             final String packageName = rootItem.getPackageName();
379             final ResolveInfo resolveInfo = appsMapping.get(packageName);
380 
381             final Item item;
382             if (resolveInfo != null) {
383                 item = new RootAndAppItem(rootItem.root, resolveInfo, mActionHandler);
384                 appItems.remove(packageName);
385             } else {
386                 item = rootItem;
387             }
388 
389             if (VERBOSE) Log.v(TAG, "Adding provider : " + item);
390             rootList.add(item);
391         }
392 
393         rootList.addAll(appItems.values());
394 
395         if (!result.isEmpty() && !rootList.isEmpty()) {
396             result.add(new SpacerItem());
397         }
398 
399         final String preferredRootPackage = getResources().getString(
400                 R.string.preferred_root_package, "");
401 
402         final ItemComparator comp = new ItemComparator(preferredRootPackage);
403         Collections.sort(rootList, comp);
404         result.addAll(rootList);
405 
406         mApplicationItemList = rootList;
407 
408         if (profileItem != null) {
409             result.add(new SpacerItem());
410             result.add(profileItem);
411         }
412     }
413 
414     @Override
onResume()415     public void onResume() {
416         super.onResume();
417         onDisplayStateChanged();
418     }
419 
onDisplayStateChanged()420     public void onDisplayStateChanged() {
421         final Context context = getActivity();
422         final State state = ((BaseActivity) context).getDisplayState();
423 
424         if (state.action == State.ACTION_GET_CONTENT) {
425             mList.setOnItemLongClickListener(mItemLongClickListener);
426         } else {
427             mList.setOnItemLongClickListener(null);
428             mList.setLongClickable(false);
429         }
430 
431         LoaderManager.getInstance(this).restartLoader(2, null, mCallbacks);
432     }
433 
onCurrentRootChanged()434     public void onCurrentRootChanged() {
435         if (mAdapter == null) {
436             return;
437         }
438 
439         final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot();
440         for (int i = 0; i < mAdapter.getCount(); i++) {
441             final Object item = mAdapter.getItem(i);
442             if (item instanceof RootItem) {
443                 final RootInfo testRoot = ((RootItem) item).root;
444                 if (Objects.equals(testRoot, root)) {
445                     // b/37358441 should reload all root title after configuration changed
446                     root.title = testRoot.title;
447                     mList.setItemChecked(i, true);
448                     return;
449                 }
450             }
451         }
452     }
453 
454     /**
455      * Attempts to shift focus back to the navigation drawer.
456      */
requestFocus()457     public boolean requestFocus() {
458         return mList.requestFocus();
459     }
460 
getBaseActivity()461     private BaseActivity getBaseActivity() {
462         return (BaseActivity) getActivity();
463     }
464 
465     @Override
onCreateContextMenu( ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)466     public void onCreateContextMenu(
467             ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
468         super.onCreateContextMenu(menu, v, menuInfo);
469         AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo;
470         final Item item = mAdapter.getItem(adapterMenuInfo.position);
471 
472         BaseActivity activity = getBaseActivity();
473         item.createContextMenu(menu, activity.getMenuInflater(), mInjector.menuManager);
474     }
475 
476     @Override
onContextItemSelected(MenuItem item)477     public boolean onContextItemSelected(MenuItem item) {
478         AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) item.getMenuInfo();
479         // There is a possibility that this is called from DirectoryFragment since
480         // all fragments' onContextItemSelected gets called when any menu item is selected
481         // This is to guard against it since DirectoryFragment's RecylerView does not have a
482         // menuInfo
483         if (adapterMenuInfo == null) {
484             return false;
485         }
486         final RootItem rootItem = (RootItem) mAdapter.getItem(adapterMenuInfo.position);
487         switch (item.getItemId()) {
488             case R.id.root_menu_eject_root:
489                 final View ejectIcon = adapterMenuInfo.targetView.findViewById(R.id.action_icon);
490                 ejectClicked(ejectIcon, rootItem.root, mActionHandler);
491                 return true;
492             case R.id.root_menu_open_in_new_window:
493                 mActionHandler.openInNewWindow(new DocumentStack(rootItem.root));
494                 return true;
495             case R.id.root_menu_paste_into_folder:
496                 mActionHandler.pasteIntoFolder(rootItem.root);
497                 return true;
498             case R.id.root_menu_settings:
499                 mActionHandler.openSettings(rootItem.root);
500                 return true;
501             default:
502                 if (DEBUG) {
503                     Log.d(TAG, "Unhandled menu item selected: " + item);
504                 }
505                 return false;
506         }
507     }
508 
getRootDocument(RootItem rootItem, RootUpdater updater)509     private void getRootDocument(RootItem rootItem, RootUpdater updater) {
510         // We need to start a GetRootDocumentTask so we can know whether items can be directly
511         // pasted into root
512         mActionHandler.getRootDocument(
513                 rootItem.root,
514                 CONTEXT_MENU_ITEM_TIMEOUT,
515                 (DocumentInfo doc) -> {
516                     updater.updateDocInfoForRoot(doc);
517                 });
518     }
519 
getItem(View v)520     private Item getItem(View v) {
521         final int pos = (Integer) v.getTag(R.id.item_position_tag);
522         return mAdapter.getItem(pos);
523     }
524 
ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler)525     static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler) {
526         assert(ejectIcon != null);
527         assert(!root.ejecting);
528         ejectIcon.setEnabled(false);
529         root.ejecting = true;
530         actionHandler.ejectRoot(
531                 root,
532                 new BooleanConsumer() {
533                     @Override
534                     public void accept(boolean ejected) {
535                         // Event if ejected is false, we should reset, since the op failed.
536                         // Either way, we are no longer attempting to eject the device.
537                         root.ejecting = false;
538 
539                         // If the view is still visible, we update its state.
540                         if (ejectIcon.getVisibility() == View.VISIBLE) {
541                             ejectIcon.setEnabled(!ejected);
542                         }
543                     }
544                 });
545     }
546 
547     private static class RootComparator implements Comparator<RootItem> {
548         @Override
compare(RootItem lhs, RootItem rhs)549         public int compare(RootItem lhs, RootItem rhs) {
550             return lhs.root.compareTo(rhs.root);
551         }
552     }
553 
554     /**
555      * The comparator of {@link AppItem}, {@link RootItem} and {@link RootAndAppItem}.
556      * Sort by if the item's package name starts with the preferred package name,
557      * then title, then summary. Because the {@link AppItem} doesn't have summary,
558      * it will have lower order than other same title items.
559      */
560     @VisibleForTesting
561     static class ItemComparator implements Comparator<Item> {
562         private final String mPreferredPackageName;
563 
ItemComparator(String preferredPackageName)564         ItemComparator(String preferredPackageName) {
565             mPreferredPackageName = preferredPackageName;
566         }
567 
568         @Override
compare(Item lhs, Item rhs)569         public int compare(Item lhs, Item rhs) {
570             // Sort by whether the item starts with preferred package name
571             if (!mPreferredPackageName.isEmpty()) {
572                 if (lhs.getPackageName().startsWith(mPreferredPackageName)) {
573                     if (!rhs.getPackageName().startsWith(mPreferredPackageName)) {
574                         // lhs starts with it, but rhs doesn't start with it
575                         return -1;
576                     }
577                 } else {
578                     if (rhs.getPackageName().startsWith(mPreferredPackageName)) {
579                         // lhs doesn't start with it, but rhs starts with it
580                         return 1;
581                     }
582                 }
583             }
584 
585             // Sort by title
586             int score = compareToIgnoreCaseNullable(lhs.title, rhs.title);
587             if (score != 0) {
588                 return score;
589             }
590 
591             // Sort by summary. If the item is AppItem, it doesn't have summary.
592             // So, the RootItem or RootAndAppItem will have higher order than AppItem.
593             if (lhs instanceof RootItem) {
594                 return rhs instanceof RootItem ? compareToIgnoreCaseNullable(
595                         ((RootItem) lhs).root.summary, ((RootItem) rhs).root.summary) : 1;
596             }
597             return rhs instanceof RootItem ? -1 : 0;
598         }
599     }
600 
601     @FunctionalInterface
602     interface RootUpdater {
updateDocInfoForRoot(DocumentInfo doc)603         void updateDocInfoForRoot(DocumentInfo doc);
604     }
605 }
606