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 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 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 269 public RestoreRootTask(Uri rootUri) { 270 mRootUri = rootUri; 271 } 272 273 @Override 274 protected RootInfo doInBackground(Void... params) { 275 final String rootId = DocumentsContract.getRootId(mRootUri); 276 return mRoots.getRootOneshot(mRootUri.getAuthority(), rootId); 277 } 278 279 @Override 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 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 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 384 protected void onPostCreate(Bundle savedInstanceState) { 385 super.onPostCreate(savedInstanceState); 386 if (mDrawerToggle != null) { 387 mDrawerToggle.syncState(); 388 } 389 updateActionBar(); 390 } 391 392 public void setRootsDrawerOpen(boolean open) { 393 if (!mShowAsDialog) { 394 if (open) { 395 mDrawerLayout.openDrawer(mRootsDrawer); 396 } else { 397 mDrawerLayout.closeDrawer(mRootsDrawer); 398 } 399 } 400 } 401 402 private boolean isRootsDrawerOpen() { 403 if (mShowAsDialog) { 404 return false; 405 } else { 406 return mDrawerLayout.isDrawerOpen(mRootsDrawer); 407 } 408 } 409 410 public void updateActionBar() { 411 if (mRootsToolbar != null) { 412 if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT 413 || mState.action == ACTION_OPEN_TREE) { 414 mRootsToolbar.setTitle(R.string.title_open); 415 } else if (mState.action == ACTION_CREATE) { 416 mRootsToolbar.setTitle(R.string.title_save); 417 } 418 } 419 420 final RootInfo root = getCurrentRoot(); 421 final boolean showRootIcon = mShowAsDialog || (mState.action == ACTION_MANAGE); 422 if (showRootIcon) { 423 mToolbar.setNavigationIcon( 424 root != null ? root.loadToolbarIcon(mToolbar.getContext()) : null); 425 mToolbar.setNavigationContentDescription(R.string.drawer_open); 426 mToolbar.setNavigationOnClickListener(null); 427 } else { 428 mToolbar.setNavigationIcon(R.drawable.ic_hamburger); 429 mToolbar.setNavigationContentDescription(R.string.drawer_open); 430 mToolbar.setNavigationOnClickListener(new View.OnClickListener() { 431 @Override 432 public void onClick(View v) { 433 setRootsDrawerOpen(true); 434 } 435 }); 436 } 437 438 if (mSearchExpanded) { 439 mToolbar.setTitle(null); 440 mToolbarStack.setVisibility(View.GONE); 441 mToolbarStack.setAdapter(null); 442 } else { 443 if (mState.stack.size() <= 1) { 444 mToolbar.setTitle(root.title); 445 mToolbarStack.setVisibility(View.GONE); 446 mToolbarStack.setAdapter(null); 447 } else { 448 mToolbar.setTitle(null); 449 mToolbarStack.setVisibility(View.VISIBLE); 450 mToolbarStack.setAdapter(mStackAdapter); 451 452 mIgnoreNextNavigation = true; 453 mToolbarStack.setSelection(mStackAdapter.getCount() - 1); 454 } 455 } 456 } 457 458 @Override 459 public boolean onCreateOptionsMenu(Menu menu) { 460 super.onCreateOptionsMenu(menu); 461 getMenuInflater().inflate(R.menu.activity, menu); 462 463 // Most actions are visible when showing as dialog 464 if (mShowAsDialog) { 465 for (int i = 0; i < menu.size(); i++) { 466 final MenuItem item = menu.getItem(i); 467 switch (item.getItemId()) { 468 case R.id.menu_advanced: 469 case R.id.menu_file_size: 470 break; 471 default: 472 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 473 } 474 } 475 } 476 477 final MenuItem searchMenu = menu.findItem(R.id.menu_search); 478 mSearchView = (SearchView) searchMenu.getActionView(); 479 mSearchView.setOnQueryTextListener(new OnQueryTextListener() { 480 @Override 481 public boolean onQueryTextSubmit(String query) { 482 mSearchExpanded = true; 483 mState.currentSearch = query; 484 mSearchView.clearFocus(); 485 onCurrentDirectoryChanged(ANIM_NONE); 486 return true; 487 } 488 489 @Override 490 public boolean onQueryTextChange(String newText) { 491 return false; 492 } 493 }); 494 495 searchMenu.setOnActionExpandListener(new OnActionExpandListener() { 496 @Override 497 public boolean onMenuItemActionExpand(MenuItem item) { 498 mSearchExpanded = true; 499 updateActionBar(); 500 return true; 501 } 502 503 @Override 504 public boolean onMenuItemActionCollapse(MenuItem item) { 505 mSearchExpanded = false; 506 if (mIgnoreNextCollapse) { 507 mIgnoreNextCollapse = false; 508 return true; 509 } 510 511 mState.currentSearch = null; 512 onCurrentDirectoryChanged(ANIM_NONE); 513 return true; 514 } 515 }); 516 517 mSearchView.setOnCloseListener(new SearchView.OnCloseListener() { 518 @Override 519 public boolean onClose() { 520 mSearchExpanded = false; 521 if (mIgnoreNextClose) { 522 mIgnoreNextClose = false; 523 return false; 524 } 525 526 mState.currentSearch = null; 527 onCurrentDirectoryChanged(ANIM_NONE); 528 return false; 529 } 530 }); 531 532 return true; 533 } 534 535 @Override 536 public boolean onPrepareOptionsMenu(Menu menu) { 537 super.onPrepareOptionsMenu(menu); 538 539 final FragmentManager fm = getFragmentManager(); 540 541 final RootInfo root = getCurrentRoot(); 542 final DocumentInfo cwd = getCurrentDirectory(); 543 544 final MenuItem createDir = menu.findItem(R.id.menu_create_dir); 545 final MenuItem search = menu.findItem(R.id.menu_search); 546 final MenuItem sort = menu.findItem(R.id.menu_sort); 547 final MenuItem sortSize = menu.findItem(R.id.menu_sort_size); 548 final MenuItem grid = menu.findItem(R.id.menu_grid); 549 final MenuItem list = menu.findItem(R.id.menu_list); 550 final MenuItem advanced = menu.findItem(R.id.menu_advanced); 551 final MenuItem fileSize = menu.findItem(R.id.menu_file_size); 552 553 sort.setVisible(cwd != null); 554 grid.setVisible(mState.derivedMode != MODE_GRID); 555 list.setVisible(mState.derivedMode != MODE_LIST); 556 557 if (mState.currentSearch != null) { 558 // Search uses backend ranking; no sorting 559 sort.setVisible(false); 560 561 search.expandActionView(); 562 563 mSearchView.setIconified(false); 564 mSearchView.clearFocus(); 565 mSearchView.setQuery(mState.currentSearch, false); 566 } else { 567 mIgnoreNextClose = true; 568 mSearchView.setIconified(true); 569 mSearchView.clearFocus(); 570 571 mIgnoreNextCollapse = true; 572 search.collapseActionView(); 573 } 574 575 // Only sort by size when visible 576 sortSize.setVisible(mState.showSize); 577 578 boolean searchVisible; 579 boolean fileSizeVisible = mState.action != ACTION_MANAGE; 580 if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) { 581 createDir.setVisible(cwd != null && cwd.isCreateSupported()); 582 searchVisible = false; 583 584 // No display options in recent directories 585 if (cwd == null) { 586 grid.setVisible(false); 587 list.setVisible(false); 588 fileSizeVisible = false; 589 } 590 591 if (mState.action == ACTION_CREATE) { 592 SaveFragment.get(fm).setSaveEnabled(cwd != null && cwd.isCreateSupported()); 593 } 594 } else { 595 createDir.setVisible(false); 596 597 searchVisible = root != null 598 && ((root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0); 599 } 600 601 // TODO: close any search in-progress when hiding 602 search.setVisible(searchVisible); 603 604 advanced.setTitle(LocalPreferences.getDisplayAdvancedDevices(this) 605 ? R.string.menu_advanced_hide : R.string.menu_advanced_show); 606 fileSize.setTitle(LocalPreferences.getDisplayFileSize(this) 607 ? R.string.menu_file_size_hide : R.string.menu_file_size_show); 608 609 advanced.setVisible(mState.action != ACTION_MANAGE); 610 fileSize.setVisible(fileSizeVisible); 611 612 return true; 613 } 614 615 @Override 616 public boolean onOptionsItemSelected(MenuItem item) { 617 if (mDrawerToggle != null && mDrawerToggle.onOptionsItemSelected(item)) { 618 return true; 619 } 620 621 final int id = item.getItemId(); 622 if (id == android.R.id.home) { 623 onBackPressed(); 624 return true; 625 } else if (id == R.id.menu_create_dir) { 626 CreateDirectoryFragment.show(getFragmentManager()); 627 return true; 628 } else if (id == R.id.menu_search) { 629 return false; 630 } else if (id == R.id.menu_sort_name) { 631 setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME); 632 return true; 633 } else if (id == R.id.menu_sort_date) { 634 setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED); 635 return true; 636 } else if (id == R.id.menu_sort_size) { 637 setUserSortOrder(State.SORT_ORDER_SIZE); 638 return true; 639 } else if (id == R.id.menu_grid) { 640 setUserMode(State.MODE_GRID); 641 return true; 642 } else if (id == R.id.menu_list) { 643 setUserMode(State.MODE_LIST); 644 return true; 645 } else if (id == R.id.menu_advanced) { 646 setDisplayAdvancedDevices(!LocalPreferences.getDisplayAdvancedDevices(this)); 647 return true; 648 } else if (id == R.id.menu_file_size) { 649 setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this)); 650 return true; 651 } else { 652 return super.onOptionsItemSelected(item); 653 } 654 } 655 656 private void setDisplayAdvancedDevices(boolean display) { 657 LocalPreferences.setDisplayAdvancedDevices(this, display); 658 mState.showAdvanced = mState.forceAdvanced | display; 659 RootsFragment.get(getFragmentManager()).onDisplayStateChanged(); 660 invalidateOptionsMenu(); 661 } 662 663 private void setDisplayFileSize(boolean display) { 664 LocalPreferences.setDisplayFileSize(this, display); 665 mState.showSize = display; 666 DirectoryFragment.get(getFragmentManager()).onDisplayStateChanged(); 667 invalidateOptionsMenu(); 668 } 669 670 /** 671 * Update UI to reflect internal state changes not from user. 672 */ 673 public void onStateChanged() { 674 invalidateOptionsMenu(); 675 } 676 677 /** 678 * Set state sort order based on explicit user action. 679 */ 680 private void setUserSortOrder(int sortOrder) { 681 mState.userSortOrder = sortOrder; 682 DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged(); 683 } 684 685 /** 686 * Set state mode based on explicit user action. 687 */ 688 private void setUserMode(int mode) { 689 mState.userMode = mode; 690 DirectoryFragment.get(getFragmentManager()).onUserModeChanged(); 691 } 692 693 public void setPending(boolean pending) { 694 final SaveFragment save = SaveFragment.get(getFragmentManager()); 695 if (save != null) { 696 save.setPending(pending); 697 } 698 } 699 700 @Override 701 public void onBackPressed() { 702 if (!mState.stackTouched) { 703 super.onBackPressed(); 704 return; 705 } 706 707 final int size = mState.stack.size(); 708 if (size > 1) { 709 mState.stack.pop(); 710 onCurrentDirectoryChanged(ANIM_UP); 711 } else if (size == 1 && !isRootsDrawerOpen()) { 712 // TODO: open root drawer once we can capture back key 713 super.onBackPressed(); 714 } else { 715 super.onBackPressed(); 716 } 717 } 718 719 @Override 720 protected void onSaveInstanceState(Bundle state) { 721 super.onSaveInstanceState(state); 722 state.putParcelable(EXTRA_STATE, mState); 723 } 724 725 @Override 726 protected void onRestoreInstanceState(Bundle state) { 727 super.onRestoreInstanceState(state); 728 } 729 730 private BaseAdapter mStackAdapter = new BaseAdapter() { 731 @Override 732 public int getCount() { 733 return mState.stack.size(); 734 } 735 736 @Override 737 public DocumentInfo getItem(int position) { 738 return mState.stack.get(mState.stack.size() - position - 1); 739 } 740 741 @Override 742 public long getItemId(int position) { 743 return position; 744 } 745 746 @Override 747 public View getView(int position, View convertView, ViewGroup parent) { 748 if (convertView == null) { 749 convertView = LayoutInflater.from(parent.getContext()) 750 .inflate(R.layout.item_subdir_title, parent, false); 751 } 752 753 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 754 final DocumentInfo doc = getItem(position); 755 756 if (position == 0) { 757 final RootInfo root = getCurrentRoot(); 758 title.setText(root.title); 759 } else { 760 title.setText(doc.displayName); 761 } 762 763 return convertView; 764 } 765 766 @Override 767 public View getDropDownView(int position, View convertView, ViewGroup parent) { 768 if (convertView == null) { 769 convertView = LayoutInflater.from(parent.getContext()) 770 .inflate(R.layout.item_subdir, parent, false); 771 } 772 773 final ImageView subdir = (ImageView) convertView.findViewById(R.id.subdir); 774 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 775 final DocumentInfo doc = getItem(position); 776 777 if (position == 0) { 778 final RootInfo root = getCurrentRoot(); 779 title.setText(root.title); 780 subdir.setVisibility(View.GONE); 781 } else { 782 title.setText(doc.displayName); 783 subdir.setVisibility(View.VISIBLE); 784 } 785 786 return convertView; 787 } 788 }; 789 790 private OnItemSelectedListener mStackListener = new OnItemSelectedListener() { 791 @Override 792 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 793 if (mIgnoreNextNavigation) { 794 mIgnoreNextNavigation = false; 795 return; 796 } 797 798 while (mState.stack.size() > position + 1) { 799 mState.stackTouched = true; 800 mState.stack.pop(); 801 } 802 onCurrentDirectoryChanged(ANIM_UP); 803 } 804 805 @Override 806 public void onNothingSelected(AdapterView<?> parent) { 807 // Ignored 808 } 809 }; 810 811 public RootInfo getCurrentRoot() { 812 if (mState.stack.root != null) { 813 return mState.stack.root; 814 } else { 815 return mRoots.getRecentsRoot(); 816 } 817 } 818 819 public DocumentInfo getCurrentDirectory() { 820 return mState.stack.peek(); 821 } 822 823 private String getCallingPackageMaybeExtra() { 824 final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME); 825 return (extra != null) ? extra : getCallingPackage(); 826 } 827 828 public Executor getCurrentExecutor() { 829 final DocumentInfo cwd = getCurrentDirectory(); 830 if (cwd != null && cwd.authority != null) { 831 return ProviderExecutor.forAuthority(cwd.authority); 832 } else { 833 return AsyncTask.THREAD_POOL_EXECUTOR; 834 } 835 } 836 837 public State getDisplayState() { 838 return mState; 839 } 840 841 private void onCurrentDirectoryChanged(int anim) { 842 final FragmentManager fm = getFragmentManager(); 843 final RootInfo root = getCurrentRoot(); 844 final DocumentInfo cwd = getCurrentDirectory(); 845 846 mDirectoryContainer.setDrawDisappearingFirst(anim == ANIM_DOWN); 847 848 if (cwd == null) { 849 // No directory means recents 850 if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) { 851 RecentsCreateFragment.show(fm); 852 } else { 853 DirectoryFragment.showRecentsOpen(fm, anim); 854 855 // Start recents in grid when requesting visual things 856 final boolean visualMimes = MimePredicate.mimeMatches( 857 MimePredicate.VISUAL_MIMES, mState.acceptMimes); 858 mState.userMode = visualMimes ? MODE_GRID : MODE_LIST; 859 mState.derivedMode = mState.userMode; 860 } 861 } else { 862 if (mState.currentSearch != null) { 863 // Ongoing search 864 DirectoryFragment.showSearch(fm, root, mState.currentSearch, anim); 865 } else { 866 // Normal boring directory 867 DirectoryFragment.showNormal(fm, root, cwd, anim); 868 } 869 } 870 871 // Forget any replacement target 872 if (mState.action == ACTION_CREATE) { 873 final SaveFragment save = SaveFragment.get(fm); 874 if (save != null) { 875 save.setReplaceTarget(null); 876 } 877 } 878 879 if (mState.action == ACTION_OPEN_TREE) { 880 final PickFragment pick = PickFragment.get(fm); 881 if (pick != null) { 882 final CharSequence displayName = (mState.stack.size() <= 1) ? root.title 883 : cwd.displayName; 884 pick.setPickTarget(cwd, displayName); 885 } 886 } 887 888 final RootsFragment roots = RootsFragment.get(fm); 889 if (roots != null) { 890 roots.onCurrentRootChanged(); 891 } 892 893 updateActionBar(); 894 invalidateOptionsMenu(); 895 dumpStack(); 896 } 897 898 public void onStackPicked(DocumentStack stack) { 899 try { 900 // Update the restored stack to ensure we have freshest data 901 stack.updateDocuments(getContentResolver()); 902 903 mState.stack = stack; 904 mState.stackTouched = true; 905 onCurrentDirectoryChanged(ANIM_SIDE); 906 907 } catch (FileNotFoundException e) { 908 Log.w(TAG, "Failed to restore stack: " + e); 909 } 910 } 911 912 public void onRootPicked(RootInfo root, boolean closeDrawer) { 913 // Clear entire backstack and start in new root 914 mState.stack.root = root; 915 mState.stack.clear(); 916 mState.stackTouched = true; 917 918 if (!mRoots.isRecentsRoot(root)) { 919 new PickRootTask(root).executeOnExecutor(getCurrentExecutor()); 920 } else { 921 onCurrentDirectoryChanged(ANIM_SIDE); 922 } 923 924 if (closeDrawer) { 925 setRootsDrawerOpen(false); 926 } 927 } 928 929 private class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> { 930 private RootInfo mRoot; 931 932 public PickRootTask(RootInfo root) { 933 mRoot = root; 934 } 935 936 @Override 937 protected DocumentInfo doInBackground(Void... params) { 938 try { 939 final Uri uri = DocumentsContract.buildDocumentUri( 940 mRoot.authority, mRoot.documentId); 941 return DocumentInfo.fromUri(getContentResolver(), uri); 942 } catch (FileNotFoundException e) { 943 Log.w(TAG, "Failed to find root", e); 944 return null; 945 } 946 } 947 948 @Override 949 protected void onPostExecute(DocumentInfo result) { 950 if (result != null) { 951 mState.stack.push(result); 952 mState.stackTouched = true; 953 onCurrentDirectoryChanged(ANIM_SIDE); 954 } 955 } 956 } 957 958 public void onAppPicked(ResolveInfo info) { 959 final Intent intent = new Intent(getIntent()); 960 intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT); 961 intent.setComponent(new ComponentName( 962 info.activityInfo.applicationInfo.packageName, info.activityInfo.name)); 963 startActivityForResult(intent, CODE_FORWARD); 964 } 965 966 @Override 967 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 968 Log.d(TAG, "onActivityResult() code=" + resultCode); 969 970 // Only relay back results when not canceled; otherwise stick around to 971 // let the user pick another app/backend. 972 if (requestCode == CODE_FORWARD && resultCode != RESULT_CANCELED) { 973 974 // Remember that we last picked via external app 975 final String packageName = getCallingPackageMaybeExtra(); 976 final ContentValues values = new ContentValues(); 977 values.put(ResumeColumns.EXTERNAL, 1); 978 getContentResolver().insert(RecentsProvider.buildResume(packageName), values); 979 980 // Pass back result to original caller 981 setResult(resultCode, data); 982 finish(); 983 } else { 984 super.onActivityResult(requestCode, resultCode, data); 985 } 986 } 987 988 public void onDocumentPicked(DocumentInfo doc) { 989 final FragmentManager fm = getFragmentManager(); 990 if (doc.isDirectory()) { 991 mState.stack.push(doc); 992 mState.stackTouched = true; 993 onCurrentDirectoryChanged(ANIM_DOWN); 994 } else if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 995 // Explicit file picked, return 996 new ExistingFinishTask(doc.derivedUri).executeOnExecutor(getCurrentExecutor()); 997 } else if (mState.action == ACTION_CREATE) { 998 // Replace selected file 999 SaveFragment.get(fm).setReplaceTarget(doc); 1000 } else if (mState.action == ACTION_MANAGE) { 1001 // First try managing the document; we expect manager to filter 1002 // based on authority, so we don't grant. 1003 final Intent manage = new Intent(DocumentsContract.ACTION_MANAGE_DOCUMENT); 1004 manage.setData(doc.derivedUri); 1005 1006 try { 1007 startActivity(manage); 1008 } catch (ActivityNotFoundException ex) { 1009 // Fall back to viewing 1010 final Intent view = new Intent(Intent.ACTION_VIEW); 1011 view.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 1012 view.setData(doc.derivedUri); 1013 1014 try { 1015 startActivity(view); 1016 } catch (ActivityNotFoundException ex2) { 1017 Toast.makeText(this, R.string.toast_no_application, Toast.LENGTH_SHORT).show(); 1018 } 1019 } 1020 } 1021 } 1022 1023 public void onDocumentsPicked(List<DocumentInfo> docs) { 1024 if (mState.action == ACTION_OPEN || mState.action == ACTION_GET_CONTENT) { 1025 final int size = docs.size(); 1026 final Uri[] uris = new Uri[size]; 1027 for (int i = 0; i < size; i++) { 1028 uris[i] = docs.get(i).derivedUri; 1029 } 1030 new ExistingFinishTask(uris).executeOnExecutor(getCurrentExecutor()); 1031 } 1032 } 1033 1034 public void onSaveRequested(DocumentInfo replaceTarget) { 1035 new ExistingFinishTask(replaceTarget.derivedUri).executeOnExecutor(getCurrentExecutor()); 1036 } 1037 1038 public void onSaveRequested(String mimeType, String displayName) { 1039 new CreateFinishTask(mimeType, displayName).executeOnExecutor(getCurrentExecutor()); 1040 } 1041 1042 public void onPickRequested(DocumentInfo pickTarget) { 1043 final Uri viaUri = DocumentsContract.buildTreeDocumentUri(pickTarget.authority, 1044 pickTarget.documentId); 1045 new PickFinishTask(viaUri).executeOnExecutor(getCurrentExecutor()); 1046 } 1047 1048 private void saveStackBlocking() { 1049 final ContentResolver resolver = getContentResolver(); 1050 final ContentValues values = new ContentValues(); 1051 1052 final byte[] rawStack = DurableUtils.writeToArrayOrNull(mState.stack); 1053 if (mState.action == ACTION_CREATE || mState.action == ACTION_OPEN_TREE) { 1054 // Remember stack for last create 1055 values.clear(); 1056 values.put(RecentColumns.KEY, mState.stack.buildKey()); 1057 values.put(RecentColumns.STACK, rawStack); 1058 resolver.insert(RecentsProvider.buildRecent(), values); 1059 } 1060 1061 // Remember location for next app launch 1062 final String packageName = getCallingPackageMaybeExtra(); 1063 values.clear(); 1064 values.put(ResumeColumns.STACK, rawStack); 1065 values.put(ResumeColumns.EXTERNAL, 0); 1066 resolver.insert(RecentsProvider.buildResume(packageName), values); 1067 } 1068 1069 private void onFinished(Uri... uris) { 1070 Log.d(TAG, "onFinished() " + Arrays.toString(uris)); 1071 1072 final Intent intent = new Intent(); 1073 if (uris.length == 1) { 1074 intent.setData(uris[0]); 1075 } else if (uris.length > 1) { 1076 final ClipData clipData = new ClipData( 1077 null, mState.acceptMimes, new ClipData.Item(uris[0])); 1078 for (int i = 1; i < uris.length; i++) { 1079 clipData.addItem(new ClipData.Item(uris[i])); 1080 } 1081 intent.setClipData(clipData); 1082 } 1083 1084 if (mState.action == ACTION_GET_CONTENT) { 1085 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 1086 } else if (mState.action == ACTION_OPEN_TREE) { 1087 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 1088 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 1089 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 1090 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION); 1091 } else { 1092 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION 1093 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION 1094 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); 1095 } 1096 1097 setResult(Activity.RESULT_OK, intent); 1098 finish(); 1099 } 1100 1101 private class CreateFinishTask extends AsyncTask<Void, Void, Uri> { 1102 private final String mMimeType; 1103 private final String mDisplayName; 1104 1105 public CreateFinishTask(String mimeType, String displayName) { 1106 mMimeType = mimeType; 1107 mDisplayName = displayName; 1108 } 1109 1110 @Override 1111 protected void onPreExecute() { 1112 setPending(true); 1113 } 1114 1115 @Override 1116 protected Uri doInBackground(Void... params) { 1117 final ContentResolver resolver = getContentResolver(); 1118 final DocumentInfo cwd = getCurrentDirectory(); 1119 1120 ContentProviderClient client = null; 1121 Uri childUri = null; 1122 try { 1123 client = DocumentsApplication.acquireUnstableProviderOrThrow( 1124 resolver, cwd.derivedUri.getAuthority()); 1125 childUri = DocumentsContract.createDocument( 1126 client, cwd.derivedUri, mMimeType, mDisplayName); 1127 } catch (Exception e) { 1128 Log.w(TAG, "Failed to create document", e); 1129 } finally { 1130 ContentProviderClient.releaseQuietly(client); 1131 } 1132 1133 if (childUri != null) { 1134 saveStackBlocking(); 1135 } 1136 1137 return childUri; 1138 } 1139 1140 @Override 1141 protected void onPostExecute(Uri result) { 1142 if (result != null) { 1143 onFinished(result); 1144 } else { 1145 Toast.makeText(DocumentsActivity.this, R.string.save_error, Toast.LENGTH_SHORT) 1146 .show(); 1147 } 1148 1149 setPending(false); 1150 } 1151 } 1152 1153 private class ExistingFinishTask extends AsyncTask<Void, Void, Void> { 1154 private final Uri[] mUris; 1155 1156 public ExistingFinishTask(Uri... uris) { 1157 mUris = uris; 1158 } 1159 1160 @Override 1161 protected Void doInBackground(Void... params) { 1162 saveStackBlocking(); 1163 return null; 1164 } 1165 1166 @Override 1167 protected void onPostExecute(Void result) { 1168 onFinished(mUris); 1169 } 1170 } 1171 1172 private class PickFinishTask extends AsyncTask<Void, Void, Void> { 1173 private final Uri mUri; 1174 1175 public PickFinishTask(Uri uri) { 1176 mUri = uri; 1177 } 1178 1179 @Override 1180 protected Void doInBackground(Void... params) { 1181 saveStackBlocking(); 1182 return null; 1183 } 1184 1185 @Override 1186 protected void onPostExecute(Void result) { 1187 onFinished(mUri); 1188 } 1189 } 1190 1191 public static class State implements android.os.Parcelable { 1192 public int action; 1193 public String[] acceptMimes; 1194 1195 /** Explicit user choice */ 1196 public int userMode = MODE_UNKNOWN; 1197 /** Derived after loader */ 1198 public int derivedMode = MODE_LIST; 1199 1200 /** Explicit user choice */ 1201 public int userSortOrder = SORT_ORDER_UNKNOWN; 1202 /** Derived after loader */ 1203 public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME; 1204 1205 public boolean allowMultiple = false; 1206 public boolean showSize = false; 1207 public boolean localOnly = false; 1208 public boolean forceAdvanced = false; 1209 public boolean showAdvanced = false; 1210 public boolean stackTouched = false; 1211 public boolean restored = false; 1212 1213 /** Current user navigation stack; empty implies recents. */ 1214 public DocumentStack stack = new DocumentStack(); 1215 /** Currently active search, overriding any stack. */ 1216 public String currentSearch; 1217 1218 /** Instance state for every shown directory */ 1219 public HashMap<String, SparseArray<Parcelable>> dirState = Maps.newHashMap(); 1220 1221 public static final int ACTION_OPEN = 1; 1222 public static final int ACTION_CREATE = 2; 1223 public static final int ACTION_GET_CONTENT = 3; 1224 public static final int ACTION_OPEN_TREE = 4; 1225 public static final int ACTION_MANAGE = 5; 1226 1227 public static final int MODE_UNKNOWN = 0; 1228 public static final int MODE_LIST = 1; 1229 public static final int MODE_GRID = 2; 1230 1231 public static final int SORT_ORDER_UNKNOWN = 0; 1232 public static final int SORT_ORDER_DISPLAY_NAME = 1; 1233 public static final int SORT_ORDER_LAST_MODIFIED = 2; 1234 public static final int SORT_ORDER_SIZE = 3; 1235 1236 @Override 1237 public int describeContents() { 1238 return 0; 1239 } 1240 1241 @Override 1242 public void writeToParcel(Parcel out, int flags) { 1243 out.writeInt(action); 1244 out.writeInt(userMode); 1245 out.writeStringArray(acceptMimes); 1246 out.writeInt(userSortOrder); 1247 out.writeInt(allowMultiple ? 1 : 0); 1248 out.writeInt(showSize ? 1 : 0); 1249 out.writeInt(localOnly ? 1 : 0); 1250 out.writeInt(forceAdvanced ? 1 : 0); 1251 out.writeInt(showAdvanced ? 1 : 0); 1252 out.writeInt(stackTouched ? 1 : 0); 1253 out.writeInt(restored ? 1 : 0); 1254 DurableUtils.writeToParcel(out, stack); 1255 out.writeString(currentSearch); 1256 out.writeMap(dirState); 1257 } 1258 1259 public static final Creator<State> CREATOR = new Creator<State>() { 1260 @Override 1261 public State createFromParcel(Parcel in) { 1262 final State state = new State(); 1263 state.action = in.readInt(); 1264 state.userMode = in.readInt(); 1265 state.acceptMimes = in.readStringArray(); 1266 state.userSortOrder = in.readInt(); 1267 state.allowMultiple = in.readInt() != 0; 1268 state.showSize = in.readInt() != 0; 1269 state.localOnly = in.readInt() != 0; 1270 state.forceAdvanced = in.readInt() != 0; 1271 state.showAdvanced = in.readInt() != 0; 1272 state.stackTouched = in.readInt() != 0; 1273 state.restored = in.readInt() != 0; 1274 DurableUtils.readFromParcel(in, state.stack); 1275 state.currentSearch = in.readString(); 1276 in.readMap(state.dirState, null); 1277 return state; 1278 } 1279 1280 @Override 1281 public State[] newArray(int size) { 1282 return new State[size]; 1283 } 1284 }; 1285 } 1286 1287 private void dumpStack() { 1288 Log.d(TAG, "Current stack: "); 1289 Log.d(TAG, " * " + mState.stack.root); 1290 for (DocumentInfo doc : mState.stack) { 1291 Log.d(TAG, " +-- " + doc); 1292 } 1293 } 1294 1295 public static DocumentsActivity get(Fragment fragment) { 1296 return (DocumentsActivity) fragment.getActivity(); 1297 } 1298 } 1299