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.DirectoryFragment.ANIM_DOWN; 20 import static com.android.documentsui.DirectoryFragment.ANIM_NONE; 21 import static com.android.documentsui.DirectoryFragment.ANIM_SIDE; 22 import static com.android.documentsui.DirectoryFragment.ANIM_UP; 23 import static com.android.documentsui.DocumentsActivity.State.ACTION_CREATE; 24 import static com.android.documentsui.DocumentsActivity.State.ACTION_GET_CONTENT; 25 import static com.android.documentsui.DocumentsActivity.State.ACTION_MANAGE; 26 import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN; 27 import static com.android.documentsui.DocumentsActivity.State.ACTION_OPEN_TREE; 28 import static com.android.documentsui.DocumentsActivity.State.MODE_GRID; 29 import static com.android.documentsui.DocumentsActivity.State.MODE_LIST; 30 31 import android.app.Activity; 32 import android.app.Fragment; 33 import android.app.FragmentManager; 34 import android.content.ActivityNotFoundException; 35 import android.content.ClipData; 36 import android.content.ComponentName; 37 import android.content.ContentProviderClient; 38 import android.content.ContentResolver; 39 import android.content.ContentValues; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.pm.ResolveInfo; 43 import android.content.res.Resources; 44 import android.database.Cursor; 45 import android.graphics.Point; 46 import android.net.Uri; 47 import android.os.AsyncTask; 48 import android.os.Bundle; 49 import android.os.Parcel; 50 import android.os.Parcelable; 51 import android.provider.DocumentsContract; 52 import android.provider.DocumentsContract.Root; 53 import android.support.v4.app.ActionBarDrawerToggle; 54 import android.support.v4.widget.DrawerLayout; 55 import android.support.v4.widget.DrawerLayout.DrawerListener; 56 import android.util.Log; 57 import android.util.SparseArray; 58 import android.view.LayoutInflater; 59 import android.view.Menu; 60 import android.view.MenuItem; 61 import android.view.MenuItem.OnActionExpandListener; 62 import android.view.View; 63 import android.view.ViewGroup; 64 import android.view.WindowManager; 65 import android.widget.AdapterView; 66 import android.widget.AdapterView.OnItemSelectedListener; 67 import android.widget.BaseAdapter; 68 import android.widget.ImageView; 69 import android.widget.SearchView; 70 import android.widget.SearchView.OnQueryTextListener; 71 import android.widget.Spinner; 72 import android.widget.TextView; 73 import android.widget.Toast; 74 import android.widget.Toolbar; 75 76 import com.android.documentsui.RecentsProvider.RecentColumns; 77 import com.android.documentsui.RecentsProvider.ResumeColumns; 78 import com.android.documentsui.model.DocumentInfo; 79 import com.android.documentsui.model.DocumentStack; 80 import com.android.documentsui.model.DurableUtils; 81 import com.android.documentsui.model.RootInfo; 82 import com.google.common.collect.Maps; 83 84 import libcore.io.IoUtils; 85 86 import java.io.FileNotFoundException; 87 import java.io.IOException; 88 import java.util.Arrays; 89 import java.util.Collection; 90 import java.util.HashMap; 91 import java.util.List; 92 import java.util.concurrent.Executor; 93 94 public class DocumentsActivity extends Activity { 95 public static final String TAG = "Documents"; 96 97 private static final String EXTRA_STATE = "state"; 98 99 private static final int CODE_FORWARD = 42; 100 101 private boolean mShowAsDialog; 102 103 private SearchView mSearchView; 104 105 private Toolbar mToolbar; 106 private Spinner mToolbarStack; 107 108 private Toolbar mRootsToolbar; 109 110 private DrawerLayout mDrawerLayout; 111 private ActionBarDrawerToggle mDrawerToggle; 112 private View mRootsDrawer; 113 114 private DirectoryContainerView mDirectoryContainer; 115 116 private boolean mIgnoreNextNavigation; 117 private boolean mIgnoreNextClose; 118 private boolean mIgnoreNextCollapse; 119 120 private boolean mSearchExpanded; 121 122 private RootsCache mRoots; 123 private State mState; 124 125 @Override onCreate(Bundle icicle)126 public void onCreate(Bundle icicle) { 127 super.onCreate(icicle); 128 129 mRoots = DocumentsApplication.getRootsCache(this); 130 131 setResult(Activity.RESULT_CANCELED); 132 setContentView(R.layout.activity); 133 134 final Context context = this; 135 final Resources res = getResources(); 136 mShowAsDialog = res.getBoolean(R.bool.show_as_dialog); 137 138 if (mShowAsDialog) { 139 // Strongly define our horizontal dimension; we leave vertical as 140 // WRAP_CONTENT so that system resizes us when IME is showing. 141 final WindowManager.LayoutParams a = getWindow().getAttributes(); 142 143 final Point size = new Point(); 144 getWindowManager().getDefaultDisplay().getSize(size); 145 a.width = (int) res.getFraction(R.dimen.dialog_width, size.x, size.x); 146 147 getWindow().setAttributes(a); 148 149 } else { 150 // Non-dialog means we have a drawer 151 mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); 152 153 mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, 154 R.drawable.ic_hamburger, R.string.drawer_open, R.string.drawer_close); 155 156 mDrawerLayout.setDrawerListener(mDrawerListener); 157 158 mRootsDrawer = findViewById(R.id.drawer_roots); 159 } 160 161 mDirectoryContainer = (DirectoryContainerView) findViewById(R.id.container_directory); 162 163 if (icicle != null) { 164 mState = icicle.getParcelable(EXTRA_STATE); 165 } else { 166 buildDefaultState(); 167 } 168 169 mToolbar = (Toolbar) findViewById(R.id.toolbar); 170 mToolbar.setTitleTextAppearance(context, 171 android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title); 172 173 mToolbarStack = (Spinner) findViewById(R.id.stack); 174 mToolbarStack.setOnItemSelectedListener(mStackListener); 175 176 mRootsToolbar = (Toolbar) findViewById(R.id.roots_toolbar); 177 if (mRootsToolbar != null) { 178 mRootsToolbar.setTitleTextAppearance(context, 179 android.R.style.TextAppearance_DeviceDefault_Widget_ActionBar_Title); 180 } 181 182 setActionBar(mToolbar); 183 184 // Hide roots when we're managing a specific root 185 if (mState.action == ACTION_MANAGE) { 186 if (mShowAsDialog) { 187 findViewById(R.id.container_roots).setVisibility(View.GONE); 188 } else { 189 mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED); 190 } 191 } 192 193 if (mState.action == ACTION_CREATE) { 194 final String mimeType = getIntent().getType(); 195 final String title = getIntent().getStringExtra(Intent.EXTRA_TITLE); 196 SaveFragment.show(getFragmentManager(), mimeType, title); 197 } else if (mState.action == ACTION_OPEN_TREE) { 198 PickFragment.show(getFragmentManager()); 199 } 200 201 if (mState.action == ACTION_GET_CONTENT) { 202 final Intent moreApps = new Intent(getIntent()); 203 moreApps.setComponent(null); 204 moreApps.setPackage(null); 205 RootsFragment.show(getFragmentManager(), moreApps); 206 } else if (mState.action == ACTION_OPEN || mState.action == ACTION_CREATE 207 || mState.action == ACTION_OPEN_TREE) { 208 RootsFragment.show(getFragmentManager(), null); 209 } 210 211 if (!mState.restored) { 212 if (mState.action == ACTION_MANAGE) { 213 final Uri rootUri = getIntent().getData(); 214 new RestoreRootTask(rootUri).executeOnExecutor(getCurrentExecutor()); 215 } else { 216 new RestoreStackTask().execute(); 217 } 218 } else { 219 onCurrentDirectoryChanged(ANIM_NONE); 220 } 221 } 222 buildDefaultState()223 private void buildDefaultState() { 224 mState = new State(); 225 226 final Intent intent = getIntent(); 227 final String action = intent.getAction(); 228 if (Intent.ACTION_OPEN_DOCUMENT.equals(action)) { 229 mState.action = ACTION_OPEN; 230 } else if (Intent.ACTION_CREATE_DOCUMENT.equals(action)) { 231 mState.action = ACTION_CREATE; 232 } else if (Intent.ACTION_GET_CONTENT.equals(action)) { 233 mState.action = ACTION_GET_CONTENT; 234 } else if (Intent.ACTION_OPEN_DOCUMENT_TREE.equals(action)) { 235 mState.action = ACTION_OPEN_TREE; 236 } else if (DocumentsContract.ACTION_MANAGE_ROOT.equals(action)) { 237 mState.action = ACTION_MANAGE; 238 } 239 240 if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 241 mState.allowMultiple = intent.getBooleanExtra( 242 Intent.EXTRA_ALLOW_MULTIPLE, false); 243 } 244 245 if (mState.action == ACTION_MANAGE) { 246 mState.acceptMimes = new String[] { "*/*" }; 247 mState.allowMultiple = true; 248 } else if (intent.hasExtra(Intent.EXTRA_MIME_TYPES)) { 249 mState.acceptMimes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES); 250 } else { 251 mState.acceptMimes = new String[] { intent.getType() }; 252 } 253 254 mState.localOnly = intent.getBooleanExtra(Intent.EXTRA_LOCAL_ONLY, false); 255 mState.forceAdvanced = intent.getBooleanExtra(DocumentsContract.EXTRA_SHOW_ADVANCED, false); 256 mState.showAdvanced = mState.forceAdvanced 257 | LocalPreferences.getDisplayAdvancedDevices(this); 258 259 if (mState.action == ACTION_MANAGE) { 260 mState.showSize = true; 261 } else { 262 mState.showSize = LocalPreferences.getDisplayFileSize(this); 263 } 264 } 265 266 private class RestoreRootTask extends AsyncTask<Void, Void, RootInfo> { 267 private Uri mRootUri; 268 RestoreRootTask(Uri rootUri)269 public RestoreRootTask(Uri rootUri) { 270 mRootUri = rootUri; 271 } 272 273 @Override doInBackground(Void... params)274 protected RootInfo doInBackground(Void... params) { 275 final String rootId = DocumentsContract.getRootId(mRootUri); 276 return mRoots.getRootOneshot(mRootUri.getAuthority(), rootId); 277 } 278 279 @Override onPostExecute(RootInfo root)280 protected void onPostExecute(RootInfo root) { 281 if (isDestroyed()) return; 282 mState.restored = true; 283 284 if (root != null) { 285 onRootPicked(root, true); 286 } else { 287 Log.w(TAG, "Failed to find root: " + mRootUri); 288 finish(); 289 } 290 } 291 } 292 293 private class RestoreStackTask extends AsyncTask<Void, Void, Void> { 294 private volatile boolean mRestoredStack; 295 private volatile boolean mExternal; 296 297 @Override doInBackground(Void... params)298 protected Void doInBackground(Void... params) { 299 // Restore last stack for calling package 300 final String packageName = getCallingPackageMaybeExtra(); 301 final Cursor cursor = getContentResolver() 302 .query(RecentsProvider.buildResume(packageName), null, null, null, null); 303 try { 304 if (cursor.moveToFirst()) { 305 mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0; 306 final byte[] rawStack = cursor.getBlob( 307 cursor.getColumnIndex(ResumeColumns.STACK)); 308 DurableUtils.readFromArray(rawStack, mState.stack); 309 mRestoredStack = true; 310 } 311 } catch (IOException e) { 312 Log.w(TAG, "Failed to resume: " + e); 313 } finally { 314 IoUtils.closeQuietly(cursor); 315 } 316 317 if (mRestoredStack) { 318 // Update the restored stack to ensure we have freshest data 319 final Collection<RootInfo> matchingRoots = mRoots.getMatchingRootsBlocking(mState); 320 try { 321 mState.stack.updateRoot(matchingRoots); 322 mState.stack.updateDocuments(getContentResolver()); 323 } catch (FileNotFoundException e) { 324 Log.w(TAG, "Failed to restore stack: " + e); 325 mState.stack.reset(); 326 mRestoredStack = false; 327 } 328 } 329 330 return null; 331 } 332 333 @Override onPostExecute(Void result)334 protected void onPostExecute(Void result) { 335 if (isDestroyed()) return; 336 mState.restored = true; 337 338 // Show drawer when no stack restored, but only when requesting 339 // non-visual content. However, if we last used an external app, 340 // drawer is always shown. 341 342 boolean showDrawer = false; 343 if (!mRestoredStack) { 344 showDrawer = true; 345 } 346 if (MimePredicate.mimeMatches(MimePredicate.VISUAL_MIMES, mState.acceptMimes)) { 347 showDrawer = false; 348 } 349 if (mExternal && mState.action == ACTION_GET_CONTENT) { 350 showDrawer = true; 351 } 352 353 if (showDrawer) { 354 setRootsDrawerOpen(true); 355 } 356 357 onCurrentDirectoryChanged(ANIM_NONE); 358 } 359 } 360 361 private DrawerListener mDrawerListener = new DrawerListener() { 362 @Override 363 public void onDrawerSlide(View drawerView, float slideOffset) { 364 mDrawerToggle.onDrawerSlide(drawerView, slideOffset); 365 } 366 367 @Override 368 public void onDrawerOpened(View drawerView) { 369 mDrawerToggle.onDrawerOpened(drawerView); 370 } 371 372 @Override 373 public void onDrawerClosed(View drawerView) { 374 mDrawerToggle.onDrawerClosed(drawerView); 375 } 376 377 @Override 378 public void onDrawerStateChanged(int newState) { 379 mDrawerToggle.onDrawerStateChanged(newState); 380 } 381 }; 382 383 @Override onPostCreate(Bundle savedInstanceState)384 protected void onPostCreate(Bundle savedInstanceState) { 385 super.onPostCreate(savedInstanceState); 386 if (mDrawerToggle != null) { 387 mDrawerToggle.syncState(); 388 } 389 } 390 setRootsDrawerOpen(boolean open)391 public void setRootsDrawerOpen(boolean open) { 392 if (!mShowAsDialog) { 393 if (open) { 394 mDrawerLayout.openDrawer(mRootsDrawer); 395 } else { 396 mDrawerLayout.closeDrawer(mRootsDrawer); 397 } 398 } 399 } 400 isRootsDrawerOpen()401 private boolean isRootsDrawerOpen() { 402 if (mShowAsDialog) { 403 return false; 404 } else { 405 return mDrawerLayout.isDrawerOpen(mRootsDrawer); 406 } 407 } 408 updateActionBar()409 public void updateActionBar() { 410 if (mRootsToolbar != null) { 411 if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT 412 || mState.action == ACTION_OPEN_TREE) { 413 mRootsToolbar.setTitle(R.string.title_open); 414 } else if (mState.action == ACTION_CREATE) { 415 mRootsToolbar.setTitle(R.string.title_save); 416 } 417 } 418 419 final RootInfo root = getCurrentRoot(); 420 final boolean showRootIcon = mShowAsDialog || (mState.action == ACTION_MANAGE); 421 if (showRootIcon) { 422 mToolbar.setNavigationIcon( 423 root != null ? root.loadToolbarIcon(mToolbar.getContext()) : null); 424 mToolbar.setNavigationContentDescription(R.string.drawer_open); 425 mToolbar.setNavigationOnClickListener(null); 426 } else { 427 mToolbar.setNavigationIcon(R.drawable.ic_hamburger); 428 mToolbar.setNavigationContentDescription(R.string.drawer_open); 429 mToolbar.setNavigationOnClickListener(new View.OnClickListener() { 430 @Override 431 public void onClick(View v) { 432 setRootsDrawerOpen(true); 433 } 434 }); 435 } 436 437 if (mSearchExpanded) { 438 mToolbar.setTitle(null); 439 mToolbarStack.setVisibility(View.GONE); 440 mToolbarStack.setAdapter(null); 441 } else { 442 if (mState.stack.size() <= 1) { 443 mToolbar.setTitle(root.title); 444 mToolbarStack.setVisibility(View.GONE); 445 mToolbarStack.setAdapter(null); 446 } else { 447 mToolbar.setTitle(null); 448 mToolbarStack.setVisibility(View.VISIBLE); 449 mToolbarStack.setAdapter(mStackAdapter); 450 451 mIgnoreNextNavigation = true; 452 mToolbarStack.setSelection(mStackAdapter.getCount() - 1); 453 } 454 } 455 } 456 457 @Override onCreateOptionsMenu(Menu menu)458 public boolean onCreateOptionsMenu(Menu menu) { 459 super.onCreateOptionsMenu(menu); 460 getMenuInflater().inflate(R.menu.activity, menu); 461 462 // Most actions are visible when showing as dialog 463 if (mShowAsDialog) { 464 for (int i = 0; i < menu.size(); i++) { 465 final MenuItem item = menu.getItem(i); 466 switch (item.getItemId()) { 467 case R.id.menu_advanced: 468 case R.id.menu_file_size: 469 break; 470 default: 471 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 472 } 473 } 474 } 475 476 final MenuItem searchMenu = menu.findItem(R.id.menu_search); 477 mSearchView = (SearchView) searchMenu.getActionView(); 478 mSearchView.setOnQueryTextListener(new OnQueryTextListener() { 479 @Override 480 public boolean onQueryTextSubmit(String query) { 481 mSearchExpanded = true; 482 mState.currentSearch = query; 483 mSearchView.clearFocus(); 484 onCurrentDirectoryChanged(ANIM_NONE); 485 return true; 486 } 487 488 @Override 489 public boolean onQueryTextChange(String newText) { 490 return false; 491 } 492 }); 493 494 searchMenu.setOnActionExpandListener(new OnActionExpandListener() { 495 @Override 496 public boolean onMenuItemActionExpand(MenuItem item) { 497 mSearchExpanded = true; 498 updateActionBar(); 499 return true; 500 } 501 502 @Override 503 public boolean onMenuItemActionCollapse(MenuItem item) { 504 mSearchExpanded = false; 505 if (mIgnoreNextCollapse) { 506 mIgnoreNextCollapse = false; 507 return true; 508 } 509 510 mState.currentSearch = null; 511 onCurrentDirectoryChanged(ANIM_NONE); 512 return true; 513 } 514 }); 515 516 mSearchView.setOnCloseListener(new SearchView.OnCloseListener() { 517 @Override 518 public boolean onClose() { 519 mSearchExpanded = false; 520 if (mIgnoreNextClose) { 521 mIgnoreNextClose = false; 522 return false; 523 } 524 525 mState.currentSearch = null; 526 onCurrentDirectoryChanged(ANIM_NONE); 527 return false; 528 } 529 }); 530 531 return true; 532 } 533 534 @Override onPrepareOptionsMenu(Menu menu)535 public boolean onPrepareOptionsMenu(Menu menu) { 536 super.onPrepareOptionsMenu(menu); 537 538 final FragmentManager fm = getFragmentManager(); 539 540 final RootInfo root = getCurrentRoot(); 541 final DocumentInfo cwd = getCurrentDirectory(); 542 543 final MenuItem createDir = menu.findItem(R.id.menu_create_dir); 544 final MenuItem search = menu.findItem(R.id.menu_search); 545 final MenuItem sort = menu.findItem(R.id.menu_sort); 546 final MenuItem sortSize = menu.findItem(R.id.menu_sort_size); 547 final MenuItem grid = menu.findItem(R.id.menu_grid); 548 final MenuItem list = menu.findItem(R.id.menu_list); 549 final MenuItem advanced = menu.findItem(R.id.menu_advanced); 550 final MenuItem fileSize = menu.findItem(R.id.menu_file_size); 551 552 sort.setVisible(cwd != null); 553 grid.setVisible(mState.derivedMode != MODE_GRID); 554 list.setVisible(mState.derivedMode != MODE_LIST); 555 556 if (mState.currentSearch != null) { 557 // Search uses backend ranking; no sorting 558 sort.setVisible(false); 559 560 search.expandActionView(); 561 562 mSearchView.setIconified(false); 563 mSearchView.clearFocus(); 564 mSearchView.setQuery(mState.currentSearch, false); 565 } else { 566 mIgnoreNextClose = true; 567 mSearchView.setIconified(true); 568 mSearchView.clearFocus(); 569 570 mIgnoreNextCollapse = true; 571 search.collapseActionView(); 572 } 573 574 // Only sort by size when visible 575 sortSize.setVisible(mState.showSize); 576 577 final boolean searchVisible; 578 if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) { 579 createDir.setVisible(cwd != null && cwd.isCreateSupported()); 580 searchVisible = false; 581 582 // No display options in recent directories 583 if (cwd == null) { 584 grid.setVisible(false); 585 list.setVisible(false); 586 } 587 588 if (mState.action == ACTION_CREATE) { 589 SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported()); 590 } 591 } else { 592 createDir.setVisible(false); 593 594 searchVisible = root != null 595 && ((root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0); 596 } 597 598 // TODO: close any search in-progress when hiding 599 search.setVisible(searchVisible); 600 601 advanced.setTitle(LocalPreferences.getDisplayAdvancedDevices(this) 602 ? R.string.menu_advanced_hide : R.string.menu_advanced_show); 603 fileSize.setTitle(LocalPreferences.getDisplayFileSize(this) 604 ? R.string.menu_file_size_hide : R.string.menu_file_size_show); 605 606 advanced.setVisible(mState.action != ACTION_MANAGE); 607 fileSize.setVisible(mState.action != ACTION_MANAGE); 608 609 return true; 610 } 611 612 @Override onOptionsItemSelected(MenuItem item)613 public boolean onOptionsItemSelected(MenuItem item) { 614 if (mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item)) { 615 return true; 616 } 617 618 final int id = item.getItemId(); 619 if (id == android.R.id.home) { 620 onBackPressed(); 621 return true; 622 } else if (id == R.id.menu_create_dir) { 623 CreateDirectoryFragment.show(getFragmentManager()); 624 return true; 625 } else if (id == R.id.menu_search) { 626 return false; 627 } else if (id == R.id.menu_sort_name) { 628 setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME); 629 return true; 630 } else if (id == R.id.menu_sort_date) { 631 setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED); 632 return true; 633 } else if (id == R.id.menu_sort_size) { 634 setUserSortOrder(State.SORT_ORDER_SIZE); 635 return true; 636 } else if (id == R.id.menu_grid) { 637 setUserMode(State.MODE_GRID); 638 return true; 639 } else if (id == R.id.menu_list) { 640 setUserMode(State.MODE_LIST); 641 return true; 642 } else if (id == R.id.menu_advanced) { 643 setDisplayAdvancedDevices(!LocalPreferences.getDisplayAdvancedDevices(this)); 644 return true; 645 } else if (id == R.id.menu_file_size) { 646 setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this)); 647 return true; 648 } else { 649 return super.onOptionsItemSelected(item); 650 } 651 } 652 setDisplayAdvancedDevices(boolean display)653 private void setDisplayAdvancedDevices(boolean display) { 654 LocalPreferences.setDisplayAdvancedDevices(this, display); 655 mState.showAdvanced = mState.forceAdvanced | display; 656 RootsFragment.get(getFragmentManager()).onDisplayStateChanged(); 657 invalidateOptionsMenu(); 658 } 659 setDisplayFileSize(boolean display)660 private void setDisplayFileSize(boolean display) { 661 LocalPreferences.setDisplayFileSize(this, display); 662 mState.showSize = display; 663 DirectoryFragment.get(getFragmentManager()).onDisplayStateChanged(); 664 invalidateOptionsMenu(); 665 } 666 667 /** 668 * Update UI to reflect internal state changes not from user. 669 */ onStateChanged()670 public void onStateChanged() { 671 invalidateOptionsMenu(); 672 } 673 674 /** 675 * Set state sort order based on explicit user action. 676 */ setUserSortOrder(int sortOrder)677 private void setUserSortOrder(int sortOrder) { 678 mState.userSortOrder = sortOrder; 679 DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged(); 680 } 681 682 /** 683 * Set state mode based on explicit user action. 684 */ setUserMode(int mode)685 private void setUserMode(int mode) { 686 mState.userMode = mode; 687 DirectoryFragment.get(getFragmentManager()).onUserModeChanged(); 688 } 689 setPending(boolean pending)690 public void setPending(boolean pending) { 691 final SaveFragment save = SaveFragment.get(getFragmentManager()); 692 if (save != null) { 693 save.setPending(pending); 694 } 695 } 696 697 @Override onBackPressed()698 public void onBackPressed() { 699 if (!mState.stackTouched) { 700 super.onBackPressed(); 701 return; 702 } 703 704 final int size = mState.stack.size(); 705 if (size > 1) { 706 mState.stack.pop(); 707 onCurrentDirectoryChanged(ANIM_UP); 708 } else if (size == 1 && !isRootsDrawerOpen()) { 709 // TODO: open root drawer once we can capture back key 710 super.onBackPressed(); 711 } else { 712 super.onBackPressed(); 713 } 714 } 715 716 @Override onSaveInstanceState(Bundle state)717 protected void onSaveInstanceState(Bundle state) { 718 super.onSaveInstanceState(state); 719 state.putParcelable(EXTRA_STATE, mState); 720 } 721 722 @Override onRestoreInstanceState(Bundle state)723 protected void onRestoreInstanceState(Bundle state) { 724 super.onRestoreInstanceState(state); 725 updateActionBar(); 726 } 727 728 private BaseAdapter mStackAdapter = new BaseAdapter() { 729 @Override 730 public int getCount() { 731 return mState.stack.size(); 732 } 733 734 @Override 735 public DocumentInfo getItem(int position) { 736 return mState.stack.get(mState.stack.size() - position - 1); 737 } 738 739 @Override 740 public long getItemId(int position) { 741 return position; 742 } 743 744 @Override 745 public View getView(int position, View convertView, ViewGroup parent) { 746 if (convertView == null) { 747 convertView = LayoutInflater.from(parent.getContext()) 748 .inflate(R.layout.item_subdir_title, parent, false); 749 } 750 751 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 752 final DocumentInfo doc = getItem(position); 753 754 if (position == 0) { 755 final RootInfo root = getCurrentRoot(); 756 title.setText(root.title); 757 } else { 758 title.setText(doc.displayName); 759 } 760 761 return convertView; 762 } 763 764 @Override 765 public View getDropDownView(int position, View convertView, ViewGroup parent) { 766 if (convertView == null) { 767 convertView = LayoutInflater.from(parent.getContext()) 768 .inflate(R.layout.item_subdir, parent, false); 769 } 770 771 final ImageView subdir = (ImageView) convertView.findViewById(R.id.subdir); 772 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 773 final DocumentInfo doc = getItem(position); 774 775 if (position == 0) { 776 final RootInfo root = getCurrentRoot(); 777 title.setText(root.title); 778 subdir.setVisibility(View.GONE); 779 } else { 780 title.setText(doc.displayName); 781 subdir.setVisibility(View.VISIBLE); 782 } 783 784 return convertView; 785 } 786 }; 787 788 private OnItemSelectedListener mStackListener = new OnItemSelectedListener() { 789 @Override 790 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 791 if (mIgnoreNextNavigation) { 792 mIgnoreNextNavigation = false; 793 return; 794 } 795 796 while (mState.stack.size() > position + 1) { 797 mState.stackTouched = true; 798 mState.stack.pop(); 799 } 800 onCurrentDirectoryChanged(ANIM_UP); 801 } 802 803 @Override 804 public void onNothingSelected(AdapterView<?> parent) { 805 // Ignored 806 } 807 }; 808 getCurrentRoot()809 public RootInfo getCurrentRoot() { 810 if (mState.stack.root != null) { 811 return mState.stack.root; 812 } else { 813 return mRoots.getRecentsRoot(); 814 } 815 } 816 getCurrentDirectory()817 public DocumentInfo getCurrentDirectory() { 818 return mState.stack.peek(); 819 } 820 getCallingPackageMaybeExtra()821 private String getCallingPackageMaybeExtra() { 822 final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME); 823 return (extra != null) ? extra : getCallingPackage(); 824 } 825 getCurrentExecutor()826 public Executor getCurrentExecutor() { 827 final DocumentInfo cwd = getCurrentDirectory(); 828 if (cwd != null && cwd.authority != null) { 829 return ProviderExecutor.forAuthority(cwd.authority); 830 } else { 831 return AsyncTask.THREAD_POOL_EXECUTOR; 832 } 833 } 834 getDisplayState()835 public State getDisplayState() { 836 return mState; 837 } 838 onCurrentDirectoryChanged(int anim)839 private void onCurrentDirectoryChanged(int anim) { 840 final FragmentManager fm = getFragmentManager(); 841 final RootInfo root = getCurrentRoot(); 842 final DocumentInfo cwd = getCurrentDirectory(); 843 844 mDirectoryContainer.setDrawDisappearingFirst(anim == ANIM_DOWN); 845 846 if (cwd == null) { 847 // No directory means recents 848 if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) { 849 RecentsCreateFragment.show(fm); 850 } else { 851 DirectoryFragment.showRecentsOpen(fm, anim); 852 853 // Start recents in grid when requesting visual things 854 final boolean visualMimes = MimePredicate.mimeMatches( 855 MimePredicate.VISUAL_MIMES, mState.acceptMimes); 856 mState.userMode = visualMimes ? MODE_GRID : MODE_LIST; 857 mState.derivedMode = mState.userMode; 858 } 859 } else { 860 if (mState.currentSearch != null) { 861 // Ongoing search 862 DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim); 863 } else { 864 // Normal boring directory 865 DirectoryFragment.showNormal(fm, root, cwd, anim); 866 } 867 } 868 869 // Forget any replacement target 870 if (mState.action == ACTION_CREATE) { 871 final SaveFragment save = SaveFragment.get(fm); 872 if (save != null) { 873 save.setReplaceTarget(null); 874 } 875 } 876 877 if (mState.action == ACTION_OPEN_TREE) { 878 final PickFragment pick = PickFragment.get(fm); 879 if (pick != null) { 880 final CharSequence displayName = (mState.stack.size() <= 1) ? root.title 881 : cwd.displayName; 882 pick.setPickTarget(cwd, displayName); 883 } 884 } 885 886 final RootsFragment roots = RootsFragment.get(fm); 887 if (roots != null) { 888 roots.onCurrentRootChanged(); 889 } 890 891 updateActionBar(); 892 invalidateOptionsMenu(); 893 dumpStack(); 894 } 895 onStackPicked(DocumentStack stack)896 public void onStackPicked(DocumentStack stack) { 897 try { 898 // Update the restored stack to ensure we have freshest data 899 stack.updateDocuments(getContentResolver()); 900 901 mState.stack = stack; 902 mState.stackTouched = true; 903 onCurrentDirectoryChanged(ANIM_SIDE); 904 905 } catch (FileNotFoundException e) { 906 Log.w(TAG, "Failed to restore stack: " + e); 907 } 908 } 909 onRootPicked(RootInfo root, boolean closeDrawer)910 public void onRootPicked(RootInfo root, boolean closeDrawer) { 911 // Clear entire backstack and start in new root 912 mState.stack.root = root; 913 mState.stack.clear(); 914 mState.stackTouched = true; 915 916 if (!mRoots.isRecentsRoot(root)) { 917 new PickRootTask(root).executeOnExecutor(getCurrentExecutor()); 918 } else { 919 onCurrentDirectoryChanged(ANIM_SIDE); 920 } 921 922 if (closeDrawer) { 923 setRootsDrawerOpen(false); 924 } 925 } 926 927 private class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> { 928 private RootInfo mRoot; 929 PickRootTask(RootInfo root)930 public PickRootTask(RootInfo root) { 931 mRoot = root; 932 } 933 934 @Override doInBackground(Void... params)935 protected DocumentInfo doInBackground(Void... params) { 936 try { 937 final Uri uri = DocumentsContract.buildDocumentUri( 938 mRoot.authority, mRoot.documentId); 939 return DocumentInfo.fromUri(getContentResolver(), uri); 940 } catch (FileNotFoundException e) { 941 Log.w(TAG, "Failed to find root", e); 942 return null; 943 } 944 } 945 946 @Override onPostExecute(DocumentInfo result)947 protected void onPostExecute(DocumentInfo result) { 948 if (result != null) { 949 mState.stack.push(result); 950 mState.stackTouched = true; 951 onCurrentDirectoryChanged(ANIM_SIDE); 952 } 953 } 954 } 955 onAppPicked(ResolveInfo info)956 public void onAppPicked(ResolveInfo info) { 957 final Intent intent = new Intent(getIntent()); 958 intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT); 959 intent.setComponent(new ComponentName( 960 info.activityInfo.applicationInfo.packageName, info.activityInfo.name)); 961 startActivityForResult(intent, CODE_FORWARD); 962 } 963 964 @Override onActivityResult(int requestCode, int resultCode, Intent data)965 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 966 Log.d(TAG, "onActivityResult() code=" + resultCode); 967 968 // Only relay back results when not canceled; otherwise stick around to 969 // let the user pick another app/backend. 970 if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) { 971 972 // Remember that we last picked via external app 973 final String packageName = getCallingPackageMaybeExtra(); 974 final ContentValues values = new ContentValues(); 975 values.put(ResumeColumns.EXTERNAL, 1); 976 getContentResolver().insert(RecentsProvider.buildResume(packageName), values); 977 978 // Pass back result to original caller 979 setResult(resultCode, data); 980 finish(); 981 } else { 982 super.onActivityResult(requestCode, resultCode, data); 983 } 984 } 985 onDocumentPicked(DocumentInfo doc)986 public void onDocumentPicked(DocumentInfo doc) { 987 final FragmentManager fm = getFragmentManager(); 988 if (doc.isDirectory()) { 989 mState.stack.push(doc); 990 mState.stackTouched = true; 991 onCurrentDirectoryChanged(ANIM_DOWN); 992 } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 993 // Explicit file picked, return 994 new ExistingFinishTask(doc.derivedUri).executeOnExecutor(getCurrentExecutor()); 995 } else if (mState.action == ACTION_CREATE) { 996 // Replace selected file 997 SaveFragment.get(fm).setReplaceTarget(doc); 998 } else if (mState.action == ACTION_MANAGE) { 999 // First try managing the document; we expect manager to filter 1000 // based on authority, so we don't grant. 1001 final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT); 1002 manage.setData(doc.derivedUri); 1003 1004 try { 1005 startActivity(manage); 1006 } catch (ActivityNotFoundException ex) { 1007 // Fall back to viewing 1008 final Intent view = new Intent(Intent.ACTION_VIEW); 1009 view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 1010 view.setData(doc.derivedUri); 1011 1012 try { 1013 startActivity(view); 1014 } catch (ActivityNotFoundException ex2) { 1015 Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show(); 1016 } 1017 } 1018 } 1019 } 1020 onDocumentsPicked(List<DocumentInfo> docs)1021 public void onDocumentsPicked(List<DocumentInfo> docs) { 1022 if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 1023 final int size = docs.size(); 1024 final Uri[] uris = new Uri[size]; 1025 for (int i = 0; i < size; i++) { 1026 uris[i] = docs.get(i).derivedUri; 1027 } 1028 new ExistingFinishTask(uris).executeOnExecutor(getCurrentExecutor()); 1029 } 1030 } 1031 onSaveRequested(DocumentInfo replaceTarget)1032 public void onSaveRequested(DocumentInfo replaceTarget) { 1033 new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor()); 1034 } 1035 onSaveRequested(String mimeType, String displayName)1036 public void onSaveRequested(String mimeType, String displayName) { 1037 new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor()); 1038 } 1039 onPickRequested(DocumentInfo pickTarget)1040 public void onPickRequested(DocumentInfo pickTarget) { 1041 final Uri viaUri = DocumentsContract.buildTreeDocumentUri(pickTarget.authority, 1042 pickTarget.documentId); 1043 new PickFinishTask(viaUri).executeOnExecutor(getCurrentExecutor()); 1044 } 1045 saveStackBlocking()1046 private void saveStackBlocking() { 1047 final ContentResolver resolver = getContentResolver(); 1048 final ContentValues values = new ContentValues(); 1049 1050 final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack); 1051 if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) { 1052 // Remember stack for last create 1053 values.clear(); 1054 values.put(RecentColumns.KEY, mState.stack.buildKey()); 1055 values.put(RecentColumns.STACK, rawStack); 1056 resolver.insert(RecentsProvider.buildRecent(), values); 1057 } 1058 1059 // Remember location for next app launch 1060 final String packageName = getCallingPackageMaybeExtra(); 1061 values.clear(); 1062 values.put(ResumeColumns.STACK, rawStack); 1063 values.put(ResumeColumns.EXTERNAL, 0); 1064 resolver.insert(RecentsProvider.buildResume(packageName), values); 1065 } 1066 onFinished(Uri... uris)1067 private void onFinished(Uri... uris) { 1068 Log.d(TAG, "onFinished() " + Arrays.toString(uris)); 1069 1070 final Intent intent = new Intent(); 1071 if (uris.length == 1) { 1072 intent.setData(uris[0]); 1073 } else if (uris.length > 1) { 1074 final ClipData clipData = new ClipData( 1075 null, mState.acceptMimes, new ClipData.Item(uris[0])); 1076 for (int i = 1; i < uris.length; i++) { 1077 clipData.addItem(new ClipData.Item(uris[i])); 1078 } 1079 intent.setClipData(clipData); 1080 } 1081 1082 if (mState.action == ACTION_GET_CONTENT) { 1083 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 1084 } else if (mState.action == ACTION_OPEN_TREE) { 1085 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 1086 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 1087 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 1088 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); 1089 } else { 1090 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 1091 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 1092 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 1093 } 1094 1095 setResult(Activity.RESULT_OK, intent); 1096 finish(); 1097 } 1098 1099 private class CreateFinishTask extends AsyncTask<Void, Void, Uri> { 1100 private final String mMimeType; 1101 private final String mDisplayName; 1102 CreateFinishTask(String mimeType, String displayName)1103 public CreateFinishTask(String mimeType, String displayName) { 1104 mMimeType = mimeType; 1105 mDisplayName = displayName; 1106 } 1107 1108 @Override onPreExecute()1109 protected void onPreExecute() { 1110 setPending(true); 1111 } 1112 1113 @Override doInBackground(Void... params)1114 protected Uri doInBackground(Void... params) { 1115 final ContentResolver resolver = getContentResolver(); 1116 final DocumentInfo cwd = getCurrentDirectory(); 1117 1118 ContentProviderClient client = null; 1119 Uri childUri = null; 1120 try { 1121 client = DocumentsApplication.acquireUnstableProviderOrThrow( 1122 resolver, cwd.derivedUri.getAuthority()); 1123 childUri = DocumentsContract.createDocument( 1124 client, cwd.derivedUri, mMimeType, mDisplayName); 1125 } catch (Exception e) { 1126 Log.w(TAG, "Failed to create document", e); 1127 } finally { 1128 ContentProviderClient.releaseQuietly(client); 1129 } 1130 1131 if (childUri != null) { 1132 saveStackBlocking(); 1133 } 1134 1135 return childUri; 1136 } 1137 1138 @Override onPostExecute(Uri result)1139 protected void onPostExecute(Uri result) { 1140 if (result != null) { 1141 onFinished(result); 1142 } else { 1143 Toast.makeText(DocumentsActivity.this, R.string.save_error, Toast.LENGTH_SHORT) 1144 .show(); 1145 } 1146 1147 setPending(false); 1148 } 1149 } 1150 1151 private class ExistingFinishTask extends AsyncTask<Void, Void, Void> { 1152 private final Uri[] mUris; 1153 ExistingFinishTask(Uri... uris)1154 public ExistingFinishTask(Uri... uris) { 1155 mUris = uris; 1156 } 1157 1158 @Override doInBackground(Void... params)1159 protected Void doInBackground(Void... params) { 1160 saveStackBlocking(); 1161 return null; 1162 } 1163 1164 @Override onPostExecute(Void result)1165 protected void onPostExecute(Void result) { 1166 onFinished(mUris); 1167 } 1168 } 1169 1170 private class PickFinishTask extends AsyncTask<Void, Void, Void> { 1171 private final Uri mUri; 1172 PickFinishTask(Uri uri)1173 public PickFinishTask(Uri uri) { 1174 mUri = uri; 1175 } 1176 1177 @Override doInBackground(Void... params)1178 protected Void doInBackground(Void... params) { 1179 saveStackBlocking(); 1180 return null; 1181 } 1182 1183 @Override onPostExecute(Void result)1184 protected void onPostExecute(Void result) { 1185 onFinished(mUri); 1186 } 1187 } 1188 1189 public static class State implements android.os.Parcelable { 1190 public int action; 1191 public String[] acceptMimes; 1192 1193 /** Explicit user choice */ 1194 public int userMode = MODE_UNKNOWN; 1195 /** Derived after loader */ 1196 public int derivedMode = MODE_LIST; 1197 1198 /** Explicit user choice */ 1199 public int userSortOrder = SORT_ORDER_UNKNOWN; 1200 /** Derived after loader */ 1201 public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME; 1202 1203 public boolean allowMultiple = false; 1204 public boolean showSize = false; 1205 public boolean localOnly = false; 1206 public boolean forceAdvanced = false; 1207 public boolean showAdvanced = false; 1208 public boolean stackTouched = false; 1209 public boolean restored = false; 1210 1211 /** Current user navigation stack; empty implies recents. */ 1212 public DocumentStack stack = new DocumentStack(); 1213 /** Currently active search, overriding any stack. */ 1214 public String currentSearch; 1215 1216 /** Instance state for every shown directory */ 1217 public HashMap<String, SparseArray<Parcelable>> dirState = Maps.newHashMap(); 1218 1219 public static final int ACTION_OPEN = 1; 1220 public static final int ACTION_CREATE = 2; 1221 public static final int ACTION_GET_CONTENT = 3; 1222 public static final int ACTION_OPEN_TREE = 4; 1223 public static final int ACTION_MANAGE = 5; 1224 1225 public static final int MODE_UNKNOWN = 0; 1226 public static final int MODE_LIST = 1; 1227 public static final int MODE_GRID = 2; 1228 1229 public static final int SORT_ORDER_UNKNOWN = 0; 1230 public static final int SORT_ORDER_DISPLAY_NAME = 1; 1231 public static final int SORT_ORDER_LAST_MODIFIED = 2; 1232 public static final int SORT_ORDER_SIZE = 3; 1233 1234 @Override describeContents()1235 public int describeContents() { 1236 return 0; 1237 } 1238 1239 @Override writeToParcel(Parcel out, int flags)1240 public void writeToParcel(Parcel out, int flags) { 1241 out.writeInt(action); 1242 out.writeInt(userMode); 1243 out.writeStringArray(acceptMimes); 1244 out.writeInt(userSortOrder); 1245 out.writeInt(allowMultiple ? 1 : 0); 1246 out.writeInt(showSize ? 1 : 0); 1247 out.writeInt(localOnly ? 1 : 0); 1248 out.writeInt(forceAdvanced ? 1 : 0); 1249 out.writeInt(showAdvanced ? 1 : 0); 1250 out.writeInt(stackTouched ? 1 : 0); 1251 out.writeInt(restored ? 1 : 0); 1252 DurableUtils.writeToParcel(out, stack); 1253 out.writeString(currentSearch); 1254 out.writeMap(dirState); 1255 } 1256 1257 public static final Creator<State> CREATOR = new Creator<State>() { 1258 @Override 1259 public State createFromParcel(Parcel in) { 1260 final State state = new State(); 1261 state.action = in.readInt(); 1262 state.userMode = in.readInt(); 1263 state.acceptMimes = in.readStringArray(); 1264 state.userSortOrder = in.readInt(); 1265 state.allowMultiple = in.readInt() != 0; 1266 state.showSize = in.readInt() != 0; 1267 state.localOnly = in.readInt() != 0; 1268 state.forceAdvanced = in.readInt() != 0; 1269 state.showAdvanced = in.readInt() != 0; 1270 state.stackTouched = in.readInt() != 0; 1271 state.restored = in.readInt() != 0; 1272 DurableUtils.readFromParcel(in, state.stack); 1273 state.currentSearch = in.readString(); 1274 in.readMap(state.dirState, null); 1275 return state; 1276 } 1277 1278 @Override 1279 public State[] newArray(int size) { 1280 return new State[size]; 1281 } 1282 }; 1283 } 1284 dumpStack()1285 private void dumpStack() { 1286 Log.d(TAG, "Current stack: "); 1287 Log.d(TAG, " * " + mState.stack.root); 1288 for (DocumentInfo doc : mState.stack) { 1289 Log.d(TAG, " +-- " + doc); 1290 } 1291 } 1292 get(Fragment fragment)1293 public static DocumentsActivity get(Fragment fragment) { 1294 return (DocumentsActivity) fragment.getActivity(); 1295 } 1296 } 1297