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.picker; 18 19 import static com.android.documentsui.base.State.ACTION_CREATE; 20 import static com.android.documentsui.base.State.ACTION_GET_CONTENT; 21 import static com.android.documentsui.base.State.ACTION_OPEN; 22 import static com.android.documentsui.base.State.ACTION_OPEN_TREE; 23 import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION; 24 25 import android.content.Intent; 26 import android.content.res.Resources; 27 import android.graphics.Color; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.os.SystemClock; 31 import android.provider.DocumentsContract; 32 import android.util.Log; 33 import android.view.KeyEvent; 34 import android.view.Menu; 35 import android.view.MenuItem; 36 import android.view.View; 37 38 import androidx.annotation.CallSuper; 39 import androidx.fragment.app.Fragment; 40 import androidx.fragment.app.FragmentManager; 41 42 import com.android.documentsui.ActionModeController; 43 import com.android.documentsui.BaseActivity; 44 import com.android.documentsui.DocsSelectionHelper; 45 import com.android.documentsui.DocumentsApplication; 46 import com.android.documentsui.FocusManager; 47 import com.android.documentsui.Injector; 48 import com.android.documentsui.MenuManager.DirectoryDetails; 49 import com.android.documentsui.Metrics; 50 import com.android.documentsui.ProfileTabsController; 51 import com.android.documentsui.ProviderExecutor; 52 import com.android.documentsui.R; 53 import com.android.documentsui.SharedInputHandler; 54 import com.android.documentsui.base.DocumentInfo; 55 import com.android.documentsui.base.Features; 56 import com.android.documentsui.base.MimeTypes; 57 import com.android.documentsui.base.RootInfo; 58 import com.android.documentsui.base.Shared; 59 import com.android.documentsui.base.State; 60 import com.android.documentsui.base.UserId; 61 import com.android.documentsui.dirlist.AppsRowManager; 62 import com.android.documentsui.dirlist.DirectoryFragment; 63 import com.android.documentsui.services.FileOperationService; 64 import com.android.documentsui.sidebar.RootsFragment; 65 import com.android.documentsui.ui.DialogController; 66 import com.android.documentsui.ui.MessageBuilder; 67 import com.android.documentsui.util.CrossProfileUtils; 68 import com.android.documentsui.util.VersionUtils; 69 70 import java.util.Collection; 71 import java.util.Collections; 72 import java.util.List; 73 74 public class PickActivity extends BaseActivity implements ActionHandler.Addons { 75 76 static final String PREFERENCES_SCOPE = "picker"; 77 78 private static final String TAG = "PickActivity"; 79 80 private Injector<ActionHandler<PickActivity>> mInjector; 81 private SharedInputHandler mSharedInputHandler; 82 PickActivity()83 public PickActivity() { 84 super(R.layout.documents_activity, TAG); 85 } 86 87 // make these methods visible in this package to work around compiler bug http://b/62218600 focusSidebar()88 @Override protected boolean focusSidebar() { return super.focusSidebar(); } popDir()89 @Override protected boolean popDir() { return super.popDir(); } 90 91 @Override onCreate(Bundle icicle)92 public void onCreate(Bundle icicle) { 93 setTheme(R.style.DocumentsTheme); 94 Features features = Features.create(this); 95 96 mInjector = new Injector<>( 97 features, 98 new Config(), 99 new MessageBuilder(this), 100 DialogController.create(features, this), 101 DocumentsApplication.getFileTypeLookup(this), 102 (Collection<RootInfo> roots) -> {}); 103 104 super.onCreate(icicle); 105 106 mInjector.selectionMgr = DocsSelectionHelper.create(); 107 108 mInjector.focusManager = new FocusManager( 109 mInjector.features, 110 mInjector.selectionMgr, 111 mDrawer, 112 this::focusSidebar, 113 getColor(R.color.primary)); 114 115 mInjector.menuManager = new MenuManager( 116 mSearchManager, 117 mState, 118 new DirectoryDetails(this), 119 mInjector.getModel()::getItemCount); 120 121 mInjector.actionModeController = new ActionModeController( 122 this, 123 mInjector.selectionMgr, 124 mNavigator, 125 mInjector.menuManager, 126 mInjector.messages); 127 128 mInjector.profileTabsController = new ProfileTabsController( 129 mInjector.selectionMgr, 130 getProfileTabsAddon()); 131 132 mInjector.pickResult = getPickResult(icicle); 133 mInjector.actions = new ActionHandler<>( 134 this, 135 mState, 136 mProviders, 137 mDocs, 138 mSearchManager, 139 ProviderExecutor::forAuthority, 140 mInjector, 141 LastAccessedStorage.create(), 142 mUserIdManager); 143 144 mInjector.searchManager = mSearchManager; 145 146 Intent intent = getIntent(); 147 148 mAppsRowManager = new AppsRowManager(mInjector.actions, mState.supportsCrossProfile(), 149 mUserIdManager); 150 mInjector.appsRowManager = mAppsRowManager; 151 152 mSharedInputHandler = 153 new SharedInputHandler( 154 mInjector.focusManager, 155 mInjector.selectionMgr, 156 mInjector.searchManager::cancelSearch, 157 this::popDir, 158 mInjector.features, 159 mDrawer, 160 mInjector.searchManager::onSearchBarClicked); 161 setupLayout(intent); 162 mInjector.actions.initLocation(intent); 163 Metrics.logPickerLaunchedFrom(Shared.getCallingPackageName(this)); 164 } 165 166 @Override onBackPressed()167 public void onBackPressed() { 168 super.onBackPressed(); 169 // log the case of user picking nothing. 170 mInjector.actions.getUpdatePickResultTask().safeExecute(); 171 } 172 173 @Override onSaveInstanceState(Bundle state)174 protected void onSaveInstanceState(Bundle state) { 175 super.onSaveInstanceState(state); 176 state.putParcelable(Shared.EXTRA_PICK_RESULT, mInjector.pickResult); 177 } 178 179 @Override onResume()180 protected void onResume() { 181 super.onResume(); 182 mInjector.pickResult.setPickStartTime(SystemClock.uptimeMillis()); 183 } 184 185 @Override onPause()186 protected void onPause() { 187 mInjector.pickResult.increaseDuration(SystemClock.uptimeMillis()); 188 super.onPause(); 189 } 190 getPickResult(Bundle icicle)191 private static PickResult getPickResult(Bundle icicle) { 192 if (icicle != null) { 193 PickResult result = icicle.getParcelable(Shared.EXTRA_PICK_RESULT); 194 return result; 195 } 196 197 return new PickResult(); 198 } 199 setupLayout(Intent intent)200 private void setupLayout(Intent intent) { 201 if (mState.action == ACTION_CREATE) { 202 final String mimeType = intent.getType(); 203 final String title = intent.getStringExtra(Intent.EXTRA_TITLE); 204 SaveFragment.show(getSupportFragmentManager(), mimeType, title); 205 } else if (mState.action == ACTION_OPEN_TREE || 206 mState.action == ACTION_PICK_COPY_DESTINATION) { 207 PickFragment.show(getSupportFragmentManager()); 208 } else { 209 // If PickFragment or SaveFragment does not show, 210 // Set save container background to transparent for edge to edge nav bar. 211 View saveContainer = findViewById(R.id.container_save); 212 saveContainer.setBackgroundColor(Color.TRANSPARENT); 213 } 214 215 final Intent moreApps = new Intent(intent); 216 moreApps.setComponent(null); 217 moreApps.setPackage(null); 218 if (mState.supportsCrossProfile() 219 && CrossProfileUtils.getCrossProfileResolveInfo( 220 getPackageManager(), moreApps) != null) { 221 mState.canShareAcrossProfile = true; 222 } 223 224 if (mState.action == ACTION_GET_CONTENT 225 || mState.action == ACTION_OPEN 226 || mState.action == ACTION_CREATE 227 || mState.action == ACTION_OPEN_TREE 228 || mState.action == ACTION_PICK_COPY_DESTINATION) { 229 RootsFragment.show(getSupportFragmentManager(), 230 /* includeApps= */ mState.action == ACTION_GET_CONTENT, 231 /* intent= */ moreApps); 232 } 233 } 234 235 @Override includeState(State state)236 protected void includeState(State state) { 237 final Intent intent = getIntent(); 238 239 String defaultMimeType = (intent.getType() == null) ? "*/*" : intent.getType(); 240 state.initAcceptMimes(intent, defaultMimeType); 241 242 final String action = intent.getAction(); 243 if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) { 244 state.action = ACTION_OPEN; 245 } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) { 246 state.action = ACTION_CREATE; 247 } else if (Intent.ACTION_GET_CONTENT.equals(action)) { 248 state.action = ACTION_GET_CONTENT; 249 } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) { 250 state.action = ACTION_OPEN_TREE; 251 } else if (Shared.ACTION_PICK_COPY_DESTINATION.equals(action)) { 252 state.action = ACTION_PICK_COPY_DESTINATION; 253 } 254 255 if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT) { 256 state.allowMultiple = intent.getBooleanExtra( 257 Intent.EXTRA_ALLOW_MULTIPLE, false); 258 } 259 260 if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT 261 || state.action == ACTION_CREATE) { 262 state.openableOnly = intent.hasCategory(Intent.CATEGORY_OPENABLE); 263 } 264 265 if (state.action == ACTION_PICK_COPY_DESTINATION) { 266 state.copyOperationSubType = intent.getIntExtra( 267 FileOperationService.EXTRA_OPERATION_TYPE, 268 FileOperationService.OPERATION_COPY); 269 } else if (Features.CROSS_PROFILE_TABS && VersionUtils.isAtLeastR()) { 270 // We show tabs on PickActivity except copying/moving, which does not support 271 // cross-profile action. 272 state.supportsCrossProfile = true; 273 } 274 } 275 276 @Override onPostCreate(Bundle savedInstanceState)277 protected void onPostCreate(Bundle savedInstanceState) { 278 super.onPostCreate(savedInstanceState); 279 mDrawer.update(); 280 mNavigator.update(); 281 } 282 283 @Override getDrawerTitle()284 public String getDrawerTitle() { 285 String title; 286 try { 287 // Internal use case, we will send string id instead of string text. 288 title = getResources().getString( 289 getIntent().getIntExtra(DocumentsContract.EXTRA_PROMPT, -1)); 290 } catch (Resources.NotFoundException e) { 291 // 3rd party use case, it should send string text. 292 title = getIntent().getStringExtra(DocumentsContract.EXTRA_PROMPT); 293 if (title == null) { 294 if (mState.action == ACTION_OPEN 295 || mState.action == ACTION_GET_CONTENT 296 || mState.action == ACTION_OPEN_TREE) { 297 title = getResources().getString(R.string.title_open); 298 } else if (mState.action == ACTION_CREATE 299 || mState.action == ACTION_PICK_COPY_DESTINATION) { 300 title = getResources().getString(R.string.title_save); 301 } else { 302 // If all else fails, just call it "Documents". 303 title = getResources().getString(R.string.app_label); 304 } 305 } 306 } 307 return title; 308 } 309 310 @Override onPrepareOptionsMenu(Menu menu)311 public boolean onPrepareOptionsMenu(Menu menu) { 312 super.onPrepareOptionsMenu(menu); 313 mInjector.menuManager.updateOptionMenu(menu); 314 315 final DocumentInfo cwd = getCurrentDirectory(); 316 317 if (mState.action == ACTION_CREATE) { 318 final FragmentManager fm = getSupportFragmentManager(); 319 SaveFragment.get(fm).prepareForDirectory(cwd); 320 } 321 322 return true; 323 } 324 325 @Override onOptionsItemSelected(MenuItem item)326 public boolean onOptionsItemSelected(MenuItem item) { 327 mInjector.pickResult.increaseActionCount(); 328 return super.onOptionsItemSelected(item); 329 } 330 331 @Override refreshDirectory(int anim)332 protected void refreshDirectory(int anim) { 333 final FragmentManager fm = getSupportFragmentManager(); 334 final RootInfo root = getCurrentRoot(); 335 final DocumentInfo cwd = getCurrentDirectory(); 336 337 if (mState.stack.isRecents()) { 338 DirectoryFragment.showRecentsOpen(fm, anim); 339 340 // In recents we pick layout mode based on the mimetype, 341 // picking GRID for visual types. We intentionally don't 342 // consult a user's saved preferences here since they are 343 // set per root (not per root and per mimetype). 344 boolean visualMimes = MimeTypes.mimeMatches( 345 MimeTypes.VISUAL_MIMES, mState.acceptMimes); 346 mState.derivedMode = visualMimes ? State.MODE_GRID : State.MODE_LIST; 347 } else { 348 // Normal boring directory 349 DirectoryFragment.showDirectory(fm, root, cwd, anim); 350 } 351 352 // Forget any replacement target 353 if (mState.action == ACTION_CREATE) { 354 final SaveFragment save = SaveFragment.get(fm); 355 if (save != null) { 356 save.setReplaceTarget(null); 357 } 358 } 359 360 if (mState.action == ACTION_OPEN_TREE || 361 mState.action == ACTION_PICK_COPY_DESTINATION) { 362 final PickFragment pick = PickFragment.get(fm); 363 if (pick != null) { 364 pick.setPickTarget(mState.action, 365 mState.copyOperationSubType, mState.restrictScopeStorage, cwd); 366 } 367 } 368 } 369 370 @Override onDirectoryCreated(DocumentInfo doc)371 protected void onDirectoryCreated(DocumentInfo doc) { 372 assert(doc.isDirectory()); 373 mInjector.actions.openContainerDocument(doc); 374 } 375 376 @Override onDocumentPicked(DocumentInfo doc)377 public void onDocumentPicked(DocumentInfo doc) { 378 final FragmentManager fm = getSupportFragmentManager(); 379 // Do not inline-open archives, as otherwise it would be impossible to pick 380 // archive files. Note, that picking files inside archives is not supported. 381 if (doc.isDirectory()) { 382 mInjector.actions.openContainerDocument(doc); 383 mSearchManager.recordHistory(); 384 } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 385 // Explicit file picked, return 386 if (!canShare(Collections.singletonList(doc))) { 387 // A final check to make sure we can share the uri before returning it. 388 Log.e(TAG, "The document cannot be shared"); 389 mInjector.dialogs.showActionNotAllowed(); 390 return; 391 } 392 mInjector.pickResult.setHasCrossProfileUri(!UserId.CURRENT_USER.equals(doc.userId)); 393 mInjector.actions.finishPicking(doc.getDocumentUri()); 394 mSearchManager.recordHistory(); 395 } else if (mState.action == ACTION_CREATE) { 396 // Replace selected file 397 SaveFragment.get(fm).setReplaceTarget(doc); 398 } 399 } 400 401 @Override onDocumentsPicked(List<DocumentInfo> docs)402 public void onDocumentsPicked(List<DocumentInfo> docs) { 403 if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 404 if (!canShare(docs)) { 405 // A final check to make sure we can share these uris before returning them. 406 Log.e(TAG, "One or more document cannot be shared"); 407 mInjector.dialogs.showActionNotAllowed(); 408 return; 409 } 410 final int size = docs.size(); 411 final Uri[] uris = new Uri[size]; 412 boolean hasCrossProfileUri = false; 413 for (int i = 0; i < docs.size(); i++) { 414 DocumentInfo doc = docs.get(i); 415 uris[i] = doc.getDocumentUri(); 416 if (!UserId.CURRENT_USER.equals(doc.userId)) { 417 hasCrossProfileUri = true; 418 } 419 } 420 mInjector.pickResult.setHasCrossProfileUri(hasCrossProfileUri); 421 mInjector.actions.finishPicking(uris); 422 mSearchManager.recordHistory(); 423 } 424 } 425 canShare(List<DocumentInfo> docs)426 private boolean canShare(List<DocumentInfo> docs) { 427 for (DocumentInfo doc : docs) { 428 if (!mState.canInteractWith(doc.userId)) { 429 return false; 430 } 431 } 432 return true; 433 } 434 435 @CallSuper 436 @Override onKeyDown(int keyCode, KeyEvent event)437 public boolean onKeyDown(int keyCode, KeyEvent event) { 438 return mSharedInputHandler.onKeyDown(keyCode, event) 439 || super.onKeyDown(keyCode, event); 440 } 441 442 @Override setResult(int resultCode, Intent intent, int notUsed)443 public void setResult(int resultCode, Intent intent, int notUsed) { 444 setResult(resultCode, intent); 445 } 446 get(Fragment fragment)447 public static PickActivity get(Fragment fragment) { 448 return (PickActivity) fragment.getActivity(); 449 } 450 451 @Override getInjector()452 public Injector<ActionHandler<PickActivity>> getInjector() { 453 return mInjector; 454 } 455 } 456