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