DocumentsActivity.java revision a8f03a2d81394002242d1909488eaa4a0467ea05
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 17package com.android.documentsui; 18 19import static com.android.documentsui.State.ACTION_CREATE; 20import static com.android.documentsui.State.ACTION_GET_CONTENT; 21import static com.android.documentsui.State.ACTION_OPEN; 22import static com.android.documentsui.State.ACTION_OPEN_TREE; 23import static com.android.documentsui.State.ACTION_PICK_COPY_DESTINATION; 24import static com.android.documentsui.dirlist.DirectoryFragment.ANIM_NONE; 25 26import android.app.Activity; 27import android.app.Fragment; 28import android.app.FragmentManager; 29import android.content.ClipData; 30import android.content.ComponentName; 31import android.content.ContentProviderClient; 32import android.content.ContentResolver; 33import android.content.ContentValues; 34import android.content.Intent; 35import android.content.pm.ResolveInfo; 36import android.content.res.Resources; 37import android.net.Uri; 38import android.os.AsyncTask; 39import android.os.Bundle; 40import android.os.Parcelable; 41import android.provider.DocumentsContract; 42import android.support.design.widget.Snackbar; 43import android.util.Log; 44import android.view.Menu; 45import android.view.MenuItem; 46import android.view.View; 47import android.widget.BaseAdapter; 48import android.widget.Spinner; 49import android.widget.Toolbar; 50 51import com.android.documentsui.RecentsProvider.RecentColumns; 52import com.android.documentsui.RecentsProvider.ResumeColumns; 53import com.android.documentsui.dirlist.DirectoryFragment; 54import com.android.documentsui.model.DocumentInfo; 55import com.android.documentsui.model.DurableUtils; 56import com.android.documentsui.model.RootInfo; 57 58import java.util.Arrays; 59import java.util.List; 60 61public class DocumentsActivity extends BaseActivity { 62 private static final int CODE_FORWARD = 42; 63 private static final String TAG = "DocumentsActivity"; 64 65 private Toolbar mToolbar; 66 private Spinner mToolbarStack; 67 68 private Toolbar mRootsToolbar; 69 70 private ItemSelectedListener mStackListener; 71 private BaseAdapter mStackAdapter; 72 73 public DocumentsActivity() { 74 super(R.layout.docs_activity, TAG); 75 } 76 77 @Override 78 public void onCreate(Bundle icicle) { 79 super.onCreate(icicle); 80 81 final Resources res = getResources(); 82 83 mDrawer = DrawerController.create(this); 84 mToolbar = (Toolbar) findViewById(R.id.toolbar); 85 86 mStackAdapter = new StackAdapter(); 87 mStackListener = new ItemSelectedListener(); 88 mToolbarStack = (Spinner) findViewById(R.id.stack); 89 mToolbarStack.setOnItemSelectedListener(mStackListener); 90 91 mRootsToolbar = (Toolbar) findViewById(R.id.roots_toolbar); 92 93 setActionBar(mToolbar); 94 95 if (mState.action == ACTION_CREATE) { 96 final String mimeType = getIntent().getType(); 97 final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE); 98 SaveFragment.show(getFragmentManager(), mimeType, title); 99 } else if (mState.action == ACTION_OPEN_TREE || 100 mState.action == ACTION_PICK_COPY_DESTINATION) { 101 PickFragment.show(getFragmentManager()); 102 } 103 104 if (mState.action == ACTION_GET_CONTENT) { 105 final Intent moreApps = new Intent(getIntent()); 106 moreApps.setComponent(null); 107 moreApps.setPackage(null); 108 RootsFragment.show(getFragmentManager(), moreApps); 109 } else if (mState.action == ACTION_OPEN || 110 mState.action == ACTION_CREATE || 111 mState.action == ACTION_OPEN_TREE || 112 mState.action == ACTION_PICK_COPY_DESTINATION) { 113 RootsFragment.show(getFragmentManager(), null); 114 } 115 116 if (!mState.restored) { 117 // In this case, we set the activity title in AsyncTask.onPostExecute(). To prevent 118 // talkback from reading aloud the default title, we clear it here. 119 setTitle(""); 120 new RestoreStackTask().execute(); 121 } else { 122 onCurrentDirectoryChanged(ANIM_NONE); 123 } 124 } 125 126 @Override 127 State buildState() { 128 State state = buildDefaultState(); 129 130 final Intent intent = getIntent(); 131 final String action = intent.getAction(); 132 if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) { 133 state.action = ACTION_OPEN; 134 } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) { 135 state.action = ACTION_CREATE; 136 } else if (Intent.ACTION_GET_CONTENT.equals(action)) { 137 state.action = ACTION_GET_CONTENT; 138 } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) { 139 state.action = ACTION_OPEN_TREE; 140 } else if (Shared.ACTION_PICK_COPY_DESTINATION.equals(action)) { 141 state.action = ACTION_PICK_COPY_DESTINATION; 142 } 143 144 if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT) { 145 state.allowMultiple = intent.getBooleanExtra( 146 Intent.EXTRA_ALLOW_MULTIPLE, false); 147 } 148 149 if (state.action == ACTION_OPEN || state.action == ACTION_GET_CONTENT 150 || state.action == ACTION_CREATE) { 151 state.openableOnly = intent.hasCategory(Intent.CATEGORY_OPENABLE); 152 } 153 154 if (state.action == ACTION_PICK_COPY_DESTINATION) { 155 state.directoryCopy = intent.getBooleanExtra( 156 Shared.EXTRA_DIRECTORY_COPY, false); 157 state.transferMode = intent.getIntExtra(CopyService.EXTRA_TRANSFER_MODE, 158 CopyService.TRANSFER_MODE_COPY); 159 } 160 161 return state; 162 } 163 164 @Override 165 void onStackRestored(boolean restored, boolean external) { 166 // Show drawer when no stack restored, but only when requesting 167 // non-visual content. However, if we last used an external app, 168 // drawer is always shown. 169 170 boolean showDrawer = false; 171 if (!restored) { 172 showDrawer = true; 173 } 174 if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) { 175 showDrawer = false; 176 } 177 if (external && mState.action == ACTION_GET_CONTENT) { 178 showDrawer = true; 179 } 180 181 if (showDrawer) { 182 setRootsDrawerOpen(true); 183 } 184 } 185 186 public void onAppPicked(ResolveInfo info) { 187 final Intent intent = new Intent(getIntent()); 188 intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT); 189 intent.setComponent(new ComponentName( 190 info.activityInfo.applicationInfo.packageName, info.activityInfo.name)); 191 startActivityForResult(intent, CODE_FORWARD); 192 } 193 194 @Override 195 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 196 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 if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) { 201 202 // Remember that we last picked via external app 203 final String packageName = getCallingPackageMaybeExtra(); 204 final ContentValues values = new ContentValues(); 205 values.put(ResumeColumns.EXTERNAL, 1); 206 getContentResolver().insert(RecentsProvider.buildResume(packageName), values); 207 208 // Pass back result to original caller 209 setResult(resultCode, data); 210 finish(); 211 } else { 212 super.onActivityResult(requestCode, resultCode, data); 213 } 214 } 215 216 @Override 217 protected void onPostCreate(Bundle savedInstanceState) { 218 super.onPostCreate(savedInstanceState); 219 mDrawer.syncState(); 220 updateActionBar(); 221 } 222 223 public void setRootsDrawerOpen(boolean open) { 224 mDrawer.setOpen(open); 225 } 226 227 @Override 228 public void updateActionBar() { 229 if (mRootsToolbar != null) { 230 final String prompt = getIntent().getStringExtra(DocumentsContract.EXTRA_PROMPT); 231 if (prompt != null) { 232 mRootsToolbar.setTitle(prompt); 233 } else { 234 if (mState.action == ACTION_OPEN || 235 mState.action == ACTION_GET_CONTENT || 236 mState.action == ACTION_OPEN_TREE) { 237 mRootsToolbar.setTitle(R.string.title_open); 238 } else if (mState.action == ACTION_CREATE || 239 mState.action == ACTION_PICK_COPY_DESTINATION) { 240 mRootsToolbar.setTitle(R.string.title_save); 241 } 242 } 243 } 244 245 if (mDrawer.isUnlocked()) { 246 mToolbar.setNavigationIcon(R.drawable.ic_hamburger); 247 mToolbar.setNavigationContentDescription(R.string.drawer_open); 248 mToolbar.setNavigationOnClickListener( 249 new View.OnClickListener() { 250 @Override 251 public void onClick(View v) { 252 setRootsDrawerOpen(true); 253 } 254 }); 255 } else { 256 mToolbar.setNavigationIcon(null); 257 mToolbar.setNavigationContentDescription(R.string.drawer_open); 258 mToolbar.setNavigationOnClickListener(null); 259 } 260 261 if (mSearchManager.isExpanded()) { 262 mToolbar.setTitle(null); 263 mToolbarStack.setVisibility(View.GONE); 264 mToolbarStack.setAdapter(null); 265 } else { 266 if (mState.stack.size() <= 1) { 267 mToolbar.setTitle(getCurrentRoot().title); 268 mToolbarStack.setVisibility(View.GONE); 269 mToolbarStack.setAdapter(null); 270 } else { 271 mToolbar.setTitle(null); 272 mToolbarStack.setVisibility(View.VISIBLE); 273 mToolbarStack.setAdapter(mStackAdapter); 274 275 mStackListener.mIgnoreNextNavigation = true; 276 mToolbarStack.setSelection(mStackAdapter.getCount() - 1); 277 } 278 } 279 } 280 281 @Override 282 public boolean onCreateOptionsMenu(Menu menu) { 283 boolean showMenu = super.onCreateOptionsMenu(menu); 284 285 expandMenus(menu); 286 return showMenu; 287 } 288 289 @Override 290 public boolean onPrepareOptionsMenu(Menu menu) { 291 super.onPrepareOptionsMenu(menu); 292 293 final DocumentInfo cwd = getCurrentDirectory(); 294 295 final MenuItem createDir = menu.findItem(R.id.menu_create_dir); 296 final MenuItem grid = menu.findItem(R.id.menu_grid); 297 final MenuItem list = menu.findItem(R.id.menu_list); 298 final MenuItem fileSize = menu.findItem(R.id.menu_file_size); 299 final MenuItem settings = menu.findItem(R.id.menu_settings); 300 301 boolean recents = cwd == null; 302 boolean picking = mState.action == ACTION_CREATE 303 || mState.action == ACTION_OPEN_TREE 304 || mState.action == ACTION_PICK_COPY_DESTINATION; 305 306 createDir.setVisible(picking && !recents && cwd.isCreateSupported()); 307 mSearchManager.showMenu(!picking); 308 309 // No display options in recent directories 310 grid.setVisible(!(picking && recents)); 311 list.setVisible(!(picking && recents)); 312 313 fileSize.setVisible(fileSize.isVisible() && !picking); 314 settings.setVisible(false); 315 316 if (mState.action == ACTION_CREATE) { 317 final FragmentManager fm = getFragmentManager(); 318 SaveFragment.get(fm).prepareForDirectory(cwd); 319 } 320 321 Menus.disableHiddenItems(menu); 322 323 return true; 324 } 325 326 @Override 327 public boolean onOptionsItemSelected(MenuItem item) { 328 return mDrawer.onOptionsItemSelected(item) || super.onOptionsItemSelected(item); 329 } 330 331 @Override 332 void onDirectoryChanged(int anim) { 333 final FragmentManager fm = getFragmentManager(); 334 final RootInfo root = getCurrentRoot(); 335 final DocumentInfo cwd = getCurrentDirectory(); 336 337 if (cwd == null) { 338 // No directory means recents 339 if (mState.action == ACTION_CREATE || 340 mState.action == ACTION_OPEN_TREE || 341 mState.action == ACTION_PICK_COPY_DESTINATION) { 342 RecentsCreateFragment.show(fm); 343 } else { 344 DirectoryFragment.showRecentsOpen(fm, anim); 345 346 // Start recents in grid when requesting visual things 347 final boolean visualMimes = MimePredicate.mimeMatches( 348 MimePredicate.VISUAL_MIMES, mState.acceptMimes); 349 mState.userMode = visualMimes ? State.MODE_GRID : State.MODE_LIST; 350 mState.derivedMode = mState.userMode; 351 } 352 } else { 353 if (mState.currentSearch != null) { 354 // Ongoing search 355 DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim); 356 } else { 357 // Normal boring directory 358 DirectoryFragment.showNormal(fm, root, cwd, anim); 359 } 360 } 361 362 // Forget any replacement target 363 if (mState.action == ACTION_CREATE) { 364 final SaveFragment save = SaveFragment.get(fm); 365 if (save != null) { 366 save.setReplaceTarget(null); 367 } 368 } 369 370 if (mState.action == ACTION_OPEN_TREE || 371 mState.action == ACTION_PICK_COPY_DESTINATION) { 372 final PickFragment pick = PickFragment.get(fm); 373 if (pick != null) { 374 pick.setPickTarget(mState.action, mState.transferMode, cwd); 375 } 376 } 377 } 378 379 void onSaveRequested(DocumentInfo replaceTarget) { 380 new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getExecutorForCurrentDirectory()); 381 } 382 383 void onSaveRequested(String mimeType, String displayName) { 384 new CreateFinishTask(mimeType, displayName).executeOnExecutor(getExecutorForCurrentDirectory()); 385 } 386 387 @Override 388 void onRootPicked(RootInfo root) { 389 super.onRootPicked(root); 390 setRootsDrawerOpen(false); 391 } 392 393 @Override 394 public void onDocumentPicked(DocumentInfo doc, DocumentContext context) { 395 final FragmentManager fm = getFragmentManager(); 396 if (doc.isContainer()) { 397 openContainerDocument(doc); 398 } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 399 // Explicit file picked, return 400 new ExistingFinishTask(doc.derivedUri).executeOnExecutor(getExecutorForCurrentDirectory()); 401 } else if (mState.action == ACTION_CREATE) { 402 // Replace selected file 403 SaveFragment.get(fm).setReplaceTarget(doc); 404 } 405 } 406 407 @Override 408 public void onDocumentsPicked(List<DocumentInfo> docs) { 409 if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 410 final int size = docs.size(); 411 final Uri[] uris = new Uri[size]; 412 for (int i = 0; i < size; i++) { 413 uris[i] = docs.get(i).derivedUri; 414 } 415 new ExistingFinishTask(uris).executeOnExecutor(getExecutorForCurrentDirectory()); 416 } 417 } 418 419 public void onPickRequested(DocumentInfo pickTarget) { 420 Uri result; 421 if (mState.action == ACTION_OPEN_TREE) { 422 result = DocumentsContract.buildTreeDocumentUri( 423 pickTarget.authority, pickTarget.documentId); 424 } else if (mState.action == ACTION_PICK_COPY_DESTINATION) { 425 result = pickTarget.derivedUri; 426 } else { 427 // Should not be reached. 428 throw new IllegalStateException("Invalid mState.action."); 429 } 430 new PickFinishTask(result).executeOnExecutor(getExecutorForCurrentDirectory()); 431 } 432 433 @Override 434 void saveStackBlocking() { 435 final ContentResolver resolver = getContentResolver(); 436 final ContentValues values = new ContentValues(); 437 438 final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack); 439 if (mState.action == ACTION_CREATE || 440 mState.action == ACTION_OPEN_TREE || 441 mState.action == ACTION_PICK_COPY_DESTINATION) { 442 // Remember stack for last create 443 values.clear(); 444 values.put(RecentColumns.KEY, mState.stack.buildKey()); 445 values.put(RecentColumns.STACK, rawStack); 446 resolver.insert(RecentsProvider.buildRecent(), values); 447 } 448 449 // Remember location for next app launch 450 final String packageName = getCallingPackageMaybeExtra(); 451 values.clear(); 452 values.put(ResumeColumns.STACK, rawStack); 453 values.put(ResumeColumns.EXTERNAL, 0); 454 resolver.insert(RecentsProvider.buildResume(packageName), values); 455 } 456 457 @Override 458 void onTaskFinished(Uri... uris) { 459 Log.d(TAG, "onFinished() " + Arrays.toString(uris)); 460 461 final Intent intent = new Intent(); 462 if (uris.length == 1) { 463 intent.setData(uris[0]); 464 } else if (uris.length > 1) { 465 final ClipData clipData = new ClipData( 466 null, mState.acceptMimes, new ClipData.Item(uris[0])); 467 for (int i = 1; i < uris.length; i++) { 468 clipData.addItem(new ClipData.Item(uris[i])); 469 } 470 intent.setClipData(clipData); 471 } 472 473 if (mState.action == ACTION_GET_CONTENT) { 474 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 475 } else if (mState.action == ACTION_OPEN_TREE) { 476 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 477 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 478 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 479 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); 480 } else if (mState.action == ACTION_PICK_COPY_DESTINATION) { 481 // Picking a copy destination is only used internally by us, so we 482 // don't need to extend permissions to the caller. 483 intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack); 484 intent.putExtra(CopyService.EXTRA_TRANSFER_MODE, mState.transferMode); 485 } else { 486 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 487 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 488 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 489 } 490 491 setResult(Activity.RESULT_OK, intent); 492 finish(); 493 } 494 495 public static DocumentsActivity get(Fragment fragment) { 496 return (DocumentsActivity) fragment.getActivity(); 497 } 498 499 private final class PickFinishTask extends AsyncTask<Void, Void, Void> { 500 private final Uri mUri; 501 502 public PickFinishTask(Uri uri) { 503 mUri = uri; 504 } 505 506 @Override 507 protected Void doInBackground(Void... params) { 508 saveStackBlocking(); 509 return null; 510 } 511 512 @Override 513 protected void onPostExecute(Void result) { 514 onTaskFinished(mUri); 515 } 516 } 517 518 final class ExistingFinishTask extends AsyncTask<Void, Void, Void> { 519 private final Uri[] mUris; 520 521 public ExistingFinishTask(Uri... uris) { 522 mUris = uris; 523 } 524 525 @Override 526 protected Void doInBackground(Void... params) { 527 saveStackBlocking(); 528 return null; 529 } 530 531 @Override 532 protected void onPostExecute(Void result) { 533 onTaskFinished(mUris); 534 } 535 } 536 537 /** 538 * Task that creates a new document in the background. 539 */ 540 final class CreateFinishTask extends AsyncTask<Void, Void, Uri> { 541 private final String mMimeType; 542 private final String mDisplayName; 543 544 public CreateFinishTask(String mimeType, String displayName) { 545 mMimeType = mimeType; 546 mDisplayName = displayName; 547 } 548 549 @Override 550 protected void onPreExecute() { 551 setPending(true); 552 } 553 554 @Override 555 protected Uri doInBackground(Void... params) { 556 final ContentResolver resolver = getContentResolver(); 557 final DocumentInfo cwd = getCurrentDirectory(); 558 559 ContentProviderClient client = null; 560 Uri childUri = null; 561 try { 562 client = DocumentsApplication.acquireUnstableProviderOrThrow( 563 resolver, cwd.derivedUri.getAuthority()); 564 childUri = DocumentsContract.createDocument( 565 client, cwd.derivedUri, mMimeType, mDisplayName); 566 } catch (Exception e) { 567 Log.w(TAG, "Failed to create document", e); 568 } finally { 569 ContentProviderClient.releaseQuietly(client); 570 } 571 572 if (childUri != null) { 573 saveStackBlocking(); 574 } 575 576 return childUri; 577 } 578 579 @Override 580 protected void onPostExecute(Uri result) { 581 if (result != null) { 582 onTaskFinished(result); 583 } else { 584 Snackbars.makeSnackbar( 585 DocumentsActivity.this, R.string.save_error, Snackbar.LENGTH_SHORT).show(); 586 } 587 588 setPending(false); 589 } 590 } 591} 592