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.Shared.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.app.Activity; 27 import android.app.FragmentManager; 28 import android.content.ClipData; 29 import android.content.ComponentName; 30 import android.content.Intent; 31 import android.content.pm.ResolveInfo; 32 import android.net.Uri; 33 import android.os.AsyncTask; 34 import android.os.Parcelable; 35 import android.provider.DocumentsContract; 36 import android.provider.Settings; 37 import android.util.Log; 38 39 import com.android.documentsui.AbstractActionHandler; 40 import com.android.documentsui.ActivityConfig; 41 import com.android.documentsui.DocumentsAccess; 42 import com.android.documentsui.Injector; 43 import com.android.documentsui.Metrics; 44 import com.android.documentsui.base.BooleanConsumer; 45 import com.android.documentsui.base.DocumentInfo; 46 import com.android.documentsui.base.DocumentStack; 47 import com.android.documentsui.base.Features; 48 import com.android.documentsui.base.Lookup; 49 import com.android.documentsui.base.RootInfo; 50 import com.android.documentsui.base.Shared; 51 import com.android.documentsui.base.State; 52 import com.android.documentsui.dirlist.AnimationView; 53 import com.android.documentsui.dirlist.DocumentDetails; 54 import com.android.documentsui.Model; 55 import com.android.documentsui.picker.ActionHandler.Addons; 56 import com.android.documentsui.queries.SearchViewManager; 57 import com.android.documentsui.roots.ProvidersAccess; 58 import com.android.documentsui.services.FileOperationService; 59 import com.android.internal.annotations.VisibleForTesting; 60 61 import java.util.Arrays; 62 import java.util.concurrent.Executor; 63 64 import javax.annotation.Nullable; 65 66 /** 67 * Provides {@link PickActivity} action specializations to fragments. 68 */ 69 class ActionHandler<T extends Activity & Addons> extends AbstractActionHandler<T> { 70 71 private static final String TAG = "PickerActionHandler"; 72 73 private final Features mFeatures; 74 private final ActivityConfig mConfig; 75 private final Model mModel; 76 private final LastAccessedStorage mLastAccessed; 77 78 ActionHandler( 79 T activity, 80 State state, 81 ProvidersAccess providers, 82 DocumentsAccess docs, 83 SearchViewManager searchMgr, 84 Lookup<String, Executor> executors, 85 Injector injector, 86 LastAccessedStorage lastAccessed) { 87 88 super(activity, state, providers, docs, searchMgr, executors, injector); 89 90 mConfig = injector.config; 91 mFeatures = injector.features; 92 mModel = injector.getModel(); 93 mLastAccessed = lastAccessed; 94 } 95 96 @Override 97 public void initLocation(Intent intent) { 98 assert(intent != null); 99 100 // stack is initialized if it's restored from bundle, which means we're restoring a 101 // previously stored state. 102 if (mState.stack.isInitialized()) { 103 if (DEBUG) Log.d(TAG, "Stack already resolved for uri: " + intent.getData()); 104 restoreRootAndDirectory(); 105 return; 106 } 107 108 // We set the activity title in AsyncTask.onPostExecute(). 109 // To prevent talkback from reading aloud the default title, we clear it here. 110 mActivity.setTitle(""); 111 112 if (launchHomeForCopyDestination(intent)) { 113 if (DEBUG) Log.d(TAG, "Launching directly into Home directory for copy destination."); 114 return; 115 } 116 117 if (mFeatures.isLaunchToDocumentEnabled() && launchToDocument(intent)) { 118 if (DEBUG) Log.d(TAG, "Launched to a document."); 119 return; 120 } 121 122 if (DEBUG) Log.d(TAG, "Load last accessed stack."); 123 loadLastAccessedStack(); 124 } 125 126 @Override 127 protected void launchToDefaultLocation() { 128 loadLastAccessedStack(); 129 } 130 131 private boolean launchHomeForCopyDestination(Intent intent) { 132 // As a matter of policy we don't load the last used stack for the copy 133 // destination picker (user is already in Files app). 134 // Consensus was that the experice was too confusing. 135 // In all other cases, where the user is visiting us from another app 136 // we restore the stack as last used from that app. 137 if (Shared.ACTION_PICK_COPY_DESTINATION.equals(intent.getAction())) { 138 loadHomeDir(); 139 return true; 140 } 141 142 return false; 143 } 144 145 private boolean launchToDocument(Intent intent) { 146 Uri uri = intent.getParcelableExtra(DocumentsContract.EXTRA_INITIAL_URI); 147 if (uri != null) { 148 return launchToDocument(uri); 149 } 150 151 return false; 152 } 153 154 private void loadLastAccessedStack() { 155 if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package."); 156 new LoadLastAccessedStackTask<>( 157 mActivity, mLastAccessed, mState, mProviders, this::onLastAccessedStackLoaded) 158 .execute(); 159 } 160 161 private void onLastAccessedStackLoaded(@Nullable DocumentStack stack) { 162 if (stack == null) { 163 loadDefaultLocation(); 164 } else { 165 mState.stack.reset(stack); 166 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 167 } 168 } 169 170 private void loadDefaultLocation() { 171 switch (mState.action) { 172 case ACTION_CREATE: 173 loadHomeDir(); 174 break; 175 case ACTION_GET_CONTENT: 176 case ACTION_OPEN: 177 case ACTION_OPEN_TREE: 178 mState.stack.changeRoot(mProviders.getRecentsRoot()); 179 mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 180 break; 181 default: 182 throw new UnsupportedOperationException("Unexpected action type: " + mState.action); 183 } 184 } 185 186 @Override 187 public void showAppDetails(ResolveInfo info) { 188 final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 189 intent.setData(Uri.fromParts("package", info.activityInfo.packageName, null)); 190 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); 191 mActivity.startActivity(intent); 192 } 193 194 @Override 195 public void onActivityResult(int requestCode, int resultCode, Intent data) { 196 if (DEBUG) Log.d(TAG, "onActivityResult() code=" + resultCode); 197 198 // Only relay back results when not canceled; otherwise stick around to 199 // let the user pick another app/backend. 200 switch (requestCode) { 201 case CODE_FORWARD: 202 onExternalAppResult(resultCode, data); 203 break; 204 default: 205 super.onActivityResult(requestCode, resultCode, data); 206 } 207 } 208 209 private void onExternalAppResult(int resultCode, Intent data) { 210 if (resultCode != Activity.RESULT_CANCELED) { 211 // Remember that we last picked via external app 212 mLastAccessed.setLastAccessedToExternalApp(mActivity); 213 214 // Pass back result to original caller 215 mActivity.setResult(resultCode, data, 0); 216 mActivity.finish(); 217 } 218 } 219 220 @Override 221 public void openInNewWindow(DocumentStack path) { 222 // Open new window support only depends on vanilla Activity, so it is 223 // implemented in our parent class. But we don't support that in 224 // picking. So as a matter of defensiveness, we override that here. 225 throw new UnsupportedOperationException("Can't open in new window"); 226 } 227 228 @Override 229 public void openRoot(RootInfo root) { 230 Metrics.logRootVisited(mActivity, Metrics.PICKER_SCOPE, root); 231 mActivity.onRootPicked(root); 232 } 233 234 @Override 235 public void openRoot(ResolveInfo info) { 236 Metrics.logAppVisited(mActivity, info); 237 final Intent intent = new Intent(mActivity.getIntent()); 238 intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT); 239 intent.setComponent(new ComponentName( 240 info.activityInfo.applicationInfo.packageName, info.activityInfo.name)); 241 mActivity.startActivityForResult(intent, CODE_FORWARD); 242 } 243 244 @Override 245 public void springOpenDirectory(DocumentInfo doc) { 246 } 247 248 @Override 249 public boolean openDocument(DocumentDetails details, @ViewType int type, 250 @ViewType int fallback) { 251 DocumentInfo doc = mModel.getDocument(details.getModelId()); 252 if (doc == null) { 253 Log.w(TAG, 254 "Can't view item. No Document available for modeId: " + details.getModelId()); 255 return false; 256 } 257 258 if (mConfig.isDocumentEnabled(doc.mimeType, doc.flags, mState)) { 259 mActivity.onDocumentPicked(doc); 260 mSelectionMgr.clearSelection(); 261 return true; 262 } 263 return false; 264 } 265 266 void pickDocument(DocumentInfo pickTarget) { 267 assert(pickTarget != null); 268 Uri result; 269 switch (mState.action) { 270 case ACTION_OPEN_TREE: 271 result = DocumentsContract.buildTreeDocumentUri( 272 pickTarget.authority, pickTarget.documentId); 273 break; 274 case ACTION_PICK_COPY_DESTINATION: 275 result = pickTarget.derivedUri; 276 break; 277 default: 278 // Should not be reached 279 throw new IllegalStateException("Invalid mState.action"); 280 } 281 finishPicking(result); 282 } 283 284 void saveDocument( 285 String mimeType, String displayName, BooleanConsumer inProgressStateListener) { 286 assert(mState.action == ACTION_CREATE); 287 new CreatePickedDocumentTask( 288 mActivity, 289 mDocs, 290 mLastAccessed, 291 mState.stack, 292 mimeType, 293 displayName, 294 inProgressStateListener, 295 this::onPickFinished) 296 .executeOnExecutor(getExecutorForCurrentDirectory()); 297 } 298 299 // User requested to overwrite a target. If confirmed by user #finishPicking() will be 300 // called. 301 void saveDocument(FragmentManager fm, DocumentInfo replaceTarget) { 302 assert(mState.action == ACTION_CREATE); 303 assert(replaceTarget != null); 304 305 // Adding a confirmation dialog breaks an inherited CTS test (testCreateExisting), so we 306 // need to add a feature flag to bypass this feature in ARC++ environment. 307 if (mFeatures.isOverwriteConfirmationEnabled()) { 308 mInjector.dialogs.confirmOverwrite(fm, replaceTarget); 309 } else { 310 finishPicking(replaceTarget.derivedUri); 311 } 312 } 313 314 void finishPicking(Uri... docs) { 315 new SetLastAccessedStackTask( 316 mActivity, 317 mLastAccessed, 318 mState.stack, 319 () -> { 320 onPickFinished(docs); 321 } 322 ) .executeOnExecutor(getExecutorForCurrentDirectory()); 323 } 324 325 private void onPickFinished(Uri... uris) { 326 if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris)); 327 328 final Intent intent = new Intent(); 329 if (uris.length == 1) { 330 intent.setData(uris[0]); 331 } else if (uris.length > 1) { 332 final ClipData clipData = new ClipData( 333 null, mState.acceptMimes, new ClipData.Item(uris[0])); 334 for (int i = 1; i < uris.length; i++) { 335 clipData.addItem(new ClipData.Item(uris[i])); 336 } 337 intent.setClipData(clipData); 338 } 339 340 // TODO: Separate this piece of logic per action. 341 // We don't instantiate different objects for different actions at the first place, so it's 342 // not a easy task to separate this logic cleanly. 343 // Maybe we can add an ActionPolicy class for IoC and provide various behaviors through its 344 // inheritance structure. 345 if (mState.action == ACTION_GET_CONTENT) { 346 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 347 } else if (mState.action == ACTION_OPEN_TREE) { 348 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 349 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 350 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 351 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); 352 } else if (mState.action == ACTION_PICK_COPY_DESTINATION) { 353 // Picking a copy destination is only used internally by us, so we 354 // don't need to extend permissions to the caller. 355 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack); 356 intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType); 357 } else { 358 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 359 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 360 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 361 } 362 363 mActivity.setResult(Activity.RESULT_OK, intent, 0); 364 mActivity.finish(); 365 } 366 367 private Executor getExecutorForCurrentDirectory() { 368 final DocumentInfo cwd = mState.stack.peek(); 369 if (cwd != null && cwd.authority != null) { 370 return mExecutors.lookup(cwd.authority); 371 } else { 372 return AsyncTask.THREAD_POOL_EXECUTOR; 373 } 374 } 375 376 public interface Addons extends CommonAddons { 377 void onDocumentPicked(DocumentInfo doc); 378 379 /** 380 * Overload final method {@link Activity#setResult(int, Intent)} so that we can intercept 381 * this method call in test environment. 382 */ 383 @VisibleForTesting 384 void setResult(int resultCode, Intent result, int notUsed); 385 } 386 } 387