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.DEBUG;
20 import static com.android.documentsui.base.Shared.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.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 com.android.documentsui.ActionHandler;
51 import com.android.documentsui.BaseActivity;
52 import com.android.documentsui.DocumentsApplication;
53 import com.android.documentsui.DragAndDropHelper;
54 import com.android.documentsui.DragShadowBuilder;
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 implements ItemDragListener.DragHost {
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             mDragListener = new ItemDragListener<RootsFragment>(this) {
196                 @Override
197                 public boolean handleDropEventChecked(View v, DragEvent event) {
198                     final int position = (Integer) v.getTag(R.id.item_position_tag);
199                     final Item item = mAdapter.getItem(position);
200 
201                     assert (item.isDropTarget());
202 
203                     return item.dropOn(event);
204                 }
205             };
206         }
207 
208         mCallbacks = new LoaderCallbacks<Collection<RootInfo>>() {
209             @Override
210             public Loader<Collection<RootInfo>> onCreateLoader(int id, Bundle args) {
211                 return new RootsLoader(activity, providers, state);
212             }
213 
214             @Override
215             public void onLoadFinished(
216                     Loader<Collection<RootInfo>> loader, Collection<RootInfo> result) {
217                 if (!isAdded()) {
218                     return;
219                 }
220 
221                 Intent handlerAppIntent = getArguments().getParcelable(EXTRA_INCLUDE_APPS);
222 
223                 List<Item> sortedItems = sortLoadResult(result, handlerAppIntent);
224                 mAdapter = new RootsAdapter(activity, sortedItems, mDragListener);
225                 mList.setAdapter(mAdapter);
226 
227                 onCurrentRootChanged();
228             }
229 
230             @Override
231             public void onLoaderReset(Loader<Collection<RootInfo>> loader) {
232                 mAdapter = null;
233                 mList.setAdapter(null);
234             }
235         };
236     }
237 
238     /**
239      * @param handlerAppIntent When not null, apps capable of handling the original intent will
240      *            be included in list of roots (in special section at bottom).
241      */
sortLoadResult( Collection<RootInfo> roots, @Nullable Intent handlerAppIntent)242     private List<Item> sortLoadResult(
243             Collection<RootInfo> roots, @Nullable Intent handlerAppIntent) {
244         final List<Item> result = new ArrayList<>();
245 
246         final List<RootItem> libraries = new ArrayList<>();
247         final List<RootItem> others = new ArrayList<>();
248 
249         for (final RootInfo root : roots) {
250             final RootItem item = new RootItem(root, mActionHandler);
251 
252             Activity activity = getActivity();
253             if (root.isHome() && !Shared.shouldShowDocumentsRoot(activity)) {
254                 continue;
255             } else if (root.isLibrary()) {
256                 libraries.add(item);
257             } else {
258                 others.add(item);
259             }
260         }
261 
262         final RootComparator comp = new RootComparator();
263         Collections.sort(libraries, comp);
264         Collections.sort(others, comp);
265 
266         if (VERBOSE) Log.v(TAG, "Adding library roots: " + libraries);
267         result.addAll(libraries);
268         // Only add the spacer if it is actually separating something.
269         if (!libraries.isEmpty() && !others.isEmpty()) {
270             result.add(new SpacerItem());
271         }
272 
273         if (VERBOSE) Log.v(TAG, "Adding plain roots: " + libraries);
274         result.addAll(others);
275 
276         // Include apps that can handle this intent too.
277         if (handlerAppIntent != null) {
278             includeHandlerApps(handlerAppIntent, result);
279         }
280 
281         return result;
282     }
283 
284     /**
285      * Adds apps capable of handling the original intent will be included in list of roots (in
286      * special section at bottom).
287      */
includeHandlerApps(Intent handlerAppIntent, List<Item> result)288     private void includeHandlerApps(Intent handlerAppIntent, List<Item> result) {
289         if (VERBOSE) Log.v(TAG, "Adding handler apps for intent: " + handlerAppIntent);
290         Context context = getContext();
291         final PackageManager pm = context.getPackageManager();
292         final List<ResolveInfo> infos = pm.queryIntentActivities(
293                 handlerAppIntent, PackageManager.MATCH_DEFAULT_ONLY);
294 
295         final List<AppItem> apps = new ArrayList<>();
296 
297         // Omit ourselves from the list
298         for (ResolveInfo info : infos) {
299             if (!context.getPackageName().equals(info.activityInfo.packageName)) {
300                 apps.add(new AppItem(info, mActionHandler));
301             }
302         }
303 
304         if (apps.size() > 0) {
305             result.add(new SpacerItem());
306             result.addAll(apps);
307         }
308     }
309 
310     @Override
onResume()311     public void onResume() {
312         super.onResume();
313         onDisplayStateChanged();
314     }
315 
onDisplayStateChanged()316     public void onDisplayStateChanged() {
317         final Context context = getActivity();
318         final State state = ((BaseActivity) context).getDisplayState();
319 
320         if (state.action == State.ACTION_GET_CONTENT) {
321             mList.setOnItemLongClickListener(mItemLongClickListener);
322         } else {
323             mList.setOnItemLongClickListener(null);
324             mList.setLongClickable(false);
325         }
326 
327         getLoaderManager().restartLoader(2, null, mCallbacks);
328     }
329 
onCurrentRootChanged()330     public void onCurrentRootChanged() {
331         if (mAdapter == null) {
332             return;
333         }
334 
335         final RootInfo root = ((BaseActivity) getActivity()).getCurrentRoot();
336         for (int i = 0; i < mAdapter.getCount(); i++) {
337             final Object item = mAdapter.getItem(i);
338             if (item instanceof RootItem) {
339                 final RootInfo testRoot = ((RootItem) item).root;
340                 if (Objects.equals(testRoot, root)) {
341                     mList.setItemChecked(i, true);
342                     return;
343                 }
344             }
345         }
346     }
347 
348     /**
349      * Attempts to shift focus back to the navigation drawer.
350      */
requestFocus()351     public boolean requestFocus() {
352         return mList.requestFocus();
353     }
354 
getBaseActivity()355     private BaseActivity getBaseActivity() {
356         return (BaseActivity) getActivity();
357     }
358 
359     @Override
runOnUiThread(Runnable runnable)360     public void runOnUiThread(Runnable runnable) {
361         getActivity().runOnUiThread(runnable);
362     }
363 
364     // In RootsFragment, we check whether the item corresponds to a RootItem, and whether
365     // the currently dragged objects can be droppable or not, and change the drop-shadow
366     // accordingly
367     @Override
onDragEntered(View v, Object localState)368     public void onDragEntered(View v, Object localState) {
369         final int pos = (Integer) v.getTag(R.id.item_position_tag);
370         final Item item = mAdapter.getItem(pos);
371 
372         // If a read-only root, no need to see if top level is writable (it's not)
373         if (!(item instanceof RootItem) || !((RootItem) item).root.supportsCreate()) {
374             getBaseActivity().getShadowBuilder().setAppearDroppable(false);
375             v.updateDragShadow(getBaseActivity().getShadowBuilder());
376             return;
377         }
378 
379         final RootItem rootItem = (RootItem) item;
380         getRootDocument(rootItem, (DocumentInfo doc) -> {
381             updateDropShadow(v, localState, rootItem, doc);
382         });
383     }
384 
updateDropShadow( View v, Object localState, RootItem rootItem, DocumentInfo rootDoc)385     private void updateDropShadow(
386             View v, Object localState, RootItem rootItem, DocumentInfo rootDoc) {
387         final DragShadowBuilder shadowBuilder = getBaseActivity().getShadowBuilder();
388         if (rootDoc == null) {
389             Log.e(TAG, "Root DocumentInfo is null. Defaulting to appear not droppable.");
390             shadowBuilder.setAppearDroppable(false);
391         } else {
392             rootItem.docInfo = rootDoc;
393             shadowBuilder.setAppearDroppable(rootDoc.isCreateSupported()
394                     && DragAndDropHelper.canCopyTo(localState, rootDoc));
395         }
396         v.updateDragShadow(shadowBuilder);
397     }
398 
399     // In RootsFragment we always reset the drag shadow as it exits a RootItemView.
400     @Override
onDragExited(View v, Object localState)401     public void onDragExited(View v, Object localState) {
402         getBaseActivity().getShadowBuilder().resetBackground();
403         v.updateDragShadow(getBaseActivity().getShadowBuilder());
404     }
405 
406     // In RootsFragment we open the hovered root.
407     @Override
onViewHovered(View v)408     public void onViewHovered(View v) {
409         // SpacerView doesn't have DragListener so this view is guaranteed to be a RootItemView.
410         RootItemView itemView = (RootItemView) v;
411         itemView.drawRipple();
412 
413         final int position = (Integer) v.getTag(R.id.item_position_tag);
414         final Item item = mAdapter.getItem(position);
415         item.open();
416     }
417 
418     @Override
setDropTargetHighlight(View v, Object localState, boolean highlight)419     public void setDropTargetHighlight(View v, Object localState, boolean highlight) {
420         // SpacerView doesn't have DragListener so this view is guaranteed to be a RootItemView.
421         RootItemView itemView = (RootItemView) v;
422         itemView.setHighlight(highlight);
423     }
424 
425     @Override
onCreateContextMenu( ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)426     public void onCreateContextMenu(
427             ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
428         super.onCreateContextMenu(menu, v, menuInfo);
429         AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) menuInfo;
430         final Item item = mAdapter.getItem(adapterMenuInfo.position);
431 
432         BaseActivity activity = getBaseActivity();
433         item.createContextMenu(menu, activity.getMenuInflater(), mInjector.menuManager);
434     }
435 
436     @Override
onContextItemSelected(MenuItem item)437     public boolean onContextItemSelected(MenuItem item) {
438         AdapterContextMenuInfo adapterMenuInfo = (AdapterContextMenuInfo) item.getMenuInfo();
439         // There is a possibility that this is called from DirectoryFragment since
440         // all fragments' onContextItemSelected gets called when any menu item is selected
441         // This is to guard against it since DirectoryFragment's RecylerView does not have a
442         // menuInfo
443         if (adapterMenuInfo == null) {
444             return false;
445         }
446         final RootItem rootItem = (RootItem) mAdapter.getItem(adapterMenuInfo.position);
447         switch (item.getItemId()) {
448             case R.id.menu_eject_root:
449                 final View ejectIcon = adapterMenuInfo.targetView.findViewById(R.id.eject_icon);
450                 ejectClicked(ejectIcon, rootItem.root, mActionHandler);
451                 return true;
452             case R.id.menu_open_in_new_window:
453                 mActionHandler.openInNewWindow(new DocumentStack(rootItem.root));
454                 return true;
455             case R.id.menu_paste_into_folder:
456                 mActionHandler.pasteIntoFolder(rootItem.root);
457                 return true;
458             case R.id.menu_settings:
459                 mActionHandler.openSettings(rootItem.root);
460                 return true;
461             default:
462                 if (DEBUG) Log.d(TAG, "Unhandled menu item selected: " + item);
463                 return false;
464         }
465     }
466 
467     @FunctionalInterface
468     interface RootUpdater {
updateDocInfoForRoot(DocumentInfo doc)469         void updateDocInfoForRoot(DocumentInfo doc);
470     }
471 
getRootDocument(RootItem rootItem, RootUpdater updater)472     private void getRootDocument(RootItem rootItem, RootUpdater updater) {
473         // We need to start a GetRootDocumentTask so we can know whether items can be directly
474         // pasted into root
475         mActionHandler.getRootDocument(
476                 rootItem.root,
477                 CONTEXT_MENU_ITEM_TIMEOUT,
478                 (DocumentInfo doc) -> {
479                     updater.updateDocInfoForRoot(doc);
480                 });
481     }
482 
ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler)483     static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler) {
484         assert(ejectIcon != null);
485         assert(!root.ejecting);
486         ejectIcon.setEnabled(false);
487         root.ejecting = true;
488         actionHandler.ejectRoot(
489                 root,
490                 new BooleanConsumer() {
491                     @Override
492                     public void accept(boolean ejected) {
493                         // Event if ejected is false, we should reset, since the op failed.
494                         // Either way, we are no longer attempting to eject the device.
495                         root.ejecting = false;
496 
497                         // If the view is still visible, we update its state.
498                         if (ejectIcon.getVisibility() == View.VISIBLE) {
499                             ejectIcon.setEnabled(!ejected);
500                         }
501                     }
502                 });
503     }
504 
505     private static class RootComparator implements Comparator<RootItem> {
506         @Override
compare(RootItem lhs, RootItem rhs)507         public int compare(RootItem lhs, RootItem rhs) {
508             return lhs.root.compareTo(rhs.root);
509         }
510     }
511 }
512