1 /* 2 * Copyright (C) 2016 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.SharedMinimal.DEBUG; 20 import static com.android.documentsui.base.State.ACTION_CREATE; 21 import static com.android.documentsui.base.State.ACTION_GET_CONTENT; 22 import static com.android.documentsui.base.State.ACTION_OPEN; 23 import static com.android.documentsui.base.State.ACTION_OPEN_TREE; 24 import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION; 25 26 import android.content.ActivityNotFoundException; 27 import android.content.ClipData; 28 import android.content.ComponentName; 29 import android.content.Intent; 30 import android.content.pm.ResolveInfo; 31 import android.net.Uri; 32 import android.os.AsyncTask; 33 import android.os.Parcelable; 34 import android.provider.DocumentsContract; 35 import android.provider.Settings; 36 import android.util.Log; 37 38 import androidx.annotation.VisibleForTesting; 39 import androidx.fragment.app.FragmentActivity; 40 import androidx.fragment.app.FragmentManager; 41 import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails; 42 43 import com.android.documentsui.AbstractActionHandler; 44 import com.android.documentsui.ActivityConfig; 45 import com.android.documentsui.DocumentsAccess; 46 import com.android.documentsui.Injector; 47 import com.android.documentsui.MetricConsts; 48 import com.android.documentsui.Metrics; 49 import com.android.documentsui.UserIdManager; 50 import com.android.documentsui.base.BooleanConsumer; 51 import com.android.documentsui.base.DocumentInfo; 52 import com.android.documentsui.base.DocumentStack; 53 import com.android.documentsui.base.Features; 54 import com.android.documentsui.base.Lookup; 55 import com.android.documentsui.base.RootInfo; 56 import com.android.documentsui.base.Shared; 57 import com.android.documentsui.base.State; 58 import com.android.documentsui.base.UserId; 59 import com.android.documentsui.dirlist.AnimationView; 60 import com.android.documentsui.picker.ActionHandler.Addons; 61 import com.android.documentsui.queries.SearchViewManager; 62 import com.android.documentsui.roots.ProvidersAccess; 63 import com.android.documentsui.services.FileOperationService; 64 65 import com.android.documentsui.util.VersionUtils; 66 import java.util.Arrays; 67 import java.util.Locale; 68 import java.util.concurrent.Executor; 69 70 import java.util.regex.Pattern; 71 import javax.annotation.Nullable; 72 73 /** 74 * Provides {@link PickActivity} action specializations to fragments. 75 */ 76 class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionHandler<T> { 77 78 private static final String TAG = "PickerActionHandler"; 79 80 private final Features mFeatures; 81 private final ActivityConfig mConfig; 82 private final LastAccessedStorage mLastAccessed; 83 private final UserIdManager mUserIdManager; 84 private final static Pattern PATTERN_BLOCK_PATH = Pattern.compile( 85 ".*:android\\/(?:data|obb|sandbox)$"); 86 87 private UpdatePickResultTask mUpdatePickResultTask; 88 ActionHandler( T activity, State state, ProvidersAccess providers, DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, Injector injector, LastAccessedStorage lastAccessed, UserIdManager userIdManager)89 ActionHandler( 90 T activity, 91 State state, 92 ProvidersAccess providers, 93 DocumentsAccess docs, 94 SearchViewManager searchMgr, 95 Lookup<String, Executor> executors, 96 Injector injector, 97 LastAccessedStorage lastAccessed, 98 UserIdManager userIdManager) { 99 super(activity, state, providers, docs, searchMgr, executors, injector); 100 101 mConfig = injector.config; 102 mFeatures = injector.features; 103 mLastAccessed = lastAccessed; 104 mUpdatePickResultTask = new UpdatePickResultTask( 105 activity.getApplicationContext(), mInjector.pickResult); 106 mUserIdManager = userIdManager; 107 } 108 109 @Override initLocation(Intent intent)110 public void initLocation(Intent intent) { 111 assert(intent != null); 112 113 // stack is initialized if it's restored from bundle, which means we're restoring a 114 // previously stored state. 115 if (mState.stack.isInitialized()) { 116 if (DEBUG) { 117 Log.d(TAG, "Stack already resolved for uri: " + intent.getData()); 118 } 119 restoreRootAndDirectory(); 120 return; 121 } 122 123 if (launchHomeForCopyDestination(intent)) { 124 if (DEBUG) { 125 Log.d(TAG, "Launching directly into Home directory for copy destination."); 126 } 127 return; 128 } 129 130 if (mFeatures.isLaunchToDocumentEnabled() && launchToInitialUri(intent)) { 131 if (DEBUG) { 132 Log.d(TAG, "Launched to initial uri."); 133 } 134 return; 135 } 136 137 if (DEBUG) { 138 Log.d(TAG, "Load last accessed stack."); 139 } 140 initLoadLastAccessedStack(); 141 } 142 143 @Override launchToDefaultLocation()144 protected void launchToDefaultLocation() { 145 loadLastAccessedStack(); 146 } 147 launchHomeForCopyDestination(Intent intent)148 private boolean launchHomeForCopyDestination(Intent intent) { 149 // As a matter of policy we don't load the last used stack for the copy 150 // destination picker (user is already in Files app). 151 // Consensus was that the experice was too confusing. 152 // In all other cases, where the user is visiting us from another app 153 // we restore the stack as last used from that app. 154 if (Shared.ACTION_PICK_COPY_DESTINATION.equals(intent.getAction())) { 155 loadHomeDir(); 156 return true; 157 } 158 159 return false; 160 } 161 launchToInitialUri(Intent intent)162 private boolean launchToInitialUri(Intent intent) { 163 Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI); 164 if (uri != null) { 165 // In android S and above if path contains Android/data, Android/obb 166 // or Android/sandbox redirect to the root for which 167 // FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE is already set 168 if(Shared.shouldRestrictStorageAccessFramework(mActivity) 169 && (PATTERN_BLOCK_PATH.matcher(uri.getPath().toLowerCase(Locale.ROOT)).matches())){ 170 loadDeviceRoot(); 171 return true; 172 } 173 if (DocumentsContract.isRootUri(mActivity, uri)) { 174 loadRoot(uri, UserId.DEFAULT_USER); 175 return true; 176 } else if (DocumentsContract.isDocumentUri(mActivity, uri)) { 177 return launchToDocument(uri); 178 } 179 } 180 181 return false; 182 } 183 initLoadLastAccessedStack()184 private void initLoadLastAccessedStack() { 185 if (DEBUG) { 186 Log.d(TAG, "Attempting to load last used stack for calling package."); 187 } 188 // Block UI until stack is fully loaded, else there is an intermediate incomplete UI state. 189 onLastAccessedStackLoaded(mLastAccessed.getLastAccessed(mActivity, mProviders, mState)); 190 } 191 loadLastAccessedStack()192 private void loadLastAccessedStack() { 193 if (DEBUG) { 194 Log.d(TAG, "Attempting to load last used stack for calling package."); 195 } 196 new LoadLastAccessedStackTask<>( 197 mActivity, mLastAccessed, mState, mProviders, this::onLastAccessedStackLoaded) 198 .execute(); 199 } 200 onLastAccessedStackLoaded(@ullable DocumentStack stack)201 private void onLastAccessedStackLoaded(@Nullable DocumentStack stack) { 202 if (stack == null) { 203 loadDefaultLocation(); 204 } else { 205 mState.stack.reset(stack); 206 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 207 } 208 } 209 getUpdatePickResultTask()210 public UpdatePickResultTask getUpdatePickResultTask() { 211 return mUpdatePickResultTask; 212 } 213 updatePickResult(Intent intent, boolean isSearching, int root)214 private void updatePickResult(Intent intent, boolean isSearching, int root) { 215 ClipData cdata = intent.getClipData(); 216 int fileCount = 0; 217 Uri uri = null; 218 219 // There are 2 cases that would be single-select: 220 // 1. getData() isn't null and getClipData() is null. 221 // 2. getClipData() isn't null and the item count of it is 1. 222 if (intent.getData() != null && cdata == null) { 223 fileCount = 1; 224 uri = intent.getData(); 225 } else if (cdata != null) { 226 fileCount = cdata.getItemCount(); 227 if (fileCount == 1) { 228 uri = cdata.getItemAt(0).getUri(); 229 } 230 } 231 232 mInjector.pickResult.setFileCount(fileCount); 233 mInjector.pickResult.setIsSearching(isSearching); 234 mInjector.pickResult.setRoot(root); 235 mInjector.pickResult.setFileUri(uri); 236 getUpdatePickResultTask().safeExecute(); 237 } 238 loadDefaultLocation()239 private void loadDefaultLocation() { 240 switch (mState.action) { 241 case ACTION_CREATE: 242 loadHomeDir(); 243 break; 244 case ACTION_OPEN_TREE: 245 loadDeviceRoot(); 246 break; 247 case ACTION_GET_CONTENT: 248 case ACTION_OPEN: 249 loadRecent(); 250 break; 251 default: 252 throw new UnsupportedOperationException("Unexpected action type: " + mState.action); 253 } 254 } 255 256 @Override showAppDetails(ResolveInfo info, UserId userId)257 public void showAppDetails(ResolveInfo info, UserId userId) { 258 mInjector.pickResult.increaseActionCount(); 259 final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 260 intent.setData(Uri.fromParts("package", info.activityInfo.packageName, null)); 261 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); 262 userId.startActivityAsUser(mActivity, intent); 263 } 264 265 @Override openInNewWindow(DocumentStack path)266 public void openInNewWindow(DocumentStack path) { 267 // Open new window support only depends on vanilla Activity, so it is 268 // implemented in our parent class. But we don't support that in 269 // picking. So as a matter of defensiveness, we override that here. 270 throw new UnsupportedOperationException("Can't open in new window"); 271 } 272 273 @Override openRoot(RootInfo root)274 public void openRoot(RootInfo root) { 275 Metrics.logRootVisited(MetricConsts.PICKER_SCOPE, root); 276 mInjector.pickResult.increaseActionCount(); 277 mActivity.onRootPicked(root); 278 } 279 280 @Override openRoot(ResolveInfo info, UserId userId)281 public void openRoot(ResolveInfo info, UserId userId) { 282 Metrics.logAppVisited(info); 283 mInjector.pickResult.increaseActionCount(); 284 285 // The App root item should not show if we cannot interact with the target user. 286 // But the user managed to get here, this is the final check of permission. We don't 287 // perform the check on activity result. 288 if (!mState.canInteractWith(userId)) { 289 mInjector.dialogs.showActionNotAllowed(); 290 return; 291 } 292 293 Intent intent = new Intent(mActivity.getIntent()); 294 final int flagsRemoved = Intent.FLAG_GRANT_READ_URI_PERMISSION 295 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 296 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 297 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; 298 intent.setFlags(intent.getFlags() & ~flagsRemoved); 299 intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 300 intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); 301 intent.setComponent(new ComponentName( 302 info.activityInfo.applicationInfo.packageName, info.activityInfo.name)); 303 try { 304 boolean isCurrentUser = UserId.CURRENT_USER.equals(userId); 305 if (isCurrentUser) { 306 mActivity.startActivity(intent); 307 } else { 308 userId.startActivityAsUser(mActivity, intent); 309 } 310 Metrics.logLaunchOtherApp(!UserId.CURRENT_USER.equals(userId)); 311 mActivity.finish(); 312 } catch (SecurityException | ActivityNotFoundException e) { 313 Log.e(TAG, "Caught error: " + e.getLocalizedMessage()); 314 mInjector.dialogs.showNoApplicationFound(); 315 } 316 } 317 318 319 @Override springOpenDirectory(DocumentInfo doc)320 public void springOpenDirectory(DocumentInfo doc) { 321 } 322 323 @Override openItem(ItemDetails<String> details, @ViewType int type, @ViewType int fallback)324 public boolean openItem(ItemDetails<String> details, @ViewType int type, 325 @ViewType int fallback) { 326 mInjector.pickResult.increaseActionCount(); 327 DocumentInfo doc = mModel.getDocument(details.getSelectionKey()); 328 if (doc == null) { 329 Log.w(TAG, "Can't view item. No Document available for modeId: " 330 + details.getSelectionKey()); 331 return false; 332 } 333 334 if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) { 335 mActivity.onDocumentPicked(doc); 336 mSelectionMgr.clearSelection(); 337 return !doc.isDirectory(); 338 } 339 return false; 340 } 341 342 @Override previewItem(ItemDetails<String> details)343 public boolean previewItem(ItemDetails<String> details) { 344 mInjector.pickResult.increaseActionCount(); 345 final DocumentInfo doc = mModel.getDocument(details.getSelectionKey()); 346 if (doc == null) { 347 Log.w(TAG, "Can't view item. No Document available for modeId: " 348 + details.getSelectionKey()); 349 return false; 350 } 351 352 onDocumentOpened(doc, VIEW_TYPE_PREVIEW, VIEW_TYPE_REGULAR, true); 353 return !doc.isContainer(); 354 } 355 pickDocument(FragmentManager fm, DocumentInfo pickTarget)356 void pickDocument(FragmentManager fm, DocumentInfo pickTarget) { 357 assert(pickTarget != null); 358 mInjector.pickResult.increaseActionCount(); 359 Uri result; 360 switch (mState.action) { 361 case ACTION_OPEN_TREE: 362 mInjector.dialogs.confirmAction(fm, pickTarget, ConfirmFragment.TYPE_OEPN_TREE); 363 break; 364 case ACTION_PICK_COPY_DESTINATION: 365 result = pickTarget.derivedUri; 366 finishPicking(result); 367 break; 368 default: 369 // Should not be reached 370 throw new IllegalStateException("Invalid mState.action"); 371 } 372 } 373 saveDocument( String mimeType, String displayName, BooleanConsumer inProgressStateListener)374 void saveDocument( 375 String mimeType, String displayName, BooleanConsumer inProgressStateListener) { 376 assert(mState.action == ACTION_CREATE); 377 mInjector.pickResult.increaseActionCount(); 378 new CreatePickedDocumentTask( 379 mActivity, 380 mDocs, 381 mLastAccessed, 382 mState.stack, 383 mimeType, 384 displayName, 385 inProgressStateListener, 386 this::onPickFinished) 387 .executeOnExecutor(getExecutorForCurrentDirectory()); 388 } 389 390 // User requested to overwrite a target. If confirmed by user #finishPicking() will be 391 // called. saveDocument(FragmentManager fm, DocumentInfo replaceTarget)392 void saveDocument(FragmentManager fm, DocumentInfo replaceTarget) { 393 assert(mState.action == ACTION_CREATE); 394 mInjector.pickResult.increaseActionCount(); 395 assert(replaceTarget != null); 396 397 // Adding a confirmation dialog breaks an inherited CTS test (testCreateExisting), so we 398 // need to add a feature flag to bypass this feature in ARC++ environment. 399 if (mFeatures.isOverwriteConfirmationEnabled()) { 400 mInjector.dialogs.confirmAction(fm, replaceTarget, ConfirmFragment.TYPE_OVERWRITE); 401 } else { 402 finishPicking(replaceTarget.getDocumentUri()); 403 } 404 } 405 finishPicking(Uri... docs)406 void finishPicking(Uri... docs) { 407 new SetLastAccessedStackTask( 408 mActivity, 409 mLastAccessed, 410 mState.stack, 411 () -> { 412 onPickFinished(docs); 413 } 414 ) .executeOnExecutor(getExecutorForCurrentDirectory()); 415 } 416 onPickFinished(Uri... uris)417 private void onPickFinished(Uri... uris) { 418 if (DEBUG) { 419 Log.d(TAG, "onFinished() " + Arrays.toString(uris)); 420 } 421 422 final Intent intent = new Intent(); 423 if (uris.length == 1) { 424 intent.setData(uris[0]); 425 } else if (uris.length > 1) { 426 final ClipData clipData = new ClipData( 427 null, mState.acceptMimes, new ClipData.Item(uris[0])); 428 for (int i = 1; i < uris.length; i++) { 429 clipData.addItem(new ClipData.Item(uris[i])); 430 } 431 intent.setClipData(clipData); 432 } 433 434 updatePickResult( 435 intent, mSearchMgr.isSearching(), Metrics.sanitizeRoot(mState.stack.getRoot())); 436 437 // TODO: Separate this piece of logic per action. 438 // We don't instantiate different objects for different actions at the first place, so it's 439 // not a easy task to separate this logic cleanly. 440 // Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its 441 // inheritance structure. 442 if (mState.action == ACTION_GET_CONTENT) { 443 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 444 } else if (mState.action == ACTION_OPEN_TREE) { 445 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 446 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 447 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 448 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); 449 } else if (mState.action == ACTION_PICK_COPY_DESTINATION) { 450 // Picking a copy destination is only used internally by us, so we 451 // don't need to extend permissions to the caller. 452 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack); 453 intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType); 454 } else { 455 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 456 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 457 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 458 } 459 460 mActivity.setResult(FragmentActivity.RESULT_OK, intent, 0); 461 mActivity.finish(); 462 } 463 getExecutorForCurrentDirectory()464 private Executor getExecutorForCurrentDirectory() { 465 final DocumentInfo cwd = mState.stack.peek(); 466 if (cwd != null && cwd.authority != null) { 467 return mExecutors.lookup(cwd.authority); 468 } else { 469 return AsyncTask.THREAD_POOL_EXECUTOR; 470 } 471 } 472 473 public interface Addons extends CommonAddons { 474 @Override onDocumentPicked(DocumentInfo doc)475 void onDocumentPicked(DocumentInfo doc); 476 477 /** 478 * Overload final method {@link FragmentActivity#setResult(int, Intent)} so that we can 479 * intercept this method call in test environment. 480 */ 481 @VisibleForTesting setResult(int resultCode, Intent result, int notUsed)482 void setResult(int resultCode, Intent result, int notUsed); 483 } 484 } 485