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