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