• 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.SharedMinimal.DEBUG;
20 import static com.android.documentsui.base.SharedMinimal.VERBOSE;
21 
22 import android.annotation.Nullable;
23 import android.app.Activity;
24 import android.app.Fragment;
25 import android.app.FragmentManager;
26 import android.app.FragmentTransaction;
27 import android.app.LoaderManager.LoaderCallbacks;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.Loader;
31 import android.content.pm.PackageManager;
32 import android.content.pm.ResolveInfo;
33 import android.os.Bundle;
34 import android.provider.DocumentsContract;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.view.ContextMenu;
38 import android.view.DragEvent;
39 import android.view.LayoutInflater;
40 import android.view.MenuItem;
41 import android.view.MotionEvent;
42 import android.view.View;
43 import android.view.View.OnDragListener;
44 import android.view.View.OnGenericMotionListener;
45 import android.view.ViewGroup;
46 import android.widget.AdapterView;
47 import android.widget.AdapterView.AdapterContextMenuInfo;
48 import android.widget.AdapterView.OnItemClickListener;
49 import android.widget.AdapterView.OnItemLongClickListener;
50 import android.widget.ListView;
51 
52 import com.android.documentsui.ActionHandler;
53 import com.android.documentsui.BaseActivity;
54 import com.android.documentsui.DocumentsApplication;
55 import com.android.documentsui.Injector;
56 import com.android.documentsui.Injector.Injected;
57 import com.android.documentsui.ItemDragListener;
58 import com.android.documentsui.R;
59 import com.android.documentsui.base.BooleanConsumer;
60 import com.android.documentsui.base.DocumentInfo;
61 import com.android.documentsui.base.DocumentStack;
62 import com.android.documentsui.base.Events;
63 import com.android.documentsui.base.RootInfo;
64 import com.android.documentsui.base.Shared;
65 import com.android.documentsui.base.State;
66 import com.android.documentsui.roots.ProvidersCache;
67 import com.android.documentsui.roots.RootsLoader;
68 
69 import java.util.ArrayList;
70 import java.util.Collection;
71 import java.util.Collections;
72 import java.util.Comparator;
73 import java.util.List;
74 import java.util.Objects;
75 
76 /**
77  * Display list of known storage backend roots.
78  */
79 public class RootsFragment extends Fragment {
80 
81     private static final String TAG = "RootsFragment";
82     private static final String EXTRA_INCLUDE_APPS = "includeApps";
83     private static final int CONTEXT_MENU_ITEM_TIMEOUT = 500;
84 
85     private final OnItemClickListener mItemListener = new OnItemClickListener() {
86         @Override
87         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
88             final Item item = mAdapter.getItem(position);
89             item.open();
90 
91             getBaseActivity().setRootsDrawerOpen(false);
92         }
93     };
94 
95     private final OnItemLongClickListener mItemLongClickListener = new OnItemLongClickListener() {
96         @Override
97         public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
98             final Item item = mAdapter.getItem(position);
99             return item.showAppDetails();
100         }
101     };
102 
103     private ListView mList;
104     private RootsAdapter mAdapter;
105     private LoaderCallbacks<Collection<RootInfo>> mCallbacks;
106     private @Nullable OnDragListener mDragListener;
107 
108     @Injected
109     private Injector<?> mInjector;
110 
111     @Injected
112     private ActionHandler mActionHandler;
113 
show(FragmentManager fm, Intent includeApps)114     public static RootsFragment show(FragmentManager fm, Intent includeApps) {
115         final Bundle args = new Bundle();
116         args.putParcelable(EXTRA_INCLUDE_APPS, includeApps);
117 
118         final RootsFragment fragment = new RootsFragment();
119         fragment.setArguments(args);
120 
121         final FragmentTransaction ft = fm.beginTransaction();
122         ft.replace(R.id.container_roots, fragment);
123         ft.commitAllowingStateLoss();
124 
125         return fragment;
126     }
127 
get(FragmentManager fm)128     public static RootsFragment get(FragmentManager fm) {
129         return (RootsFragment) fm.findFragmentById(R.id.container_roots);
130     }
131 
132     @Override
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)133     public View onCreateView(
134             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
135 
136         mInjector = getBaseActivity().getInjector();
137 
138         final View view = inflater.inflate(R.layout.fragment_roots, container, false);
139         mList = (ListView) view.findViewById(R.id.roots_list);
140         mList.setOnItemClickListener(mItemListener);
141         // ListView does not have right-click specific listeners, so we will have a
142         // GenericMotionListener to listen for it.
143         // Currently, right click is viewed the same as long press, so we will have to quickly
144         // register for context menu when we receive a right click event, and quickly unregister
145         // it afterwards to prevent context menus popping up upon long presses.
146         // All other motion events will then get passed to OnItemClickListener.
147         mList.setOnGenericMotionListener(
148                 new OnGenericMotionListener() {
149                     @Override
150                     public boolean onGenericMotion(View v, MotionEvent event) {
151                         if (Events.isMouseEvent(event)
152                                 && event.getButtonState() == MotionEvent.BUTTON_SECONDARY) {
153                             int x = (int) event.getX();
154                             int y = (int) event.getY();
155                             return onRightClick(v, x, y, () -> {
156                                 mInjector.menuManager.showContextMenu(
157                                         RootsFragment.this, v, x, y);
158                             });
159                         }
160                         return false;
161             }
162         });
163         mList.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
164         return view;
165     }
166 
onRightClick(View v, int x, int y, Runnable callback)167     private boolean onRightClick(View v, int x, int y, Runnable callback) {
168         final int pos = mList.pointToPosition(x, y);
169         final Item item = mAdapter.getItem(pos);
170 
171         // If a read-only root, no need to see if top level is writable (it's not)
172         if (!(item instanceof RootItem) || !((RootItem) item).root.supportsCreate()) {
173             return false;
174         }
175 
176         final RootItem rootItem = (RootItem) item;
177         getRootDocument(rootItem, (DocumentInfo doc) -> {
178             rootItem.docInfo = doc;
179             callback.run();
180         });
181         return true;
182     }
183 
184     @Override
onActivityCreated(Bundle savedInstanceState)185     public void onActivityCreated(Bundle savedInstanceState) {
186         super.onActivityCreated(savedInstanceState);
187 
188         final BaseActivity activity = getBaseActivity();
189         final ProvidersCache providers = DocumentsApplication.getProvidersCache(activity);
190         final State state = activity.getDisplayState();
191 
192         mActionHandler = mInjector.actions;
193 
194         if (mInjector.config.dragAndDropEnabled()) {
195             final DragHost host = new DragHost(
196                     activity,
197                     DocumentsApplication.getDragAndDropManager(activity),
198                     this::getItem,
199                     mActionHandler);
200             mDragListener = new ItemDragListener<DragHost>(host) {
201                 @Override
202                 public boolean handleDropEventChecked(View v, DragEvent event) {
203                     final Item item = getItem(v);
204 
205                     assert (item.isRoot());
206 
207                     return item.dropOn(event);
208                 }
209             };
210         }
211 
212         mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() {
213             @Override
214             public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) {
215                 return new RootsLoader(activity, providers, state);
216             }
217 
218             @Override
219             public void onLoadFinished(
220                     Loader<Collection<RootInfo>> loader, Collection<RootInfo> roots) {
221                 if (!isAdded()) {
222                     return;
223                 }
224 
225                 Intent handlerAppIntent = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
226 
227                 final Intent intent = activity.getIntent();
228                 final boolean excludeSelf =
229                         intent.getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false);
230                 final String excludePackage = excludeSelf ? activity.getCallingPackage() : null;
231                 List<Item> sortedItems =
232                         sortLoadResult(roots, excludePackage, handlerAppIntent);
233                 mAdapter = new RootsAdapter(activity, sortedItems, mDragListener);
234                 mList.setAdapter(mAdapter);
235 
236                 mInjector.shortcutsUpdater.accept(roots);
237                 onCurrentRootChanged();
238             }
239 
240             @Override
241             public void onLoaderReset(Loader<Collection<RootInfo>> loader) {
242                 mAdapter = null;
243                 mList.setAdapter(null);
244             }
245         };
246     }
247 
248     /**
249      * @param excludePackage Exclude activities from this given package
250      * @param handlerAppIntent When not null, apps capable of handling the original intent will
251      *            be included in list of roots (in special section at bottom).
252      */
sortLoadResult( Collection<RootInfo> roots, @Nullable String excludePackage, @Nullable Intent handlerAppIntent)253     private List<Item> sortLoadResult(
254             Collection<RootInfo> roots,
255             @Nullable String excludePackage,
256             @Nullable Intent handlerAppIntent) {
257         final List<Item> result = new ArrayList<>();
258 
259         final List<RootItem> libraries = new ArrayList<>();
260         final List<RootItem> others = new ArrayList<>();
261 
262         for (final RootInfo root : roots) {
263             final RootItem item = new RootItem(root, mActionHandler);
264 
265             Activity activity = getActivity();
266             if (root.isHome() && !Shared.shouldShowDocumentsRoot(activity)) {
267                 continue;
268             } else if (root.isLibrary()) {
269                 libraries.add(item);
270             } else {
271                 others.add(item);
272             }
273         }
274 
275         final RootComparator comp = new RootComparator();
276         Collections.sort(libraries, comp);
277         Collections.sort(others, comp);
278 
279         if (VERBOSE) Log.v(TAG, "Adding library roots: " + libraries);
280         result.addAll(libraries);
281         // Only add the spacer if it is actually separating something.
282         if (!libraries.isEmpty() && !others.isEmpty()) {
283             result.add(new SpacerItem());
284         }
285 
286         if (VERBOSE) Log.v(TAG, "Adding plain roots: " + libraries);
287         result.addAll(others);
288 
289         // Include apps that can handle this intent too.
290         if (handlerAppIntent != null) {
291             includeHandlerApps(handlerAppIntent, excludePackage, result);
292         }
293 
294         return result;
295     }
296 
297     /**
298      * Adds apps capable of handling the original intent will be included in list of roots (in
299      * special section at bottom).
300      */
includeHandlerApps( Intent handlerAppIntent, @Nullable String excludePackage, List<Item> result)301     private void includeHandlerApps(
302             Intent handlerAppIntent, @Nullable String excludePackage, List<Item> result) {
303         if (VERBOSE) Log.v(TAG, "Adding handler apps for intent: " + handlerAppIntent);
304         Context context = getContext();
305         final PackageManager pm = context.getPackageManager();
306         final List<ResolveInfo> infos = pm.queryIntentActivities(
307                 handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY);
308 
309         final List<AppItem> apps = new ArrayList<>();
310 
311         // Omit ourselves and maybe calling package from the list
312         for (ResolveInfo info : infos) {
313             if (!context.getPackageName().equals(info.activityInfo.packageName) &&
314                     !TextUtils.equals(excludePackage, info.activityInfo.packageName)) {
315                 final AppItem app = new AppItem(info, mActionHandler);
316                 if (VERBOSE) Log.v(TAG, "Adding handler app: " + app);
317                 apps.add(app);
318             }
319         }
320 
321         if (apps.size() > 0) {
322             result.add(new SpacerItem());
323             result.addAll(apps);
324         }
325     }
326 
327     @Override
onResume()328     public void onResume() {
329         super.onResume();
330         onDisplayStateChanged();
331     }
332 
onDisplayStateChanged()333     public void onDisplayStateChanged() {
334         final Context context = getActivity();
335         final State state = ((BaseActivity) context).getDisplayState();
336 
337         if (state.action == State.ACTION_GET_CONTENT) {
338             mList.setOnItemLongClickListener(mItemLongClickListener);
339         } else {
340             mList.setOnItemLongClickListener(null);
341             mList.setLongClickable(false);
342         }
343 
344         getLoaderManager().restartLoader(2, null, mCallbacks);
345     }
346 
onCurrentRootChanged()347     public void onCurrentRootChanged() {
348         if (mAdapter == null) {
349             return;
350         }
351 
352         final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot();
353         for (int i = 0; i < mAdapter.getCount(); i++) {
354             final Object item = mAdapter.getItem(i);
355             if (item instanceof RootItem) {
356                 final RootInfo testRoot = ((RootItem) item).root;
357                 if (Objects.equals(testRoot, root)) {
358                     mList.setItemChecked(i, true);
359                     return;
360                 }
361             }
362         }
363     }
364 
365     /**
366      * Attempts to shift focus back to the navigation drawer.
367      */
requestFocus()368     public boolean requestFocus() {
369         return mList.requestFocus();
370     }
371 
getBaseActivity()372     private BaseActivity getBaseActivity() {
373         return (BaseActivity) getActivity();
374     }
375 
376     @Override
onCreateContextMenu( ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)377     public void onCreateContextMenu(
378             ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
379         super.onCreateContextMenu(menu, v, menuInfo);
380         AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo;
381         final Item item = mAdapter.getItem(adapterMenuInfo.position);
382 
383         BaseActivity activity = getBaseActivity();
384         item.createContextMenu(menu, activity.getMenuInflater(), mInjector.menuManager);
385     }
386 
387     @Override
onContextItemSelected(MenuItem item)388     public boolean onContextItemSelected(MenuItem item) {
389         AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) item.getMenuInfo();
390         // There is a possibility that this is called from DirectoryFragment since
391         // all fragments' onContextItemSelected gets called when any menu item is selected
392         // This is to guard against it since DirectoryFragment's RecylerView does not have a
393         // menuInfo
394         if (adapterMenuInfo == null) {
395             return false;
396         }
397         final RootItem rootItem = (RootItem) mAdapter.getItem(adapterMenuInfo.position);
398         switch (item.getItemId()) {
399             case R.id.root_menu_eject_root:
400                 final View ejectIcon = adapterMenuInfo.targetView.findViewById(R.id.eject_icon);
401                 ejectClicked(ejectIcon, rootItem.root, mActionHandler);
402                 return true;
403             case R.id.root_menu_open_in_new_window:
404                 mActionHandler.openInNewWindow(new DocumentStack(rootItem.root));
405                 return true;
406             case R.id.root_menu_paste_into_folder:
407                 mActionHandler.pasteIntoFolder(rootItem.root);
408                 return true;
409             case R.id.root_menu_settings:
410                 mActionHandler.openSettings(rootItem.root);
411                 return true;
412             default:
413                 if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
414                 return false;
415         }
416     }
417 
getRootDocument(RootItem rootItem, RootUpdater updater)418     private void getRootDocument(RootItem rootItem, RootUpdater updater) {
419         // We need to start a GetRootDocumentTask so we can know whether items can be directly
420         // pasted into root
421         mActionHandler.getRootDocument(
422                 rootItem.root,
423                 CONTEXT_MENU_ITEM_TIMEOUT,
424                 (DocumentInfo doc) -> {
425                     updater.updateDocInfoForRoot(doc);
426                 });
427     }
428 
getItem(View v)429     private Item getItem(View v) {
430         final int pos = (Integer) v.getTag(R.id.item_position_tag);
431         return mAdapter.getItem(pos);
432     }
433 
ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler)434     static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler) {
435         assert(ejectIcon != null);
436         assert(!root.ejecting);
437         ejectIcon.setEnabled(false);
438         root.ejecting = true;
439         actionHandler.ejectRoot(
440                 root,
441                 new BooleanConsumer() {
442                     @Override
443                     public void accept(boolean ejected) {
444                         // Event if ejected is false, we should reset, since the op failed.
445                         // Either way, we are no longer attempting to eject the device.
446                         root.ejecting = false;
447 
448                         // If the view is still visible, we update its state.
449                         if (ejectIcon.getVisibility() == View.VISIBLE) {
450                             ejectIcon.setEnabled(!ejected);
451                         }
452                     }
453                 });
454     }
455 
456     private static class RootComparator implements Comparator<RootItem> {
457         @Override
compare(RootItem lhs, RootItem rhs)458         public int compare(RootItem lhs, RootItem rhs) {
459             return lhs.root.compareTo(rhs.root);
460         }
461     }
462 
463     @FunctionalInterface
464     interface RootUpdater {
updateDocInfoForRoot(DocumentInfo doc)465         void updateDocInfoForRoot(DocumentInfo doc);
466     }
467 }
468