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; 18 19 import static com.android.documentsui.Shared.DEBUG; 20 import static com.android.documentsui.State.ACTION_CREATE; 21 import static com.android.documentsui.State.ACTION_GET_CONTENT; 22 import static com.android.documentsui.State.ACTION_OPEN; 23 import static com.android.documentsui.State.ACTION_OPEN_TREE; 24 import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION; 25 26 import android.app.Activity; 27 import android.app.Fragment; 28 import android.app.FragmentManager; 29 import android.content.ClipData; 30 import android.content.ComponentName; 31 import android.content.ContentProviderClient; 32 import android.content.ContentResolver; 33 import android.content.ContentValues; 34 import android.content.Intent; 35 import android.content.pm.ResolveInfo; 36 import android.database.Cursor; 37 import android.net.Uri; 38 import android.os.Bundle; 39 import android.os.Parcelable; 40 import android.provider.DocumentsContract; 41 import android.support.design.widget.Snackbar; 42 import android.util.Log; 43 import android.view.Menu; 44 import android.view.MenuItem; 45 46 import com.android.documentsui.RecentsProvider.RecentColumns; 47 import com.android.documentsui.RecentsProvider.ResumeColumns; 48 import com.android.documentsui.dirlist.AnimationView; 49 import com.android.documentsui.dirlist.DirectoryFragment; 50 import com.android.documentsui.dirlist.Model; 51 import com.android.documentsui.model.DocumentInfo; 52 import com.android.documentsui.model.DurableUtils; 53 import com.android.documentsui.model.RootInfo; 54 import com.android.documentsui.services.FileOperationService; 55 56 import libcore.io.IoUtils; 57 58 import java.io.FileNotFoundException; 59 import java.io.IOException; 60 import java.util.Arrays; 61 import java.util.Collection; 62 import java.util.List; 63 64 public class DocumentsActivity extends BaseActivity { 65 private static final int CODE_FORWARD = 42; 66 private static final String TAG = "DocumentsActivity"; 67 DocumentsActivity()68 public DocumentsActivity() { 69 super(R.layout.documents_activity, TAG); 70 } 71 72 @Override onCreate(Bundle icicle)73 public void onCreate(Bundle icicle) { 74 super.onCreate(icicle); 75 76 if (mState.action == ACTION_CREATE) { 77 final String mimeType = getIntent().getType(); 78 final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE); 79 SaveFragment.show(getFragmentManager(), mimeType, title); 80 } else if (mState.action == ACTION_OPEN_TREE || 81 mState.action == ACTION_PICK_COPY_DESTINATION) { 82 PickFragment.show(getFragmentManager()); 83 } 84 85 if (mState.action == ACTION_GET_CONTENT) { 86 final Intent moreApps = new Intent(getIntent()); 87 moreApps.setComponent(null); 88 moreApps.setPackage(null); 89 RootsFragment.show(getFragmentManager(), moreApps); 90 } else if (mState.action == ACTION_OPEN || 91 mState.action == ACTION_CREATE || 92 mState.action == ACTION_OPEN_TREE || 93 mState.action == ACTION_PICK_COPY_DESTINATION) { 94 RootsFragment.show(getFragmentManager(), null); 95 } 96 97 if (mState.restored) { 98 if (DEBUG) Log.d(TAG, "Stack already resolved"); 99 } else { 100 // We set the activity title in AsyncTask.onPostExecute(). 101 // To prevent talkback from reading aloud the default title, we clear it here. 102 setTitle(""); 103 104 // As a matter of policy we don't load the last used stack for the copy 105 // destination picker (user is already in Files app). 106 // Concensus was that the experice was too confusing. 107 // In all other cases, where the user is visiting us from another app 108 // we restore the stack as last used from that app. 109 if (mState.action == ACTION_PICK_COPY_DESTINATION) { 110 if (DEBUG) Log.d(TAG, "Launching directly into Home directory."); 111 loadRoot(getDefaultRoot()); 112 } else { 113 if (DEBUG) Log.d(TAG, "Attempting to load last used stack for calling package."); 114 new LoadLastUsedStackTask(this).execute(); 115 } 116 } 117 } 118 119 @Override includeState(State state)120 void includeState(State state) { 121 final Intent intent = getIntent(); 122 final String action = intent.getAction(); 123 if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) { 124 state.action = ACTION_OPEN; 125 } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) { 126 state.action = ACTION_CREATE; 127 } else if (Intent.ACTION_GET_CONTENT.equals(action)) { 128 state.action = ACTION_GET_CONTENT; 129 } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) { 130 state.action = ACTION_OPEN_TREE; 131 } else if (Shared.ACTION_PICK_COPY_DESTINATION.equals(action)) { 132 state.action = ACTION_PICK_COPY_DESTINATION; 133 } 134 135 if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT) { 136 state.allowMultiple = intent.getBooleanExtra( 137 Intent.EXTRA_ALLOW_MULTIPLE, false); 138 } 139 140 if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT 141 || state.action == ACTION_CREATE) { 142 state.openableOnly = intent.hasCategory(Intent.CATEGORY_OPENABLE); 143 } 144 145 if (state.action == ACTION_PICK_COPY_DESTINATION) { 146 // Indicates that a copy operation (or move) includes a directory. 147 // Why? Directory creation isn't supported by some roots (like Downloads). 148 // This allows us to restrict available roots to just those with support. 149 state.directoryCopy = intent.getBooleanExtra( 150 Shared.EXTRA_DIRECTORY_COPY, false); 151 state.copyOperationSubType = intent.getIntExtra( 152 FileOperationService.EXTRA_OPERATION, 153 FileOperationService.OPERATION_COPY); 154 } 155 } 156 onAppPicked(ResolveInfo info)157 public void onAppPicked(ResolveInfo info) { 158 final Intent intent = new Intent(getIntent()); 159 intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT); 160 intent.setComponent(new ComponentName( 161 info.activityInfo.applicationInfo.packageName, info.activityInfo.name)); 162 startActivityForResult(intent, CODE_FORWARD); 163 } 164 165 @Override onActivityResult(int requestCode, int resultCode, Intent data)166 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 167 if (DEBUG) Log.d(TAG, "onActivityResult() code=" + resultCode); 168 169 // Only relay back results when not canceled; otherwise stick around to 170 // let the user pick another app/backend. 171 if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) { 172 173 // Remember that we last picked via external app 174 final String packageName = getCallingPackageMaybeExtra(); 175 final ContentValues values = new ContentValues(); 176 values.put(ResumeColumns.EXTERNAL, 1); 177 getContentResolver().insert(RecentsProvider.buildResume(packageName), values); 178 179 // Pass back result to original caller 180 setResult(resultCode, data); 181 finish(); 182 } else { 183 super.onActivityResult(requestCode, resultCode, data); 184 } 185 } 186 187 @Override onPostCreate(Bundle savedInstanceState)188 protected void onPostCreate(Bundle savedInstanceState) { 189 super.onPostCreate(savedInstanceState); 190 mDrawer.update(); 191 mNavigator.update(); 192 } 193 194 @Override getDrawerTitle()195 public String getDrawerTitle() { 196 String title = getIntent().getStringExtra(DocumentsContract.EXTRA_PROMPT); 197 if (title == null) { 198 if (mState.action == ACTION_OPEN || 199 mState.action == ACTION_GET_CONTENT || 200 mState.action == ACTION_OPEN_TREE) { 201 title = getResources().getString(R.string.title_open); 202 } else if (mState.action == ACTION_CREATE || 203 mState.action == ACTION_PICK_COPY_DESTINATION) { 204 title = getResources().getString(R.string.title_save); 205 } else { 206 // If all else fails, just call it "Documents". 207 title = getResources().getString(R.string.app_label); 208 } 209 } 210 211 return title; 212 } 213 214 @Override onPrepareOptionsMenu(Menu menu)215 public boolean onPrepareOptionsMenu(Menu menu) { 216 super.onPrepareOptionsMenu(menu); 217 218 final DocumentInfo cwd = getCurrentDirectory(); 219 220 boolean picking = mState.action == ACTION_CREATE 221 || mState.action == ACTION_OPEN_TREE 222 || mState.action == ACTION_PICK_COPY_DESTINATION; 223 224 if (picking) { 225 // May already be hidden because the root 226 // doesn't support search. 227 mSearchManager.showMenu(false); 228 } 229 230 final MenuItem createDir = menu.findItem(R.id.menu_create_dir); 231 final MenuItem grid = menu.findItem(R.id.menu_grid); 232 final MenuItem list = menu.findItem(R.id.menu_list); 233 final MenuItem fileSize = menu.findItem(R.id.menu_file_size); 234 235 236 createDir.setVisible(picking); 237 createDir.setEnabled(canCreateDirectory()); 238 239 // No display options in recent directories 240 boolean inRecents = cwd == null; 241 if (picking && inRecents) { 242 grid.setVisible(false); 243 list.setVisible(false); 244 } 245 246 fileSize.setVisible(fileSize.isVisible() && !picking); 247 248 if (mState.action == ACTION_CREATE) { 249 final FragmentManager fm = getFragmentManager(); 250 SaveFragment.get(fm).prepareForDirectory(cwd); 251 } 252 253 Menus.disableHiddenItems(menu); 254 255 return true; 256 } 257 258 @Override refreshDirectory(int anim)259 void refreshDirectory(int anim) { 260 final FragmentManager fm = getFragmentManager(); 261 final RootInfo root = getCurrentRoot(); 262 final DocumentInfo cwd = getCurrentDirectory(); 263 264 if (cwd == null) { 265 // No directory means recents 266 if (mState.action == ACTION_CREATE || 267 mState.action == ACTION_OPEN_TREE || 268 mState.action == ACTION_PICK_COPY_DESTINATION) { 269 RecentsCreateFragment.show(fm); 270 } else { 271 DirectoryFragment.showRecentsOpen(fm, anim); 272 273 // In recents we pick layout mode based on the mimetype, 274 // picking GRID for visual types. We intentionally don't 275 // consult a user's saved preferences here since they are 276 // set per root (not per root and per mimetype). 277 boolean visualMimes = MimePredicate.mimeMatches( 278 MimePredicate.VISUAL_MIMES, mState.acceptMimes); 279 mState.derivedMode = visualMimes ? State.MODE_GRID : State.MODE_LIST; 280 } 281 } else { 282 // Normal boring directory 283 DirectoryFragment.showDirectory(fm, root, cwd, anim); 284 } 285 286 // Forget any replacement target 287 if (mState.action == ACTION_CREATE) { 288 final SaveFragment save = SaveFragment.get(fm); 289 if (save != null) { 290 save.setReplaceTarget(null); 291 } 292 } 293 294 if (mState.action == ACTION_OPEN_TREE || 295 mState.action == ACTION_PICK_COPY_DESTINATION) { 296 final PickFragment pick = PickFragment.get(fm); 297 if (pick != null) { 298 pick.setPickTarget(mState.action, mState.copyOperationSubType, cwd); 299 } 300 } 301 } 302 onSaveRequested(DocumentInfo replaceTarget)303 void onSaveRequested(DocumentInfo replaceTarget) { 304 new ExistingFinishTask(this, replaceTarget.derivedUri) 305 .executeOnExecutor(getExecutorForCurrentDirectory()); 306 } 307 308 @Override onDirectoryCreated(DocumentInfo doc)309 void onDirectoryCreated(DocumentInfo doc) { 310 assert(doc.isDirectory()); 311 openContainerDocument(doc); 312 } 313 onSaveRequested(String mimeType, String displayName)314 void onSaveRequested(String mimeType, String displayName) { 315 new CreateFinishTask(this, mimeType, displayName) 316 .executeOnExecutor(getExecutorForCurrentDirectory()); 317 } 318 319 @Override onRootPicked(RootInfo root)320 void onRootPicked(RootInfo root) { 321 super.onRootPicked(root); 322 mNavigator.revealRootsDrawer(false); 323 } 324 325 @Override onDocumentPicked(DocumentInfo doc, Model model)326 public void onDocumentPicked(DocumentInfo doc, Model model) { 327 final FragmentManager fm = getFragmentManager(); 328 if (doc.isContainer()) { 329 openContainerDocument(doc); 330 } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 331 // Explicit file picked, return 332 new ExistingFinishTask(this, doc.derivedUri) 333 .executeOnExecutor(getExecutorForCurrentDirectory()); 334 } else if (mState.action == ACTION_CREATE) { 335 // Replace selected file 336 SaveFragment.get(fm).setReplaceTarget(doc); 337 } 338 } 339 340 @Override onDocumentsPicked(List<DocumentInfo> docs)341 public void onDocumentsPicked(List<DocumentInfo> docs) { 342 if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 343 final int size = docs.size(); 344 final Uri[] uris = new Uri[size]; 345 for (int i = 0; i < size; i++) { 346 uris[i] = docs.get(i).derivedUri; 347 } 348 new ExistingFinishTask(this, uris) 349 .executeOnExecutor(getExecutorForCurrentDirectory()); 350 } 351 } 352 onPickRequested(DocumentInfo pickTarget)353 public void onPickRequested(DocumentInfo pickTarget) { 354 Uri result; 355 if (mState.action == ACTION_OPEN_TREE) { 356 result = DocumentsContract.buildTreeDocumentUri( 357 pickTarget.authority, pickTarget.documentId); 358 } else if (mState.action == ACTION_PICK_COPY_DESTINATION) { 359 result = pickTarget.derivedUri; 360 } else { 361 // Should not be reached. 362 throw new IllegalStateException("Invalid mState.action."); 363 } 364 new PickFinishTask(this, result).executeOnExecutor(getExecutorForCurrentDirectory()); 365 } 366 writeStackToRecentsBlocking()367 void writeStackToRecentsBlocking() { 368 final ContentResolver resolver = getContentResolver(); 369 final ContentValues values = new ContentValues(); 370 371 final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack); 372 if (mState.action == ACTION_CREATE || 373 mState.action == ACTION_OPEN_TREE || 374 mState.action == ACTION_PICK_COPY_DESTINATION) { 375 // Remember stack for last create 376 values.clear(); 377 values.put(RecentColumns.KEY, mState.stack.buildKey()); 378 values.put(RecentColumns.STACK, rawStack); 379 resolver.insert(RecentsProvider.buildRecent(), values); 380 } 381 382 // Remember location for next app launch 383 final String packageName = getCallingPackageMaybeExtra(); 384 values.clear(); 385 values.put(ResumeColumns.STACK, rawStack); 386 values.put(ResumeColumns.EXTERNAL, 0); 387 resolver.insert(RecentsProvider.buildResume(packageName), values); 388 } 389 390 @Override onTaskFinished(Uri... uris)391 void onTaskFinished(Uri... uris) { 392 if (DEBUG) Log.d(TAG, "onFinished() " + Arrays.toString(uris)); 393 394 final Intent intent = new Intent(); 395 if (uris.length == 1) { 396 intent.setData(uris[0]); 397 } else if (uris.length > 1) { 398 final ClipData clipData = new ClipData( 399 null, mState.acceptMimes, new ClipData.Item(uris[0])); 400 for (int i = 1; i < uris.length; i++) { 401 clipData.addItem(new ClipData.Item(uris[i])); 402 } 403 intent.setClipData(clipData); 404 } 405 406 if (mState.action == ACTION_GET_CONTENT) { 407 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 408 } else if (mState.action == ACTION_OPEN_TREE) { 409 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 410 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 411 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 412 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); 413 } else if (mState.action == ACTION_PICK_COPY_DESTINATION) { 414 // Picking a copy destination is only used internally by us, so we 415 // don't need to extend permissions to the caller. 416 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack); 417 intent.putExtra(FileOperationService.EXTRA_OPERATION, mState.copyOperationSubType); 418 } else { 419 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 420 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 421 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 422 } 423 424 setResult(Activity.RESULT_OK, intent); 425 finish(); 426 } 427 428 get(Fragment fragment)429 public static DocumentsActivity get(Fragment fragment) { 430 return (DocumentsActivity) fragment.getActivity(); 431 } 432 433 /** 434 * Loads the last used path (stack) from Recents (history). 435 * The path selected is based on the calling package name. So the last 436 * path for an app like Gmail can be different than the last path 437 * for an app like DropBox. 438 */ 439 private static final class LoadLastUsedStackTask 440 extends PairedTask<DocumentsActivity, Void, Void> { 441 442 private volatile boolean mRestoredStack; 443 private volatile boolean mExternal; 444 private State mState; 445 LoadLastUsedStackTask(DocumentsActivity activity)446 public LoadLastUsedStackTask(DocumentsActivity activity) { 447 super(activity); 448 mState = activity.mState; 449 } 450 451 @Override run(Void... params)452 protected Void run(Void... params) { 453 if (DEBUG && !mState.stack.isEmpty()) { 454 Log.w(TAG, "Overwriting existing stack."); 455 } 456 RootsCache roots = DocumentsApplication.getRootsCache(mOwner); 457 458 String packageName = mOwner.getCallingPackageMaybeExtra(); 459 Uri resumeUri = RecentsProvider.buildResume(packageName); 460 Cursor cursor = mOwner.getContentResolver().query(resumeUri, null, null, null, null); 461 try { 462 if (cursor.moveToFirst()) { 463 mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0; 464 final byte[] rawStack = cursor.getBlob( 465 cursor.getColumnIndex(ResumeColumns.STACK)); 466 DurableUtils.readFromArray(rawStack, mState.stack); 467 mRestoredStack = true; 468 } 469 } catch (IOException e) { 470 Log.w(TAG, "Failed to resume: " + e); 471 } finally { 472 IoUtils.closeQuietly(cursor); 473 } 474 475 if (mRestoredStack) { 476 // Update the restored stack to ensure we have freshest data 477 final Collection<RootInfo> matchingRoots = roots.getMatchingRootsBlocking(mState); 478 try { 479 mState.stack.updateRoot(matchingRoots); 480 mState.stack.updateDocuments(mOwner.getContentResolver()); 481 } catch (FileNotFoundException e) { 482 Log.w(TAG, "Failed to restore stack for package: " + packageName 483 + " because of error: "+ e); 484 mState.stack.reset(); 485 mRestoredStack = false; 486 } 487 } 488 489 return null; 490 } 491 492 @Override finish(Void result)493 protected void finish(Void result) { 494 mState.restored = true; 495 mState.external = mExternal; 496 mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); 497 } 498 } 499 500 private static final class PickFinishTask extends PairedTask<DocumentsActivity, Void, Void> { 501 private final Uri mUri; 502 PickFinishTask(DocumentsActivity activity, Uri uri)503 public PickFinishTask(DocumentsActivity activity, Uri uri) { 504 super(activity); 505 mUri = uri; 506 } 507 508 @Override run(Void... params)509 protected Void run(Void... params) { 510 mOwner.writeStackToRecentsBlocking(); 511 return null; 512 } 513 514 @Override finish(Void result)515 protected void finish(Void result) { 516 mOwner.onTaskFinished(mUri); 517 } 518 } 519 520 private static final class ExistingFinishTask extends PairedTask<DocumentsActivity, Void, Void> { 521 private final Uri[] mUris; 522 ExistingFinishTask(DocumentsActivity activity, Uri... uris)523 public ExistingFinishTask(DocumentsActivity activity, Uri... uris) { 524 super(activity); 525 mUris = uris; 526 } 527 528 @Override run(Void... params)529 protected Void run(Void... params) { 530 mOwner.writeStackToRecentsBlocking(); 531 return null; 532 } 533 534 @Override finish(Void result)535 protected void finish(Void result) { 536 mOwner.onTaskFinished(mUris); 537 } 538 } 539 540 /** 541 * Task that creates a new document in the background. 542 */ 543 private static final class CreateFinishTask extends PairedTask<DocumentsActivity, Void, Uri> { 544 private final String mMimeType; 545 private final String mDisplayName; 546 CreateFinishTask(DocumentsActivity activity, String mimeType, String displayName)547 public CreateFinishTask(DocumentsActivity activity, String mimeType, String displayName) { 548 super(activity); 549 mMimeType = mimeType; 550 mDisplayName = displayName; 551 } 552 553 @Override prepare()554 protected void prepare() { 555 mOwner.setPending(true); 556 } 557 558 @Override run(Void... params)559 protected Uri run(Void... params) { 560 final ContentResolver resolver = mOwner.getContentResolver(); 561 final DocumentInfo cwd = mOwner.getCurrentDirectory(); 562 563 ContentProviderClient client = null; 564 Uri childUri = null; 565 try { 566 client = DocumentsApplication.acquireUnstableProviderOrThrow( 567 resolver, cwd.derivedUri.getAuthority()); 568 childUri = DocumentsContract.createDocument( 569 client, cwd.derivedUri, mMimeType, mDisplayName); 570 } catch (Exception e) { 571 Log.w(TAG, "Failed to create document", e); 572 } finally { 573 ContentProviderClient.releaseQuietly(client); 574 } 575 576 if (childUri != null) { 577 mOwner.writeStackToRecentsBlocking(); 578 } 579 580 return childUri; 581 } 582 583 @Override finish(Uri result)584 protected void finish(Uri result) { 585 if (result != null) { 586 mOwner.onTaskFinished(result); 587 } else { 588 Snackbars.makeSnackbar( 589 mOwner, R.string.save_error, Snackbar.LENGTH_SHORT).show(); 590 } 591 592 mOwner.setPending(false); 593 } 594 } 595 } 596