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