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