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.dirlist; 18 19 import static com.android.documentsui.base.DocumentInfo.getCursorString; 20 import static com.android.documentsui.base.SharedMinimal.DEBUG; 21 import static com.android.documentsui.base.SharedMinimal.VERBOSE; 22 import static com.android.documentsui.base.State.MODE_GRID; 23 import static com.android.documentsui.base.State.MODE_LIST; 24 25 import android.app.ActivityManager; 26 import android.content.BroadcastReceiver; 27 import android.content.ContentProviderClient; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.IntentFilter; 31 import android.database.Cursor; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.Looper; 36 import android.os.Parcelable; 37 import android.os.UserHandle; 38 import android.provider.DocumentsContract; 39 import android.provider.DocumentsContract.Document; 40 import android.util.Log; 41 import android.util.SparseArray; 42 import android.view.ContextMenu; 43 import android.view.LayoutInflater; 44 import android.view.MenuInflater; 45 import android.view.MenuItem; 46 import android.view.MotionEvent; 47 import android.view.View; 48 import android.view.ViewGroup; 49 import android.view.ViewTreeObserver; 50 import android.widget.ImageView; 51 52 import androidx.annotation.DimenRes; 53 import androidx.annotation.FractionRes; 54 import androidx.annotation.IntDef; 55 import androidx.annotation.Nullable; 56 import androidx.fragment.app.Fragment; 57 import androidx.fragment.app.FragmentActivity; 58 import androidx.fragment.app.FragmentManager; 59 import androidx.fragment.app.FragmentTransaction; 60 import androidx.localbroadcastmanager.content.LocalBroadcastManager; 61 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; 62 import androidx.recyclerview.selection.MutableSelection; 63 import androidx.recyclerview.selection.Selection; 64 import androidx.recyclerview.selection.SelectionTracker; 65 import androidx.recyclerview.selection.SelectionTracker.SelectionPredicate; 66 import androidx.recyclerview.selection.StorageStrategy; 67 import androidx.recyclerview.widget.GridLayoutManager; 68 import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup; 69 import androidx.recyclerview.widget.RecyclerView; 70 import androidx.recyclerview.widget.RecyclerView.RecyclerListener; 71 import androidx.recyclerview.widget.RecyclerView.ViewHolder; 72 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; 73 74 import com.android.documentsui.ActionHandler; 75 import com.android.documentsui.ActionModeController; 76 import com.android.documentsui.BaseActivity; 77 import com.android.documentsui.ContentLock; 78 import com.android.documentsui.DocsSelectionHelper.DocDetailsLookup; 79 import com.android.documentsui.DocumentsApplication; 80 import com.android.documentsui.DragHoverListener; 81 import com.android.documentsui.FocusManager; 82 import com.android.documentsui.Injector; 83 import com.android.documentsui.Injector.ContentScoped; 84 import com.android.documentsui.Injector.Injected; 85 import com.android.documentsui.MetricConsts; 86 import com.android.documentsui.Metrics; 87 import com.android.documentsui.Model; 88 import com.android.documentsui.ProfileTabsController; 89 import com.android.documentsui.R; 90 import com.android.documentsui.ThumbnailCache; 91 import com.android.documentsui.TimeoutTask; 92 import com.android.documentsui.base.DocumentFilters; 93 import com.android.documentsui.base.DocumentInfo; 94 import com.android.documentsui.base.DocumentStack; 95 import com.android.documentsui.base.EventListener; 96 import com.android.documentsui.base.Features; 97 import com.android.documentsui.base.RootInfo; 98 import com.android.documentsui.base.Shared; 99 import com.android.documentsui.base.State; 100 import com.android.documentsui.base.State.ViewMode; 101 import com.android.documentsui.base.UserId; 102 import com.android.documentsui.clipping.ClipStore; 103 import com.android.documentsui.clipping.DocumentClipper; 104 import com.android.documentsui.clipping.UrisSupplier; 105 import com.android.documentsui.dirlist.AnimationView.AnimationType; 106 import com.android.documentsui.picker.PickActivity; 107 import com.android.documentsui.services.FileOperation; 108 import com.android.documentsui.services.FileOperationService; 109 import com.android.documentsui.services.FileOperationService.OpType; 110 import com.android.documentsui.services.FileOperations; 111 import com.android.documentsui.sorting.SortDimension; 112 import com.android.documentsui.sorting.SortModel; 113 114 import com.google.common.base.Objects; 115 116 import java.io.IOException; 117 import java.lang.annotation.Retention; 118 import java.lang.annotation.RetentionPolicy; 119 import java.util.Iterator; 120 import java.util.List; 121 122 /** 123 * Display the documents inside a single directory. 124 */ 125 public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { 126 127 static final int TYPE_NORMAL = 1; 128 static final int TYPE_RECENT_OPEN = 2; 129 130 @IntDef(flag = true, value = { 131 REQUEST_COPY_DESTINATION 132 }) 133 @Retention(RetentionPolicy.SOURCE) 134 public @interface RequestCode {} 135 136 public static final int REQUEST_COPY_DESTINATION = 1; 137 138 static final String TAG = "DirectoryFragment"; 139 140 private static final int CACHE_EVICT_LIMIT = 100; 141 private static final int REFRESH_SPINNER_TIMEOUT = 500; 142 private static final int PROVIDER_MAX_RETRIES = 10; 143 private static final long PROVIDER_TEST_DELAY = 4000; 144 private static final String ACTION_MEDIA_REMOVED = "android.intent.action.MEDIA_REMOVED"; 145 private static final String ACTION_MEDIA_MOUNTED = "android.intent.action.MEDIA_MOUNTED"; 146 private static final String ACTION_MEDIA_EJECT = "android.intent.action.MEDIA_EJECT"; 147 148 private BaseActivity mActivity; 149 150 private State mState; 151 private Model mModel; 152 private final EventListener<Model.Update> mModelUpdateListener = new ModelUpdateListener(); 153 private final DocumentsAdapter.Environment mAdapterEnv = new AdapterEnvironment(); 154 155 @Injected 156 @ContentScoped 157 private Injector<?> mInjector; 158 159 @Injected 160 @ContentScoped 161 private SelectionTracker<String> mSelectionMgr; 162 163 @Injected 164 @ContentScoped 165 private FocusManager mFocusManager; 166 167 @Injected 168 @ContentScoped 169 private ActionHandler mActions; 170 171 @Injected 172 @ContentScoped 173 private ActionModeController mActionModeController; 174 175 @Injected 176 @ContentScoped 177 private ProfileTabsController mProfileTabsController; 178 179 private DocDetailsLookup mDetailsLookup; 180 private SelectionMetadata mSelectionMetadata; 181 private KeyInputHandler mKeyListener; 182 private @Nullable DragHoverListener mDragHoverListener; 183 private View mRootView; 184 private IconHelper mIconHelper; 185 private SwipeRefreshLayout mRefreshLayout; 186 private RecyclerView mRecView; 187 private DocumentsAdapter mAdapter; 188 private DocumentClipper mClipper; 189 private GridLayoutManager mLayout; 190 private int mColumnCount = 1; // This will get updated when layout changes. 191 private int mColumnUnit = 1; 192 193 private float mLiveScale = 1.0f; 194 private @ViewMode int mMode; 195 private int mAppBarHeight; 196 private int mSaveLayoutHeight; 197 198 private View mProgressBar; 199 200 private DirectoryState mLocalState; 201 202 private Handler mHandler; 203 private Runnable mProviderTestRunnable; 204 205 // Note, we use !null to indicate that selection was restored (from rotation). 206 // So don't fiddle with this field unless you've got the bigger picture in mind. 207 private @Nullable Bundle mRestoredState; 208 209 // Blocks loading/reloading of content while user is actively making selection. 210 private ContentLock mContentLock = new ContentLock(); 211 212 private SortModel.UpdateListener mSortListener = (model, updateType) -> { 213 // Only when sort order has changed do we need to trigger another loading. 214 if ((updateType & SortModel.UPDATE_TYPE_SORTING) != 0) { 215 mActions.loadDocumentsForCurrentStack(); 216 } 217 }; 218 219 private final Runnable mOnDisplayStateChanged = this::onDisplayStateChanged; 220 221 private final ViewTreeObserver.OnPreDrawListener mToolbarPreDrawListener = () -> { 222 final boolean appBarHeightChanged = mAppBarHeight != getAppBarLayoutHeight(); 223 if (appBarHeightChanged || mSaveLayoutHeight != getSaveLayoutHeight()) { 224 updateLayout(mState.derivedMode); 225 226 if (appBarHeightChanged) { 227 scrollToTop(); 228 } 229 return false; 230 } 231 return true; 232 }; 233 234 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 235 @Override 236 public void onReceive(Context context, Intent intent) { 237 final String action = intent.getAction(); 238 if (isManagedProfileAction(action)) { 239 UserHandle userHandle = intent.getParcelableExtra(Intent.EXTRA_USER); 240 UserId userId = UserId.of(userHandle); 241 if (Objects.equal(mActivity.getSelectedUser(), userId)) { 242 // We only need to refresh the layout when the selected user is equal to the 243 // received profile user. 244 if (Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) { 245 // If the managed profile is turned off, we need to refresh the directory 246 // to update the UI to show an appropriate error message. 247 if (mProviderTestRunnable != null) { 248 mHandler.removeCallbacks(mProviderTestRunnable); 249 mProviderTestRunnable = null; 250 } 251 onRefresh(); 252 return; 253 } 254 255 // When the managed profile becomes available, the provider may not be available 256 // immediately, we need to check if it is ready before we reload the content. 257 if (Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) { 258 checkUriAndScheduleCheckIfNeeded(userId); 259 } 260 } 261 } 262 } 263 }; 264 265 private final BroadcastReceiver mSdCardBroadcastReceiver = new BroadcastReceiver() { 266 @Override 267 public void onReceive(Context context, Intent intent) { 268 onRefresh(); 269 } 270 }; 271 getSdCardStateChangeFilter()272 private IntentFilter getSdCardStateChangeFilter() { 273 IntentFilter sdCardStateChangeFilter = new IntentFilter(); 274 sdCardStateChangeFilter.addAction(ACTION_MEDIA_REMOVED); 275 sdCardStateChangeFilter.addAction(ACTION_MEDIA_MOUNTED); 276 sdCardStateChangeFilter.addAction(ACTION_MEDIA_EJECT); 277 sdCardStateChangeFilter.addDataScheme("file"); 278 return sdCardStateChangeFilter; 279 } 280 checkUriAndScheduleCheckIfNeeded(UserId userId)281 private void checkUriAndScheduleCheckIfNeeded(UserId userId) { 282 RootInfo currentRoot = mActivity.getCurrentRoot(); 283 DocumentInfo currentDoc = mActivity.getDisplayState().stack.peek(); 284 Uri uri = getCurrentUri(currentRoot, currentDoc); 285 if (isProviderAvailable(uri, userId) || mActivity.isInRecents()) { 286 if (mProviderTestRunnable != null) { 287 mHandler.removeCallbacks(mProviderTestRunnable); 288 mProviderTestRunnable = null; 289 } 290 mHandler.post(() -> onRefresh()); 291 } else { 292 checkUriWithDelay(/* numOfRetries= */1, uri, userId); 293 } 294 } 295 checkUriWithDelay(int numOfRetries, Uri uri, UserId userId)296 private void checkUriWithDelay(int numOfRetries, Uri uri, UserId userId) { 297 mProviderTestRunnable = () -> { 298 RootInfo currentRoot = mActivity.getCurrentRoot(); 299 DocumentInfo currentDoc = mActivity.getDisplayState().stack.peek(); 300 if (mActivity.getSelectedUser().equals(userId) 301 && uri.equals(getCurrentUri(currentRoot, currentDoc))) { 302 if (isProviderAvailable(uri, userId) 303 || userId.isQuietModeEnabled(mActivity) 304 || numOfRetries >= PROVIDER_MAX_RETRIES) { 305 // We stop the recursive check when 306 // 1. the provider is available 307 // 2. the profile is in quiet mode, i.e. provider will not be available 308 // 3. after maximum retries 309 onRefresh(); 310 mProviderTestRunnable = null; 311 } else { 312 Log.d(TAG, "Provider is not available. Retry after " + PROVIDER_TEST_DELAY); 313 checkUriWithDelay(numOfRetries + 1, uri, userId); 314 } 315 } 316 }; 317 mHandler.postDelayed(mProviderTestRunnable, PROVIDER_TEST_DELAY); 318 } 319 getCurrentUri(RootInfo root, @Nullable DocumentInfo doc)320 private Uri getCurrentUri(RootInfo root, @Nullable DocumentInfo doc) { 321 String authority = doc == null ? root.authority : doc.authority; 322 String documentId = doc == null ? root.documentId : doc.documentId; 323 return DocumentsContract.buildDocumentUri(authority, documentId); 324 } 325 isProviderAvailable(Uri uri, UserId userId)326 private boolean isProviderAvailable(Uri uri, UserId userId) { 327 try (ContentProviderClient userClient = 328 DocumentsApplication.acquireUnstableProviderOrThrow( 329 userId.getContentResolver(mActivity), uri.getAuthority())) { 330 Cursor testCursor = userClient.query(uri, /* projection= */ null, 331 /* queryArgs= */null, /* cancellationSignal= */ null); 332 if (testCursor != null) { 333 return true; 334 } 335 } catch (Exception e) { 336 // Provider is not available. Ignore. 337 } 338 return false; 339 } 340 isManagedProfileAction(String action)341 private static boolean isManagedProfileAction(String action) { 342 return Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action) 343 || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action); 344 } 345 346 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)347 public View onCreateView( 348 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 349 350 mHandler = new Handler(Looper.getMainLooper()); 351 mActivity = (BaseActivity) getActivity(); 352 mRootView = inflater.inflate(R.layout.fragment_directory, container, false); 353 354 mProgressBar = mRootView.findViewById(R.id.progressbar); 355 assert mProgressBar != null; 356 357 mRecView = (RecyclerView) mRootView.findViewById(R.id.dir_list); 358 mRecView.setRecyclerListener( 359 new RecyclerListener() { 360 @Override 361 public void onViewRecycled(ViewHolder holder) { 362 cancelThumbnailTask(holder.itemView); 363 } 364 }); 365 366 mRefreshLayout = (SwipeRefreshLayout) mRootView.findViewById(R.id.refresh_layout); 367 mRefreshLayout.setOnRefreshListener(this); 368 mRecView.setItemAnimator(new DirectoryItemAnimator()); 369 370 mInjector = mActivity.getInjector(); 371 // Initially, this selection tracker (delegator) uses a stub implementation, so it must be 372 // updated (reset) when necessary things are ready. 373 mSelectionMgr = mInjector.selectionMgr; 374 mModel = mInjector.getModel(); 375 mModel.reset(); 376 377 mInjector.actions.registerDisplayStateChangedListener(mOnDisplayStateChanged); 378 379 mClipper = DocumentsApplication.getDocumentClipper(getContext()); 380 if (mInjector.config.dragAndDropEnabled()) { 381 DirectoryDragListener listener = new DirectoryDragListener( 382 new DragHost<>( 383 mActivity, 384 DocumentsApplication.getDragAndDropManager(mActivity), 385 mSelectionMgr, 386 mInjector.actions, 387 mActivity.getDisplayState(), 388 mInjector.dialogs, 389 (View v) -> { 390 return getModelId(v) != null; 391 }, 392 this::getDocumentHolder, 393 this::getDestination 394 )); 395 mDragHoverListener = DragHoverListener.create(listener, mRecView); 396 } 397 // Make the recycler and the empty views responsive to drop events when allowed. 398 mRecView.setOnDragListener(mDragHoverListener); 399 400 setPreDrawListenerEnabled(true); 401 402 return mRootView; 403 } 404 405 @Override onDestroyView()406 public void onDestroyView() { 407 mInjector.actions.unregisterDisplayStateChangedListener(mOnDisplayStateChanged); 408 if (mState.supportsCrossProfile()) { 409 LocalBroadcastManager.getInstance(mActivity).unregisterReceiver(mReceiver); 410 if (mProviderTestRunnable != null) { 411 mHandler.removeCallbacks(mProviderTestRunnable); 412 } 413 } 414 getContext().unregisterReceiver(mSdCardBroadcastReceiver); 415 416 // Cancel any outstanding thumbnail requests 417 final int count = mRecView.getChildCount(); 418 for (int i = 0; i < count; i++) { 419 final View view = mRecView.getChildAt(i); 420 cancelThumbnailTask(view); 421 } 422 423 mModel.removeUpdateListener(mModelUpdateListener); 424 mModel.removeUpdateListener(mAdapter.getModelUpdateListener()); 425 setPreDrawListenerEnabled(false); 426 427 super.onDestroyView(); 428 } 429 430 @Override onActivityCreated(Bundle savedInstanceState)431 public void onActivityCreated(Bundle savedInstanceState) { 432 super.onActivityCreated(savedInstanceState); 433 434 mState = mActivity.getDisplayState(); 435 436 // Read arguments when object created for the first time. 437 // Restore state if fragment recreated. 438 Bundle args = savedInstanceState == null ? getArguments() : savedInstanceState; 439 mRestoredState = args; 440 441 mLocalState = new DirectoryState(); 442 mLocalState.restore(args); 443 if (mLocalState.mSelectionId == null) { 444 mLocalState.mSelectionId = Integer.toHexString(System.identityHashCode(mRecView)); 445 } 446 447 mIconHelper = new IconHelper(mActivity, MODE_GRID, mState.supportsCrossProfile()); 448 449 mAdapter = new DirectoryAddonsAdapter( 450 mAdapterEnv, 451 new ModelBackedDocumentsAdapter(mAdapterEnv, mIconHelper, mInjector.fileTypeLookup) 452 ); 453 454 mRecView.setAdapter(mAdapter); 455 456 mLayout = new GridLayoutManager(getContext(), mColumnCount) { 457 @Override 458 public void onLayoutCompleted(RecyclerView.State state) { 459 super.onLayoutCompleted(state); 460 mFocusManager.onLayoutCompleted(); 461 } 462 }; 463 464 SpanSizeLookup lookup = mAdapter.createSpanSizeLookup(); 465 if (lookup != null) { 466 mLayout.setSpanSizeLookup(lookup); 467 } 468 mRecView.setLayoutManager(mLayout); 469 470 mModel.addUpdateListener(mAdapter.getModelUpdateListener()); 471 mModel.addUpdateListener(mModelUpdateListener); 472 473 SelectionPredicate<String> selectionPredicate = 474 new DocsSelectionPredicate(mInjector.config, mState, mModel, mRecView); 475 476 mFocusManager = mInjector.getFocusManager(mRecView, mModel); 477 mActions = mInjector.getActionHandler(mContentLock); 478 479 mRecView.setAccessibilityDelegateCompat( 480 new AccessibilityEventRouter(mRecView, 481 (View child) -> onAccessibilityClick(child), 482 (View child) -> onAccessibilityLongClick(child))); 483 mSelectionMetadata = new SelectionMetadata(mModel::getItem); 484 mDetailsLookup = new DocsItemDetailsLookup(mRecView); 485 486 DragStartListener dragStartListener = mInjector.config.dragAndDropEnabled() 487 ? DragStartListener.create( 488 mIconHelper, 489 mModel, 490 mSelectionMgr, 491 mSelectionMetadata, 492 mState, 493 this::getModelId, 494 mRecView::findChildViewUnder, 495 DocumentsApplication.getDragAndDropManager(mActivity)) 496 : DragStartListener.STUB; 497 498 { 499 // Limiting the scope of the localTracker so nobody uses it. 500 // This block initializes/updates the global SelectionTracker held in mSelectionMgr. 501 SelectionTracker<String> localTracker = new SelectionTracker.Builder<>( 502 mLocalState.mSelectionId, 503 mRecView, 504 new DocsStableIdProvider(mAdapter), 505 mDetailsLookup, 506 StorageStrategy.createStringStorage()) 507 .withBandOverlay(R.drawable.band_select_overlay) 508 .withFocusDelegate(mFocusManager) 509 .withOnDragInitiatedListener(dragStartListener::onDragEvent) 510 .withOnContextClickListener(this::onContextMenuClick) 511 .withOnItemActivatedListener(this::onItemActivated) 512 .withOperationMonitor(mContentLock.getMonitor()) 513 .withSelectionPredicate(selectionPredicate) 514 .withGestureTooltypes(MotionEvent.TOOL_TYPE_FINGER, 515 MotionEvent.TOOL_TYPE_STYLUS) 516 .build(); 517 mInjector.updateSharedSelectionTracker(localTracker); 518 } 519 520 mSelectionMgr.addObserver(mSelectionMetadata); 521 522 // Construction of the input handlers is non trivial, so to keep logic clear, 523 // and code flexible, and DirectoryFragment small, the construction has been 524 // moved off into a separate class. 525 InputHandlers handlers = new InputHandlers( 526 mActions, 527 mSelectionMgr, 528 selectionPredicate, 529 mFocusManager, 530 mRecView); 531 532 // This little guy gets added to each Holder, so that we can be notified of key events 533 // on RecyclerView items. 534 mKeyListener = handlers.createKeyHandler(); 535 536 if (DEBUG) { 537 new ScaleHelper(this.getContext(), mInjector.features, this::scaleLayout) 538 .attach(mRecView); 539 } 540 541 new RefreshHelper(mRefreshLayout::setEnabled) 542 .attach(mRecView); 543 544 mActionModeController = mInjector.getActionModeController( 545 mSelectionMetadata, 546 this::handleMenuItemClick); 547 548 mSelectionMgr.addObserver(mActionModeController); 549 550 mProfileTabsController = mInjector.profileTabsController; 551 mSelectionMgr.addObserver(mProfileTabsController); 552 553 final ActivityManager am = (ActivityManager) mActivity.getSystemService( 554 Context.ACTIVITY_SERVICE); 555 boolean svelte = am.isLowRamDevice() && (mState.stack.isRecents()); 556 mIconHelper.setThumbnailsEnabled(!svelte); 557 558 // If mDocument is null, we sort it by last modified by default because it's in Recents. 559 final boolean prefersLastModified = 560 (mLocalState.mDocument == null) 561 || mLocalState.mDocument.prefersSortByLastModified(); 562 // Call this before adding the listener to avoid restarting the loader one more time 563 mState.sortModel.setDefaultDimension( 564 prefersLastModified 565 ? SortModel.SORT_DIMENSION_ID_DATE 566 : SortModel.SORT_DIMENSION_ID_TITLE); 567 568 // Kick off loader at least once 569 mActions.loadDocumentsForCurrentStack(); 570 571 if (mState.supportsCrossProfile()) { 572 final IntentFilter filter = new IntentFilter(); 573 filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNLOCKED); 574 filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE); 575 // DocumentsApplication will resend the broadcast locally after roots are updated. 576 // Register to a local broadcast manager to avoid this fragment from updating before 577 // roots are updated. 578 LocalBroadcastManager.getInstance(mActivity).registerReceiver(mReceiver, filter); 579 } 580 getContext().registerReceiver(mSdCardBroadcastReceiver, getSdCardStateChangeFilter()); 581 } 582 583 @Override onStart()584 public void onStart() { 585 super.onStart(); 586 587 // Add listener to update contents on sort model change 588 mState.sortModel.addListener(mSortListener); 589 // After SD card is formatted, we go out of the view and come back. Similarly when users 590 // go out of the app to delete some files, we want to refresh the directory. 591 onRefresh(); 592 } 593 594 @Override onStop()595 public void onStop() { 596 super.onStop(); 597 598 mState.sortModel.removeListener(mSortListener); 599 600 // Remember last scroll location 601 final SparseArray<Parcelable> container = new SparseArray<>(); 602 getView().saveHierarchyState(container); 603 mState.dirConfigs.put(mLocalState.getConfigKey(), container); 604 } 605 606 @Override onSaveInstanceState(Bundle outState)607 public void onSaveInstanceState(Bundle outState) { 608 super.onSaveInstanceState(outState); 609 610 mLocalState.save(outState); 611 mSelectionMgr.onSaveInstanceState(outState); 612 } 613 614 @Override onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)615 public void onCreateContextMenu(ContextMenu menu, 616 View v, 617 ContextMenu.ContextMenuInfo menuInfo) { 618 super.onCreateContextMenu(menu, v, menuInfo); 619 final MenuInflater inflater = getActivity().getMenuInflater(); 620 621 final String modelId = getModelId(v); 622 if (modelId == null) { 623 // TODO: inject DirectoryDetails into MenuManager constructor 624 // Since both classes are supplied by Activity and created 625 // at the same time. 626 mInjector.menuManager.inflateContextMenuForContainer( 627 menu, inflater, mSelectionMetadata); 628 } else { 629 mInjector.menuManager.inflateContextMenuForDocs( 630 menu, inflater, mSelectionMetadata); 631 } 632 } 633 634 @Override onContextItemSelected(MenuItem item)635 public boolean onContextItemSelected(MenuItem item) { 636 return handleMenuItemClick(item); 637 } 638 onCopyDestinationPicked(int resultCode, Intent data)639 private void onCopyDestinationPicked(int resultCode, Intent data) { 640 641 FileOperation operation = mLocalState.claimPendingOperation(); 642 643 if (resultCode == FragmentActivity.RESULT_CANCELED || data == null) { 644 // User pressed the back button or otherwise cancelled the destination pick. Don't 645 // proceed with the copy. 646 operation.dispose(); 647 return; 648 } 649 650 operation.setDestination(data.getParcelableExtra(Shared.EXTRA_STACK)); 651 final String jobId = FileOperations.createJobId(); 652 mInjector.dialogs.showProgressDialog(jobId, operation); 653 FileOperations.start( 654 mActivity, 655 operation, 656 mInjector.dialogs::showFileOperationStatus, 657 jobId); 658 } 659 660 // TODO: Move to UserInputHander. onContextMenuClick(MotionEvent e)661 protected boolean onContextMenuClick(MotionEvent e) { 662 663 if (mDetailsLookup.overItemWithSelectionKey(e)) { 664 View childView = mRecView.findChildViewUnder(e.getX(), e.getY()); 665 ViewHolder holder = mRecView.getChildViewHolder(childView); 666 667 View view = holder.itemView; 668 float x = e.getX() - view.getLeft(); 669 float y = e.getY() - view.getTop(); 670 mInjector.menuManager.showContextMenu(this, view, x, y); 671 return true; 672 } 673 674 mInjector.menuManager.showContextMenu(this, mRecView, e.getX(), e.getY()); 675 return true; 676 } 677 onItemActivated(ItemDetails<String> item, MotionEvent e)678 private boolean onItemActivated(ItemDetails<String> item, MotionEvent e) { 679 if (((DocumentItemDetails) item).inPreviewIconHotspot(e)) { 680 return mActions.previewItem(item); 681 } 682 683 return mActions.openItem( 684 item, 685 ActionHandler.VIEW_TYPE_PREVIEW, 686 ActionHandler.VIEW_TYPE_REGULAR); 687 } 688 onViewModeChanged()689 public void onViewModeChanged() { 690 // Mode change is just visual change; no need to kick loader. 691 mRootView.announceForAccessibility(getString( 692 mState.derivedMode == State.MODE_GRID ? R.string.grid_mode_showing 693 : R.string.list_mode_showing)); 694 onDisplayStateChanged(); 695 } 696 onDisplayStateChanged()697 private void onDisplayStateChanged() { 698 updateLayout(mState.derivedMode); 699 mRecView.setAdapter(mAdapter); 700 } 701 702 /** 703 * Updates the layout after the view mode switches. 704 * 705 * @param mode The new view mode. 706 */ updateLayout(@iewMode int mode)707 private void updateLayout(@ViewMode int mode) { 708 mMode = mode; 709 mColumnCount = calculateColumnCount(mode); 710 if (mLayout != null) { 711 mLayout.setSpanCount(mColumnCount); 712 } 713 714 int pad = getDirectoryPadding(mode); 715 mAppBarHeight = getAppBarLayoutHeight(); 716 mSaveLayoutHeight = getSaveLayoutHeight(); 717 mRecView.setPadding(pad, mAppBarHeight, pad, mSaveLayoutHeight); 718 mRecView.requestLayout(); 719 mIconHelper.setViewMode(mode); 720 721 int range = getResources().getDimensionPixelOffset(R.dimen.refresh_icon_range); 722 mRefreshLayout.setProgressViewOffset(true, mAppBarHeight, mAppBarHeight + range); 723 } 724 getAppBarLayoutHeight()725 private int getAppBarLayoutHeight() { 726 View appBarLayout = getActivity().findViewById(R.id.app_bar); 727 View collapsingBar = getActivity().findViewById(R.id.collapsing_toolbar); 728 return collapsingBar == null ? 0 : appBarLayout.getHeight(); 729 } 730 getSaveLayoutHeight()731 private int getSaveLayoutHeight() { 732 View containerSave = getActivity().findViewById(R.id.container_save); 733 return containerSave == null ? 0 : containerSave.getHeight(); 734 } 735 736 /** 737 * Updates the layout after the view mode switches. 738 * 739 * @param mode The new view mode. 740 */ scaleLayout(float scale)741 private void scaleLayout(float scale) { 742 assert DEBUG; 743 744 if (VERBOSE) { 745 Log.v( 746 TAG, "Handling scale event: " + scale + ", existing scale: " + mLiveScale); 747 } 748 749 if (mMode == MODE_GRID) { 750 float minScale = getFraction(R.fraction.grid_scale_min); 751 float maxScale = getFraction(R.fraction.grid_scale_max); 752 float nextScale = mLiveScale * scale; 753 754 if (VERBOSE) { 755 Log.v(TAG, 756 "Next scale " + nextScale + ", Min/max scale " + minScale + "/" + maxScale); 757 } 758 759 if (nextScale > minScale && nextScale < maxScale) { 760 if (DEBUG) { 761 Log.d(TAG, "Updating grid scale: " + scale); 762 } 763 mLiveScale = nextScale; 764 updateLayout(mMode); 765 } 766 767 } else { 768 if (DEBUG) { 769 Log.d(TAG, "List mode, ignoring scale: " + scale); 770 } 771 mLiveScale = 1.0f; 772 } 773 } 774 calculateColumnCount(@iewMode int mode)775 private int calculateColumnCount(@ViewMode int mode) { 776 // For fixing a11y issue b/141223688, if there's only "no items" displayed, we should set 777 // span column to 1 to avoid talkback speaking unnecessary information. 778 if (mModel != null && mModel.getItemCount() == 0) { 779 return 1; 780 } 781 782 if (mode == MODE_LIST) { 783 // List mode is a "grid" with 1 column. 784 return 1; 785 } 786 787 int cellWidth = getScaledSize(R.dimen.grid_width); 788 int cellMargin = 2 * getScaledSize(R.dimen.grid_item_margin); 789 int viewPadding = 790 (int) ((mRecView.getPaddingLeft() + mRecView.getPaddingRight()) * mLiveScale); 791 792 // RecyclerView sometimes gets a width of 0 (see b/27150284). 793 // Clamp so that we always lay out the grid with at least 2 columns by default. 794 // If on photo picking state, the UI should show 3 images a row or 2 folders a row, 795 // so use 6 columns by default and set folder size to 3 and document size is to 2. 796 mColumnUnit = mState.isPhotoPicking() ? 3 : 1; 797 int columnCount = mColumnUnit * Math.max(2, 798 (mRecView.getWidth() - viewPadding) / (cellWidth + cellMargin)); 799 800 // Finally with our grid count logic firmly in place, we apply any live scaling 801 // captured by the scale gesture detector. 802 return Math.max(1, Math.round(columnCount / mLiveScale)); 803 } 804 805 806 /** 807 * Moderately abuse the "fraction" resource type for our purposes. 808 */ getFraction(@ractionRes int id)809 private float getFraction(@FractionRes int id) { 810 return getResources().getFraction(id, 1, 0); 811 } 812 getScaledSize(@imenRes int id)813 private int getScaledSize(@DimenRes int id) { 814 return (int) (getResources().getDimensionPixelSize(id) * mLiveScale); 815 } 816 getDirectoryPadding(@iewMode int mode)817 private int getDirectoryPadding(@ViewMode int mode) { 818 switch (mode) { 819 case MODE_GRID: 820 return getResources().getDimensionPixelSize(R.dimen.grid_container_padding); 821 case MODE_LIST: 822 return getResources().getDimensionPixelSize(R.dimen.list_container_padding); 823 default: 824 throw new IllegalArgumentException("Unsupported layout mode: " + mode); 825 } 826 } 827 handleMenuItemClick(MenuItem item)828 private boolean handleMenuItemClick(MenuItem item) { 829 if (mInjector.pickResult != null) { 830 mInjector.pickResult.increaseActionCount(); 831 } 832 MutableSelection<String> selection = new MutableSelection<>(); 833 mSelectionMgr.copySelection(selection); 834 835 switch (item.getItemId()) { 836 case R.id.action_menu_select: 837 case R.id.dir_menu_open: 838 openDocuments(selection); 839 mActionModeController.finishActionMode(); 840 return true; 841 842 case R.id.action_menu_open_with: 843 case R.id.dir_menu_open_with: 844 showChooserForDoc(selection); 845 return true; 846 847 case R.id.dir_menu_open_in_new_window: 848 mActions.openSelectedInNewWindow(); 849 return true; 850 851 case R.id.action_menu_share: 852 case R.id.dir_menu_share: 853 mActions.shareSelectedDocuments(); 854 return true; 855 856 case R.id.action_menu_delete: 857 case R.id.dir_menu_delete: 858 // deleteDocuments will end action mode if the documents are deleted. 859 // It won't end action mode if user cancels the delete. 860 mActions.showDeleteDialog(); 861 return true; 862 863 case R.id.action_menu_copy_to: 864 transferDocuments(selection, null, FileOperationService.OPERATION_COPY); 865 // TODO: Only finish selection mode if copy-to is not canceled. 866 // Need to plum down into handling the way we do with deleteDocuments. 867 mActionModeController.finishActionMode(); 868 return true; 869 870 case R.id.action_menu_compress: 871 transferDocuments(selection, mState.stack, 872 FileOperationService.OPERATION_COMPRESS); 873 // TODO: Only finish selection mode if compress is not canceled. 874 // Need to plum down into handling the way we do with deleteDocuments. 875 mActionModeController.finishActionMode(); 876 return true; 877 878 // TODO: Implement extract (to the current directory). 879 case R.id.action_menu_extract_to: 880 transferDocuments(selection, null, FileOperationService.OPERATION_EXTRACT); 881 // TODO: Only finish selection mode if compress-to is not canceled. 882 // Need to plum down into handling the way we do with deleteDocuments. 883 mActionModeController.finishActionMode(); 884 return true; 885 886 case R.id.action_menu_move_to: 887 if (mModel.hasDocuments(selection, DocumentFilters.NOT_MOVABLE)) { 888 mInjector.dialogs.showOperationUnsupported(); 889 return true; 890 } 891 // Exit selection mode first, so we avoid deselecting deleted documents. 892 mActionModeController.finishActionMode(); 893 transferDocuments(selection, null, FileOperationService.OPERATION_MOVE); 894 return true; 895 896 case R.id.action_menu_inspect: 897 case R.id.dir_menu_inspect: 898 mActionModeController.finishActionMode(); 899 assert selection.size() <= 1; 900 DocumentInfo doc = selection.isEmpty() 901 ? mActivity.getCurrentDirectory() 902 : mModel.getDocuments(selection).get(0); 903 904 mActions.showInspector(doc); 905 return true; 906 907 case R.id.dir_menu_cut_to_clipboard: 908 mActions.cutToClipboard(); 909 return true; 910 911 case R.id.dir_menu_copy_to_clipboard: 912 mActions.copyToClipboard(); 913 return true; 914 915 case R.id.dir_menu_paste_from_clipboard: 916 pasteFromClipboard(); 917 return true; 918 919 case R.id.dir_menu_paste_into_folder: 920 pasteIntoFolder(); 921 return true; 922 923 case R.id.action_menu_select_all: 924 case R.id.dir_menu_select_all: 925 mActions.selectAllFiles(); 926 return true; 927 928 case R.id.action_menu_deselect_all: 929 case R.id.dir_menu_deselect_all: 930 mActions.deselectAllFiles(); 931 return true; 932 933 case R.id.action_menu_rename: 934 case R.id.dir_menu_rename: 935 renameDocuments(selection); 936 return true; 937 938 case R.id.dir_menu_create_dir: 939 mActions.showCreateDirectoryDialog(); 940 return true; 941 942 case R.id.dir_menu_view_in_owner: 943 mActions.viewInOwner(); 944 return true; 945 946 case R.id.action_menu_sort: 947 mActions.showSortDialog(); 948 return true; 949 950 default: 951 if (DEBUG) { 952 Log.d(TAG, "Unhandled menu item selected: " + item); 953 } 954 return false; 955 } 956 } 957 onAccessibilityClick(View child)958 private boolean onAccessibilityClick(View child) { 959 if (mSelectionMgr.hasSelection()) { 960 selectItem(child); 961 } else { 962 DocumentHolder holder = getDocumentHolder(child); 963 mActions.openItem(holder.getItemDetails(), ActionHandler.VIEW_TYPE_PREVIEW, 964 ActionHandler.VIEW_TYPE_REGULAR); 965 } 966 return true; 967 } 968 onAccessibilityLongClick(View child)969 private boolean onAccessibilityLongClick(View child) { 970 selectItem(child); 971 return true; 972 } 973 selectItem(View child)974 private void selectItem(View child) { 975 final String id = getModelId(child); 976 if (mSelectionMgr.isSelected(id)) { 977 mSelectionMgr.deselect(id); 978 } else { 979 mSelectionMgr.select(id); 980 } 981 } 982 cancelThumbnailTask(View view)983 private void cancelThumbnailTask(View view) { 984 final ImageView iconThumb = (ImageView) view.findViewById(R.id.icon_thumb); 985 if (iconThumb != null) { 986 mIconHelper.stopLoading(iconThumb); 987 } 988 } 989 990 // Support for opening multiple documents is currently exclusive to DocumentsActivity. openDocuments(final Selection selected)991 private void openDocuments(final Selection selected) { 992 Metrics.logUserAction(MetricConsts.USER_ACTION_OPEN); 993 994 if (selected.isEmpty()) { 995 return; 996 } 997 998 // Model must be accessed in UI thread, since underlying cursor is not threadsafe. 999 List<DocumentInfo> docs = mModel.getDocuments(selected); 1000 if (docs.size() > 1) { 1001 mActivity.onDocumentsPicked(docs); 1002 } else { 1003 mActivity.onDocumentPicked(docs.get(0)); 1004 } 1005 } 1006 showChooserForDoc(final Selection<String> selected)1007 private void showChooserForDoc(final Selection<String> selected) { 1008 Metrics.logUserAction(MetricConsts.USER_ACTION_OPEN); 1009 1010 if (selected.isEmpty()) { 1011 return; 1012 } 1013 1014 assert selected.size() == 1; 1015 DocumentInfo doc = 1016 DocumentInfo.fromDirectoryCursor(mModel.getItem(selected.iterator().next())); 1017 mActions.showChooserForDoc(doc); 1018 } 1019 transferDocuments( final Selection<String> selected, @Nullable DocumentStack destination, final @OpType int mode)1020 private void transferDocuments( 1021 final Selection<String> selected, @Nullable DocumentStack destination, 1022 final @OpType int mode) { 1023 if (selected.isEmpty()) { 1024 return; 1025 } 1026 1027 switch (mode) { 1028 case FileOperationService.OPERATION_COPY: 1029 Metrics.logUserAction(MetricConsts.USER_ACTION_COPY_TO); 1030 break; 1031 case FileOperationService.OPERATION_COMPRESS: 1032 Metrics.logUserAction(MetricConsts.USER_ACTION_COMPRESS); 1033 break; 1034 case FileOperationService.OPERATION_EXTRACT: 1035 Metrics.logUserAction(MetricConsts.USER_ACTION_EXTRACT_TO); 1036 break; 1037 case FileOperationService.OPERATION_MOVE: 1038 Metrics.logUserAction(MetricConsts.USER_ACTION_MOVE_TO); 1039 break; 1040 } 1041 1042 UrisSupplier srcs; 1043 try { 1044 ClipStore clipStorage = DocumentsApplication.getClipStore(getContext()); 1045 srcs = UrisSupplier.create(selected, mModel::getItemUri, clipStorage); 1046 } catch (IOException e) { 1047 throw new RuntimeException("Failed to create uri supplier.", e); 1048 } 1049 1050 final DocumentInfo parent = mActivity.getCurrentDirectory(); 1051 final FileOperation operation = new FileOperation.Builder() 1052 .withOpType(mode) 1053 .withSrcParent(parent == null ? null : parent.derivedUri) 1054 .withSrcs(srcs) 1055 .build(); 1056 1057 if (destination != null) { 1058 operation.setDestination(destination); 1059 final String jobId = FileOperations.createJobId(); 1060 mInjector.dialogs.showProgressDialog(jobId, operation); 1061 FileOperations.start( 1062 mActivity, 1063 operation, 1064 mInjector.dialogs::showFileOperationStatus, 1065 jobId); 1066 return; 1067 } 1068 1069 // Pop up a dialog to pick a destination. This is inadequate but works for now. 1070 // TODO: Implement a picker that is to spec. 1071 mLocalState.mPendingOperation = operation; 1072 final Intent intent = new Intent( 1073 Shared.ACTION_PICK_COPY_DESTINATION, 1074 Uri.EMPTY, 1075 getActivity(), 1076 PickActivity.class); 1077 1078 // Set an appropriate title on the drawer when it is shown in the picker. 1079 // Coupled with the fact that we auto-open the drawer for copy/move operations 1080 // it should basically be the thing people see first. 1081 int drawerTitleId; 1082 switch (mode) { 1083 case FileOperationService.OPERATION_COPY: 1084 drawerTitleId = R.string.menu_copy; 1085 break; 1086 case FileOperationService.OPERATION_COMPRESS: 1087 drawerTitleId = R.string.menu_compress; 1088 break; 1089 case FileOperationService.OPERATION_EXTRACT: 1090 drawerTitleId = R.string.menu_extract; 1091 break; 1092 case FileOperationService.OPERATION_MOVE: 1093 drawerTitleId = R.string.menu_move; 1094 break; 1095 default: 1096 throw new UnsupportedOperationException("Unknown mode: " + mode); 1097 } 1098 1099 intent.putExtra(DocumentsContract.EXTRA_PROMPT, drawerTitleId); 1100 1101 // Model must be accessed in UI thread, since underlying cursor is not threadsafe. 1102 List<DocumentInfo> docs = mModel.getDocuments(selected); 1103 1104 // Determine if there is a directory in the set of documents 1105 // to be copied? Why? Directory creation isn't supported by some roots 1106 // (like Downloads). This informs DocumentsActivity (the "picker") 1107 // to restrict available roots to just those with support. 1108 intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mode); 1109 1110 // This just identifies the type of request...we'll check it 1111 // when we reveive a response. 1112 startActivityForResult(intent, REQUEST_COPY_DESTINATION); 1113 } 1114 1115 @Override onActivityResult(@equestCode int requestCode, int resultCode, Intent data)1116 public void onActivityResult(@RequestCode int requestCode, int resultCode, Intent data) { 1117 switch (requestCode) { 1118 case REQUEST_COPY_DESTINATION: 1119 onCopyDestinationPicked(resultCode, data); 1120 break; 1121 default: 1122 throw new UnsupportedOperationException("Unknown request code: " + requestCode); 1123 } 1124 } 1125 renameDocuments(Selection selected)1126 private void renameDocuments(Selection selected) { 1127 Metrics.logUserAction(MetricConsts.USER_ACTION_RENAME); 1128 1129 if (selected.isEmpty()) { 1130 return; 1131 } 1132 1133 // Batch renaming not supported 1134 // Rename option is only available in menu when 1 document selected 1135 assert selected.size() == 1; 1136 1137 // Model must be accessed in UI thread, since underlying cursor is not threadsafe. 1138 List<DocumentInfo> docs = mModel.getDocuments(selected); 1139 RenameDocumentFragment.show(getChildFragmentManager(), docs.get(0)); 1140 } 1141 getModel()1142 Model getModel() { 1143 return mModel; 1144 } 1145 1146 /** 1147 * Paste selection files from the primary clip into the current window. 1148 */ pasteFromClipboard()1149 public void pasteFromClipboard() { 1150 Metrics.logUserAction(MetricConsts.USER_ACTION_PASTE_CLIPBOARD); 1151 // Since we are pasting into the current window, we already have the destination in the 1152 // stack. No need for a destination DocumentInfo. 1153 mClipper.copyFromClipboard( 1154 mState.stack, 1155 mInjector.dialogs::showFileOperationStatus); 1156 getActivity().invalidateOptionsMenu(); 1157 } 1158 pasteIntoFolder()1159 public void pasteIntoFolder() { 1160 if (mSelectionMgr.getSelection().isEmpty()) { 1161 return; 1162 } 1163 assert (mSelectionMgr.getSelection().size() == 1); 1164 1165 String modelId = mSelectionMgr.getSelection().iterator().next(); 1166 Cursor dstCursor = mModel.getItem(modelId); 1167 if (dstCursor == null) { 1168 Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + modelId); 1169 return; 1170 } 1171 DocumentInfo destination = DocumentInfo.fromDirectoryCursor(dstCursor); 1172 mClipper.copyFromClipboard( 1173 destination, 1174 mState.stack, 1175 mInjector.dialogs::showFileOperationStatus); 1176 getActivity().invalidateOptionsMenu(); 1177 } 1178 setupDragAndDropOnDocumentView(View view, Cursor cursor)1179 private void setupDragAndDropOnDocumentView(View view, Cursor cursor) { 1180 final String docMimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 1181 if (Document.MIME_TYPE_DIR.equals(docMimeType)) { 1182 // Make a directory item a drop target. Drop on non-directories and empty space 1183 // is handled at the list/grid view level. 1184 view.setOnDragListener(mDragHoverListener); 1185 } 1186 } 1187 getDestination(View v)1188 private DocumentInfo getDestination(View v) { 1189 String id = getModelId(v); 1190 if (id != null) { 1191 Cursor dstCursor = mModel.getItem(id); 1192 if (dstCursor == null) { 1193 Log.w(TAG, "Invalid destination. Can't obtain cursor for modelId: " + id); 1194 return null; 1195 } 1196 return DocumentInfo.fromDirectoryCursor(dstCursor); 1197 } 1198 1199 if (v == mRecView) { 1200 return mActivity.getCurrentDirectory(); 1201 } 1202 1203 return null; 1204 } 1205 1206 /** 1207 * Gets the model ID for a given RecyclerView item. 1208 * 1209 * @param view A View that is a document item view, or a child of a document item view. 1210 * @return The Model ID for the given document, or null if the given view is not associated with 1211 * a document item view. 1212 */ getModelId(View view)1213 private @Nullable String getModelId(View view) { 1214 View itemView = mRecView.findContainingItemView(view); 1215 if (itemView != null) { 1216 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(itemView); 1217 if (vh instanceof DocumentHolder) { 1218 return ((DocumentHolder) vh).getModelId(); 1219 } 1220 } 1221 return null; 1222 } 1223 getDocumentHolder(View v)1224 private @Nullable DocumentHolder getDocumentHolder(View v) { 1225 RecyclerView.ViewHolder vh = mRecView.getChildViewHolder(v); 1226 if (vh instanceof DocumentHolder) { 1227 return (DocumentHolder) vh; 1228 } 1229 return null; 1230 } 1231 1232 /** 1233 * Add or remove mToolbarPreDrawListener implement on DirectoryFragment to ViewTreeObserver. 1234 */ setPreDrawListenerEnabled(boolean enable)1235 public void setPreDrawListenerEnabled(boolean enable) { 1236 if (mActivity == null) { 1237 return; 1238 } 1239 1240 final View bar = mActivity.findViewById(R.id.collapsing_toolbar); 1241 if (bar != null) { 1242 bar.getViewTreeObserver().removeOnPreDrawListener(mToolbarPreDrawListener); 1243 if (enable) { 1244 bar.getViewTreeObserver().addOnPreDrawListener(mToolbarPreDrawListener); 1245 } 1246 } 1247 } 1248 showDirectory( FragmentManager fm, RootInfo root, DocumentInfo doc, int anim)1249 public static void showDirectory( 1250 FragmentManager fm, RootInfo root, DocumentInfo doc, int anim) { 1251 if (DEBUG) { 1252 Log.d(TAG, "Showing directory: " + DocumentInfo.debugString(doc)); 1253 } 1254 create(fm, root, doc, anim); 1255 } 1256 showRecentsOpen(FragmentManager fm, int anim)1257 public static void showRecentsOpen(FragmentManager fm, int anim) { 1258 create(fm, null, null, anim); 1259 } 1260 create( FragmentManager fm, RootInfo root, @Nullable DocumentInfo doc, @AnimationType int anim)1261 public static void create( 1262 FragmentManager fm, 1263 RootInfo root, 1264 @Nullable DocumentInfo doc, 1265 @AnimationType int anim) { 1266 1267 if (DEBUG) { 1268 if (doc == null) { 1269 Log.d(TAG, "Creating new fragment null directory"); 1270 } else { 1271 Log.d(TAG, "Creating new fragment for directory: " + DocumentInfo.debugString(doc)); 1272 } 1273 } 1274 1275 final Bundle args = new Bundle(); 1276 args.putParcelable(Shared.EXTRA_ROOT, root); 1277 args.putParcelable(Shared.EXTRA_DOC, doc); 1278 1279 final FragmentTransaction ft = fm.beginTransaction(); 1280 AnimationView.setupAnimations(ft, anim, args); 1281 1282 final DirectoryFragment fragment = new DirectoryFragment(); 1283 fragment.setArguments(args); 1284 1285 ft.replace(getFragmentId(), fragment); 1286 ft.commitAllowingStateLoss(); 1287 } 1288 1289 /** Gets the fragment from the fragment manager. */ get(FragmentManager fm)1290 public static @Nullable DirectoryFragment get(FragmentManager fm) { 1291 // TODO: deal with multiple directories shown at once 1292 Fragment fragment = fm.findFragmentById(getFragmentId()); 1293 return fragment instanceof DirectoryFragment 1294 ? (DirectoryFragment) fragment 1295 : null; 1296 } 1297 getFragmentId()1298 private static int getFragmentId() { 1299 return R.id.container_directory; 1300 } 1301 1302 /** 1303 * Scroll to top of recyclerView in fragment 1304 */ scrollToTop()1305 public void scrollToTop() { 1306 if (mRecView != null) { 1307 mRecView.scrollToPosition(0); 1308 } 1309 } 1310 1311 /** 1312 * Stop the scroll of recyclerView in fragment 1313 */ stopScroll()1314 public void stopScroll() { 1315 if (mRecView != null) { 1316 mRecView.stopScroll(); 1317 } 1318 } 1319 1320 @Override onRefresh()1321 public void onRefresh() { 1322 // Remove thumbnail cache. We do this not because we're worried about stale thumbnails as it 1323 // should be covered by last modified value we store in thumbnail cache, but rather to give 1324 // the user a greater sense that contents are being reloaded. 1325 ThumbnailCache cache = DocumentsApplication.getThumbnailCache(getContext()); 1326 String[] ids = mModel.getModelIds(); 1327 int numOfEvicts = Math.min(ids.length, CACHE_EVICT_LIMIT); 1328 for (int i = 0; i < numOfEvicts; ++i) { 1329 cache.removeUri(mModel.getItemUri(ids[i]), mModel.getItemUserId(ids[i])); 1330 } 1331 1332 final DocumentInfo doc = mActivity.getCurrentDirectory(); 1333 if (doc == null && !mActivity.getSelectedUser().isQuietModeEnabled(mActivity)) { 1334 // If there is no root doc, try to reload the root doc from root info. 1335 Log.w(TAG, "No root document. Try to get root document."); 1336 getRootDocumentAndMaybeRefreshDocument(); 1337 return; 1338 } 1339 mActions.refreshDocument(doc, (boolean refreshSupported) -> { 1340 if (refreshSupported) { 1341 mRefreshLayout.setRefreshing(false); 1342 } else { 1343 // If Refresh API isn't available, we will explicitly reload the loader 1344 mActions.loadDocumentsForCurrentStack(); 1345 } 1346 }); 1347 } 1348 getRootDocumentAndMaybeRefreshDocument()1349 private void getRootDocumentAndMaybeRefreshDocument() { 1350 // If we can reload the root doc successfully, we will push it to the stack and load the 1351 // stack. 1352 final RootInfo emptyDocRoot = mActivity.getCurrentRoot(); 1353 mInjector.actions.getRootDocument( 1354 emptyDocRoot, 1355 TimeoutTask.DEFAULT_TIMEOUT, 1356 rootDoc -> { 1357 mRefreshLayout.setRefreshing(false); 1358 if (rootDoc != null && mActivity.getCurrentDirectory() == null) { 1359 // Make sure the stack does not change during task was running. 1360 Log.d(TAG, "Root doc is retrieved. Pushing to the stack"); 1361 mState.stack.push(rootDoc); 1362 mActivity.updateNavigator(); 1363 mActions.loadDocumentsForCurrentStack(); 1364 } 1365 } 1366 ); 1367 } 1368 1369 private final class ModelUpdateListener implements EventListener<Model.Update> { 1370 1371 @Override accept(Model.Update update)1372 public void accept(Model.Update update) { 1373 if (DEBUG) { 1374 Log.d(TAG, "Received model update. Loading=" + mModel.isLoading()); 1375 } 1376 1377 mProgressBar.setVisibility(mModel.isLoading() ? View.VISIBLE : View.GONE); 1378 1379 updateLayout(mState.derivedMode); 1380 1381 // Update the selection to remove any disappeared IDs. 1382 Iterator<String> selectionIter = mSelectionMgr.getSelection().iterator(); 1383 while (selectionIter.hasNext()) { 1384 if (!mAdapter.getStableIds().contains(selectionIter.next())) { 1385 selectionIter.remove(); 1386 } 1387 } 1388 1389 mAdapter.notifyDataSetChanged(); 1390 1391 if (mRestoredState != null) { 1392 mSelectionMgr.onRestoreInstanceState(mRestoredState); 1393 mRestoredState = null; 1394 } 1395 1396 // Restore any previous instance state 1397 final SparseArray<Parcelable> container = 1398 mState.dirConfigs.remove(mLocalState.getConfigKey()); 1399 final int curSortedDimensionId = mState.sortModel.getSortedDimensionId(); 1400 1401 final SortDimension curSortedDimension = 1402 mState.sortModel.getDimensionById(curSortedDimensionId); 1403 1404 // Default not restore to avoid app bar layout expand to confuse users. 1405 if (container != null 1406 && !getArguments().getBoolean(Shared.EXTRA_IGNORE_STATE, true)) { 1407 getView().restoreHierarchyState(container); 1408 } else if (mLocalState.mLastSortDimensionId != curSortedDimension.getId() 1409 || mLocalState.mLastSortDimensionId == SortModel.SORT_DIMENSION_ID_UNKNOWN 1410 || mLocalState.mLastSortDirection != curSortedDimension.getSortDirection()) { 1411 // Scroll to the top if the sort order actually changed. 1412 mRecView.smoothScrollToPosition(0); 1413 } 1414 1415 mLocalState.mLastSortDimensionId = curSortedDimension.getId(); 1416 mLocalState.mLastSortDirection = curSortedDimension.getSortDirection(); 1417 1418 if (mRefreshLayout.isRefreshing()) { 1419 new Handler().postDelayed( 1420 () -> mRefreshLayout.setRefreshing(false), 1421 REFRESH_SPINNER_TIMEOUT); 1422 } 1423 1424 if (!mModel.isLoading()) { 1425 mActivity.notifyDirectoryLoaded( 1426 mModel.doc != null ? mModel.doc.derivedUri : null); 1427 // For orientation changed case, sometimes the docs loading comes after the menu 1428 // update. We need to update the menu here to ensure the status is correct. 1429 mInjector.menuManager.updateModel(mModel); 1430 mInjector.menuManager.updateOptionMenu(); 1431 1432 mActivity.updateHeaderTitle(); 1433 } 1434 } 1435 } 1436 1437 private final class AdapterEnvironment implements DocumentsAdapter.Environment { 1438 1439 @Override getFeatures()1440 public Features getFeatures() { 1441 return mInjector.features; 1442 } 1443 1444 @Override getContext()1445 public Context getContext() { 1446 return mActivity; 1447 } 1448 1449 @Override getDisplayState()1450 public State getDisplayState() { 1451 return mState; 1452 } 1453 1454 @Override isInSearchMode()1455 public boolean isInSearchMode() { 1456 return mInjector.searchManager.isSearching(); 1457 } 1458 1459 @Override getModel()1460 public Model getModel() { 1461 return mModel; 1462 } 1463 1464 @Override getColumnCount()1465 public int getColumnCount() { 1466 return mColumnCount; 1467 } 1468 1469 @Override isSelected(String id)1470 public boolean isSelected(String id) { 1471 return mSelectionMgr.isSelected(id); 1472 } 1473 1474 @Override isDocumentEnabled(String mimeType, int flags)1475 public boolean isDocumentEnabled(String mimeType, int flags) { 1476 return mInjector.config.isDocumentEnabled(mimeType, flags, mState); 1477 } 1478 1479 @Override initDocumentHolder(DocumentHolder holder)1480 public void initDocumentHolder(DocumentHolder holder) { 1481 holder.addKeyEventListener(mKeyListener); 1482 holder.itemView.setOnFocusChangeListener(mFocusManager); 1483 } 1484 1485 @Override onBindDocumentHolder(DocumentHolder holder, Cursor cursor)1486 public void onBindDocumentHolder(DocumentHolder holder, Cursor cursor) { 1487 setupDragAndDropOnDocumentView(holder.itemView, cursor); 1488 } 1489 1490 @Override getActionHandler()1491 public ActionHandler getActionHandler() { 1492 return mActions; 1493 } 1494 1495 @Override getCallingAppName()1496 public String getCallingAppName() { 1497 return Shared.getCallingAppName(mActivity); 1498 } 1499 } 1500 } 1501