1 /* 2 * Copyright (C) 2015 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_NONE; 20 import static com.android.documentsui.DirectoryFragment.ANIM_SIDE; 21 import static com.android.documentsui.DirectoryFragment.ANIM_UP; 22 23 import java.io.FileNotFoundException; 24 import java.io.IOException; 25 import java.util.ArrayList; 26 import java.util.Collection; 27 import java.util.HashMap; 28 import java.util.List; 29 import java.util.concurrent.Executor; 30 31 import libcore.io.IoUtils; 32 import android.app.Activity; 33 import android.app.Fragment; 34 import android.content.Intent; 35 import android.content.pm.ApplicationInfo; 36 import android.content.pm.PackageInfo; 37 import android.content.pm.PackageManager; 38 import android.content.pm.ProviderInfo; 39 import android.database.Cursor; 40 import android.net.Uri; 41 import android.os.AsyncTask; 42 import android.os.Bundle; 43 import android.os.Parcel; 44 import android.os.Parcelable; 45 import android.provider.DocumentsContract; 46 import android.provider.DocumentsContract.Root; 47 import android.util.Log; 48 import android.util.SparseArray; 49 import android.view.LayoutInflater; 50 import android.view.Menu; 51 import android.view.MenuItem; 52 import android.view.MenuItem.OnActionExpandListener; 53 import android.view.View; 54 import android.view.ViewGroup; 55 import android.widget.AdapterView; 56 import android.widget.AdapterView.OnItemSelectedListener; 57 import android.widget.BaseAdapter; 58 import android.widget.ImageView; 59 import android.widget.SearchView; 60 import android.widget.SearchView.OnQueryTextListener; 61 import android.widget.TextView; 62 63 import com.android.documentsui.RecentsProvider.ResumeColumns; 64 import com.android.documentsui.model.DocumentInfo; 65 import com.android.documentsui.model.DocumentStack; 66 import com.android.documentsui.model.DurableUtils; 67 import com.android.documentsui.model.RootInfo; 68 import com.google.common.collect.Maps; 69 70 abstract class BaseActivity extends Activity { 71 72 static final String EXTRA_STATE = "state"; 73 74 RootsCache mRoots; 75 SearchManager mSearchManager; 76 77 private final String mTag; 78 getDisplayState()79 public abstract State getDisplayState(); onDocumentPicked(DocumentInfo doc)80 public abstract void onDocumentPicked(DocumentInfo doc); onDocumentsPicked(List<DocumentInfo> docs)81 public abstract void onDocumentsPicked(List<DocumentInfo> docs); onTaskFinished(Uri... uris)82 abstract void onTaskFinished(Uri... uris); onDirectoryChanged(int anim)83 abstract void onDirectoryChanged(int anim); updateActionBar()84 abstract void updateActionBar(); saveStackBlocking()85 abstract void saveStackBlocking(); 86 BaseActivity(String tag)87 public BaseActivity(String tag) { 88 mTag = tag; 89 } 90 91 @Override onCreate(Bundle icicle)92 public void onCreate(Bundle icicle) { 93 super.onCreate(icicle); 94 mRoots = DocumentsApplication.getRootsCache(this); 95 mSearchManager = new SearchManager(); 96 } 97 98 @Override onResume()99 public void onResume() { 100 super.onResume(); 101 102 final State state = getDisplayState(); 103 final RootInfo root = getCurrentRoot(); 104 105 // If we're browsing a specific root, and that root went away, then we 106 // have no reason to hang around 107 if (state.action == State.ACTION_BROWSE && root != null) { 108 if (mRoots.getRootBlocking(root.authority, root.rootId) == null) { 109 finish(); 110 } 111 } 112 } 113 114 @Override onCreateOptionsMenu(Menu menu)115 public boolean onCreateOptionsMenu(Menu menu) { 116 boolean showMenu = super.onCreateOptionsMenu(menu); 117 118 getMenuInflater().inflate(R.menu.activity, menu); 119 mSearchManager.install((DocumentsToolBar) findViewById(R.id.toolbar)); 120 121 return showMenu; 122 } 123 124 @Override onPrepareOptionsMenu(Menu menu)125 public boolean onPrepareOptionsMenu(Menu menu) { 126 boolean shown = super.onPrepareOptionsMenu(menu); 127 128 final RootInfo root = getCurrentRoot(); 129 final DocumentInfo cwd = getCurrentDirectory(); 130 131 final MenuItem sort = menu.findItem(R.id.menu_sort); 132 final MenuItem sortSize = menu.findItem(R.id.menu_sort_size); 133 final MenuItem grid = menu.findItem(R.id.menu_grid); 134 final MenuItem list = menu.findItem(R.id.menu_list); 135 136 final MenuItem advanced = menu.findItem(R.id.menu_advanced); 137 final MenuItem fileSize = menu.findItem(R.id.menu_file_size); 138 139 mSearchManager.update(root); 140 141 // Search uses backend ranking; no sorting 142 sort.setVisible(cwd != null && !mSearchManager.isSearching()); 143 144 State state = getDisplayState(); 145 grid.setVisible(state.derivedMode != State.MODE_GRID); 146 list.setVisible(state.derivedMode != State.MODE_LIST); 147 148 // Only sort by size when visible 149 sortSize.setVisible(state.showSize); 150 151 advanced.setTitle(LocalPreferences.getDisplayAdvancedDevices(this) 152 ? R.string.menu_advanced_hide : R.string.menu_advanced_show); 153 fileSize.setTitle(LocalPreferences.getDisplayFileSize(this) 154 ? R.string.menu_file_size_hide : R.string.menu_file_size_show); 155 156 return shown; 157 } 158 onStackRestored(boolean restored, boolean external)159 void onStackRestored(boolean restored, boolean external) {} 160 onRootPicked(RootInfo root)161 void onRootPicked(RootInfo root) { 162 State state = getDisplayState(); 163 164 // Clear entire backstack and start in new root 165 state.stack.root = root; 166 state.stack.clear(); 167 state.stackTouched = true; 168 169 mSearchManager.update(root); 170 171 // Recents is always in memory, so we just load it directly. 172 // Otherwise we delegate loading data from disk to a task 173 // to ensure a responsive ui. 174 if (mRoots.isRecentsRoot(root)) { 175 onCurrentDirectoryChanged(ANIM_SIDE); 176 } else { 177 new PickRootTask(root).executeOnExecutor(getCurrentExecutor()); 178 } 179 } 180 expandMenus(Menu menu)181 void expandMenus(Menu menu) { 182 for (int i = 0; i < menu.size(); i++) { 183 final MenuItem item = menu.getItem(i); 184 switch (item.getItemId()) { 185 case R.id.menu_advanced: 186 case R.id.menu_file_size: 187 break; 188 default: 189 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 190 } 191 } 192 } 193 194 @Override onOptionsItemSelected(MenuItem item)195 public boolean onOptionsItemSelected(MenuItem item) { 196 final int id = item.getItemId(); 197 if (id == android.R.id.home) { 198 onBackPressed(); 199 return true; 200 } else if (id == R.id.menu_create_dir) { 201 CreateDirectoryFragment.show(getFragmentManager()); 202 return true; 203 } else if (id == R.id.menu_search) { 204 return false; 205 } else if (id == R.id.menu_sort_name) { 206 setUserSortOrder(State.SORT_ORDER_DISPLAY_NAME); 207 return true; 208 } else if (id == R.id.menu_sort_date) { 209 setUserSortOrder(State.SORT_ORDER_LAST_MODIFIED); 210 return true; 211 } else if (id == R.id.menu_sort_size) { 212 setUserSortOrder(State.SORT_ORDER_SIZE); 213 return true; 214 } else if (id == R.id.menu_grid) { 215 setUserMode(State.MODE_GRID); 216 return true; 217 } else if (id == R.id.menu_list) { 218 setUserMode(State.MODE_LIST); 219 return true; 220 } else if (id == R.id.menu_advanced) { 221 setDisplayAdvancedDevices(!LocalPreferences.getDisplayAdvancedDevices(this)); 222 return true; 223 } else if (id == R.id.menu_file_size) { 224 setDisplayFileSize(!LocalPreferences.getDisplayFileSize(this)); 225 return true; 226 } else if (id == R.id.menu_settings) { 227 final RootInfo root = getCurrentRoot(); 228 final Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS); 229 intent.setDataAndType(DocumentsContract.buildRootUri(root.authority, root.rootId), 230 DocumentsContract.Root.MIME_TYPE_ITEM); 231 startActivity(intent); 232 return true; 233 } 234 235 return super.onOptionsItemSelected(item); 236 } 237 238 /** 239 * Call this when directory changes. Prior to root fragment update 240 * the (abstract) directoryChanged method will be called. 241 * @param anim 242 */ onCurrentDirectoryChanged(int anim)243 final void onCurrentDirectoryChanged(int anim) { 244 onDirectoryChanged(anim); 245 246 final RootsFragment roots = RootsFragment.get(getFragmentManager()); 247 if (roots != null) { 248 roots.onCurrentRootChanged(); 249 } 250 251 updateActionBar(); 252 invalidateOptionsMenu(); 253 } 254 getExcludedAuthorities()255 final List<String> getExcludedAuthorities() { 256 List<String> authorities = new ArrayList<>(); 257 if (getIntent().getBooleanExtra(DocumentsContract.EXTRA_EXCLUDE_SELF, false)) { 258 // Exclude roots provided by the calling package. 259 String packageName = getCallingPackageMaybeExtra(); 260 try { 261 PackageInfo pkgInfo = getPackageManager().getPackageInfo(packageName, 262 PackageManager.GET_PROVIDERS); 263 for (ProviderInfo provider: pkgInfo.providers) { 264 authorities.add(provider.authority); 265 } 266 } catch (PackageManager.NameNotFoundException e) { 267 Log.e(mTag, "Calling package name does not resolve: " + packageName); 268 } 269 } 270 return authorities; 271 } 272 getCallingPackageMaybeExtra()273 final String getCallingPackageMaybeExtra() { 274 String callingPackage = getCallingPackage(); 275 // System apps can set the calling package name using an extra. 276 try { 277 ApplicationInfo info = getPackageManager().getApplicationInfo(callingPackage, 0); 278 if (info.isSystemApp() || info.isUpdatedSystemApp()) { 279 final String extra = getIntent().getStringExtra(DocumentsContract.EXTRA_PACKAGE_NAME); 280 if (extra != null) { 281 callingPackage = extra; 282 } 283 } 284 } finally { 285 return callingPackage; 286 } 287 } 288 get(Fragment fragment)289 public static BaseActivity get(Fragment fragment) { 290 return (BaseActivity) fragment.getActivity(); 291 } 292 293 public static abstract class DocumentsIntent { 294 /** Intent action name to open copy destination. */ 295 public static String ACTION_OPEN_COPY_DESTINATION = 296 "com.android.documentsui.OPEN_COPY_DESTINATION"; 297 298 /** 299 * Extra boolean flag for ACTION_OPEN_COPY_DESTINATION_STRING, which 300 * specifies if the destination directory needs to create new directory or not. 301 */ 302 public static String EXTRA_DIRECTORY_COPY = "com.android.documentsui.DIRECTORY_COPY"; 303 } 304 305 public static class State implements android.os.Parcelable { 306 public int action; 307 public String[] acceptMimes; 308 309 /** Explicit user choice */ 310 public int userMode = MODE_UNKNOWN; 311 /** Derived after loader */ 312 public int derivedMode = MODE_LIST; 313 314 /** Explicit user choice */ 315 public int userSortOrder = SORT_ORDER_UNKNOWN; 316 /** Derived after loader */ 317 public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME; 318 319 public boolean allowMultiple = false; 320 public boolean showSize = false; 321 public boolean localOnly = false; 322 public boolean forceAdvanced = false; 323 public boolean showAdvanced = false; 324 public boolean stackTouched = false; 325 public boolean restored = false; 326 public boolean directoryCopy = false; 327 328 /** Current user navigation stack; empty implies recents. */ 329 public DocumentStack stack = new DocumentStack(); 330 /** Currently active search, overriding any stack. */ 331 public String currentSearch; 332 333 /** Instance state for every shown directory */ 334 public HashMap<String, SparseArray<Parcelable>> dirState = Maps.newHashMap(); 335 336 /** Currently copying file */ 337 public List<DocumentInfo> selectedDocumentsForCopy = new ArrayList<DocumentInfo>(); 338 339 /** Name of the package that started DocsUI */ 340 public List<String> excludedAuthorities = new ArrayList<>(); 341 342 public static final int ACTION_OPEN = 1; 343 public static final int ACTION_CREATE = 2; 344 public static final int ACTION_GET_CONTENT = 3; 345 public static final int ACTION_OPEN_TREE = 4; 346 public static final int ACTION_MANAGE = 5; 347 public static final int ACTION_BROWSE = 6; 348 public static final int ACTION_BROWSE_ALL = 7; 349 public static final int ACTION_OPEN_COPY_DESTINATION = 8; 350 351 public static final int MODE_UNKNOWN = 0; 352 public static final int MODE_LIST = 1; 353 public static final int MODE_GRID = 2; 354 355 public static final int SORT_ORDER_UNKNOWN = 0; 356 public static final int SORT_ORDER_DISPLAY_NAME = 1; 357 public static final int SORT_ORDER_LAST_MODIFIED = 2; 358 public static final int SORT_ORDER_SIZE = 3; 359 360 @Override describeContents()361 public int describeContents() { 362 return 0; 363 } 364 365 @Override writeToParcel(Parcel out, int flags)366 public void writeToParcel(Parcel out, int flags) { 367 out.writeInt(action); 368 out.writeInt(userMode); 369 out.writeStringArray(acceptMimes); 370 out.writeInt(userSortOrder); 371 out.writeInt(allowMultiple ? 1 : 0); 372 out.writeInt(showSize ? 1 : 0); 373 out.writeInt(localOnly ? 1 : 0); 374 out.writeInt(forceAdvanced ? 1 : 0); 375 out.writeInt(showAdvanced ? 1 : 0); 376 out.writeInt(stackTouched ? 1 : 0); 377 out.writeInt(restored ? 1 : 0); 378 DurableUtils.writeToParcel(out, stack); 379 out.writeString(currentSearch); 380 out.writeMap(dirState); 381 out.writeList(selectedDocumentsForCopy); 382 out.writeList(excludedAuthorities); 383 } 384 385 public static final Creator<State> CREATOR = new Creator<State>() { 386 @Override 387 public State createFromParcel(Parcel in) { 388 final State state = new State(); 389 state.action = in.readInt(); 390 state.userMode = in.readInt(); 391 state.acceptMimes = in.readStringArray(); 392 state.userSortOrder = in.readInt(); 393 state.allowMultiple = in.readInt() != 0; 394 state.showSize = in.readInt() != 0; 395 state.localOnly = in.readInt() != 0; 396 state.forceAdvanced = in.readInt() != 0; 397 state.showAdvanced = in.readInt() != 0; 398 state.stackTouched = in.readInt() != 0; 399 state.restored = in.readInt() != 0; 400 DurableUtils.readFromParcel(in, state.stack); 401 state.currentSearch = in.readString(); 402 in.readMap(state.dirState, null); 403 in.readList(state.selectedDocumentsForCopy, null); 404 in.readList(state.excludedAuthorities, null); 405 return state; 406 } 407 408 @Override 409 public State[] newArray(int size) { 410 return new State[size]; 411 } 412 }; 413 } 414 setDisplayAdvancedDevices(boolean display)415 void setDisplayAdvancedDevices(boolean display) { 416 State state = getDisplayState(); 417 LocalPreferences.setDisplayAdvancedDevices(this, display); 418 state.showAdvanced = state.forceAdvanced | display; 419 RootsFragment.get(getFragmentManager()).onDisplayStateChanged(); 420 invalidateOptionsMenu(); 421 } 422 setDisplayFileSize(boolean display)423 void setDisplayFileSize(boolean display) { 424 LocalPreferences.setDisplayFileSize(this, display); 425 getDisplayState().showSize = display; 426 DirectoryFragment.get(getFragmentManager()).onDisplayStateChanged(); 427 invalidateOptionsMenu(); 428 } 429 onStateChanged()430 void onStateChanged() { 431 invalidateOptionsMenu(); 432 } 433 434 /** 435 * Set state sort order based on explicit user action. 436 */ setUserSortOrder(int sortOrder)437 void setUserSortOrder(int sortOrder) { 438 getDisplayState().userSortOrder = sortOrder; 439 DirectoryFragment.get(getFragmentManager()).onUserSortOrderChanged(); 440 } 441 442 /** 443 * Set state mode based on explicit user action. 444 */ setUserMode(int mode)445 void setUserMode(int mode) { 446 getDisplayState().userMode = mode; 447 DirectoryFragment.get(getFragmentManager()).onUserModeChanged(); 448 } 449 setPending(boolean pending)450 void setPending(boolean pending) { 451 final SaveFragment save = SaveFragment.get(getFragmentManager()); 452 if (save != null) { 453 save.setPending(pending); 454 } 455 } 456 457 @Override onSaveInstanceState(Bundle state)458 protected void onSaveInstanceState(Bundle state) { 459 super.onSaveInstanceState(state); 460 state.putParcelable(EXTRA_STATE, getDisplayState()); 461 } 462 463 @Override onRestoreInstanceState(Bundle state)464 protected void onRestoreInstanceState(Bundle state) { 465 super.onRestoreInstanceState(state); 466 } 467 getCurrentRoot()468 RootInfo getCurrentRoot() { 469 State state = getDisplayState(); 470 if (state.stack.root != null) { 471 return state.stack.root; 472 } else { 473 return mRoots.getRecentsRoot(); 474 } 475 } 476 getCurrentDirectory()477 public DocumentInfo getCurrentDirectory() { 478 return getDisplayState().stack.peek(); 479 } 480 getCurrentExecutor()481 public Executor getCurrentExecutor() { 482 final DocumentInfo cwd = getCurrentDirectory(); 483 if (cwd != null && cwd.authority != null) { 484 return ProviderExecutor.forAuthority(cwd.authority); 485 } else { 486 return AsyncTask.THREAD_POOL_EXECUTOR; 487 } 488 } 489 onStackPicked(DocumentStack stack)490 public void onStackPicked(DocumentStack stack) { 491 try { 492 // Update the restored stack to ensure we have freshest data 493 stack.updateDocuments(getContentResolver()); 494 495 State state = getDisplayState(); 496 state.stack = stack; 497 state.stackTouched = true; 498 onCurrentDirectoryChanged(ANIM_SIDE); 499 500 } catch (FileNotFoundException e) { 501 Log.w(mTag, "Failed to restore stack: " + e); 502 } 503 } 504 505 final class PickRootTask extends AsyncTask<Void, Void, DocumentInfo> { 506 private RootInfo mRoot; 507 PickRootTask(RootInfo root)508 public PickRootTask(RootInfo root) { 509 mRoot = root; 510 } 511 512 @Override doInBackground(Void... params)513 protected DocumentInfo doInBackground(Void... params) { 514 try { 515 final Uri uri = DocumentsContract.buildDocumentUri( 516 mRoot.authority, mRoot.documentId); 517 return DocumentInfo.fromUri(getContentResolver(), uri); 518 } catch (FileNotFoundException e) { 519 Log.w(mTag, "Failed to find root", e); 520 return null; 521 } 522 } 523 524 @Override onPostExecute(DocumentInfo result)525 protected void onPostExecute(DocumentInfo result) { 526 if (result != null) { 527 State state = getDisplayState(); 528 state.stack.push(result); 529 state.stackTouched = true; 530 onCurrentDirectoryChanged(ANIM_SIDE); 531 } 532 } 533 } 534 535 final class RestoreStackTask extends AsyncTask<Void, Void, Void> { 536 private volatile boolean mRestoredStack; 537 private volatile boolean mExternal; 538 539 @Override doInBackground(Void... params)540 protected Void doInBackground(Void... params) { 541 State state = getDisplayState(); 542 RootsCache roots = DocumentsApplication.getRootsCache(BaseActivity.this); 543 544 // Restore last stack for calling package 545 final String packageName = getCallingPackageMaybeExtra(); 546 final Cursor cursor = getContentResolver() 547 .query(RecentsProvider.buildResume(packageName), null, null, null, null); 548 try { 549 if (cursor.moveToFirst()) { 550 mExternal = cursor.getInt(cursor.getColumnIndex(ResumeColumns.EXTERNAL)) != 0; 551 final byte[] rawStack = cursor.getBlob( 552 cursor.getColumnIndex(ResumeColumns.STACK)); 553 DurableUtils.readFromArray(rawStack, state.stack); 554 mRestoredStack = true; 555 } 556 } catch (IOException e) { 557 Log.w(mTag, "Failed to resume: " + e); 558 } finally { 559 IoUtils.closeQuietly(cursor); 560 } 561 562 if (mRestoredStack) { 563 // Update the restored stack to ensure we have freshest data 564 final Collection<RootInfo> matchingRoots = roots.getMatchingRootsBlocking(state); 565 try { 566 state.stack.updateRoot(matchingRoots); 567 state.stack.updateDocuments(getContentResolver()); 568 } catch (FileNotFoundException e) { 569 Log.w(mTag, "Failed to restore stack: " + e); 570 state.stack.reset(); 571 mRestoredStack = false; 572 } 573 } 574 575 return null; 576 } 577 578 @Override onPostExecute(Void result)579 protected void onPostExecute(Void result) { 580 if (isDestroyed()) return; 581 getDisplayState().restored = true; 582 onCurrentDirectoryChanged(ANIM_NONE); 583 584 onStackRestored(mRestoredStack, mExternal); 585 586 getDisplayState().restored = true; 587 onCurrentDirectoryChanged(ANIM_NONE); 588 } 589 } 590 591 final class ItemSelectedListener implements OnItemSelectedListener { 592 593 boolean mIgnoreNextNavigation; 594 595 @Override onItemSelected(AdapterView<?> parent, View view, int position, long id)596 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 597 if (mIgnoreNextNavigation) { 598 mIgnoreNextNavigation = false; 599 return; 600 } 601 602 State state = getDisplayState(); 603 while (state.stack.size() > position + 1) { 604 state.stackTouched = true; 605 state.stack.pop(); 606 } 607 onCurrentDirectoryChanged(ANIM_UP); 608 } 609 610 @Override onNothingSelected(AdapterView<?> parent)611 public void onNothingSelected(AdapterView<?> parent) { 612 // Ignored 613 } 614 } 615 616 /** 617 * Class providing toolbar with runtime access to useful activity data. 618 */ 619 final class StackAdapter extends BaseAdapter { 620 @Override getCount()621 public int getCount() { 622 return getDisplayState().stack.size(); 623 } 624 625 @Override getItem(int position)626 public DocumentInfo getItem(int position) { 627 State state = getDisplayState(); 628 return state.stack.get(state.stack.size() - position - 1); 629 } 630 631 @Override getItemId(int position)632 public long getItemId(int position) { 633 return position; 634 } 635 636 @Override getView(int position, View convertView, ViewGroup parent)637 public View getView(int position, View convertView, ViewGroup parent) { 638 if (convertView == null) { 639 convertView = LayoutInflater.from(parent.getContext()) 640 .inflate(R.layout.item_subdir_title, parent, false); 641 } 642 643 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 644 final DocumentInfo doc = getItem(position); 645 646 if (position == 0) { 647 final RootInfo root = getCurrentRoot(); 648 title.setText(root.title); 649 } else { 650 title.setText(doc.displayName); 651 } 652 653 return convertView; 654 } 655 656 @Override getDropDownView(int position, View convertView, ViewGroup parent)657 public View getDropDownView(int position, View convertView, ViewGroup parent) { 658 if (convertView == null) { 659 convertView = LayoutInflater.from(parent.getContext()) 660 .inflate(R.layout.item_subdir, parent, false); 661 } 662 663 final ImageView subdir = (ImageView) convertView.findViewById(R.id.subdir); 664 final TextView title = (TextView) convertView.findViewById(android.R.id.title); 665 final DocumentInfo doc = getItem(position); 666 667 if (position == 0) { 668 final RootInfo root = getCurrentRoot(); 669 title.setText(root.title); 670 subdir.setVisibility(View.GONE); 671 } else { 672 title.setText(doc.displayName); 673 subdir.setVisibility(View.VISIBLE); 674 } 675 676 return convertView; 677 } 678 } 679 680 /** 681 * Facade over the various search parts in the menu. 682 */ 683 final class SearchManager implements 684 SearchView.OnCloseListener, OnActionExpandListener, OnQueryTextListener, 685 DocumentsToolBar.OnActionViewCollapsedListener { 686 687 private boolean mSearchExpanded; 688 private boolean mIgnoreNextClose; 689 private boolean mIgnoreNextCollapse; 690 691 private DocumentsToolBar mActionBar; 692 private MenuItem mMenu; 693 private SearchView mView; 694 install(DocumentsToolBar actionBar)695 public void install(DocumentsToolBar actionBar) { 696 assert(mActionBar == null); 697 mActionBar = actionBar; 698 mMenu = actionBar.getSearchMenu(); 699 mView = (SearchView) mMenu.getActionView(); 700 701 mActionBar.setOnActionViewCollapsedListener(this); 702 mMenu.setOnActionExpandListener(this); 703 mView.setOnQueryTextListener(this); 704 mView.setOnCloseListener(this); 705 } 706 707 /** 708 * @param root Info about the current directory. 709 */ update(RootInfo root)710 void update(RootInfo root) { 711 if (mMenu == null) { 712 Log.d(mTag, "update called before Search MenuItem installed."); 713 return; 714 } 715 716 State state = getDisplayState(); 717 if (state.currentSearch != null) { 718 mMenu.expandActionView(); 719 720 mView.setIconified(false); 721 mView.clearFocus(); 722 mView.setQuery(state.currentSearch, false); 723 } else { 724 mView.clearFocus(); 725 if (!mView.isIconified()) { 726 mIgnoreNextClose = true; 727 mView.setIconified(true); 728 } 729 730 if (mMenu.isActionViewExpanded()) { 731 mIgnoreNextCollapse = true; 732 mMenu.collapseActionView(); 733 } 734 } 735 736 showMenu(root != null 737 && ((root.flags & Root.FLAG_SUPPORTS_SEARCH) != 0)); 738 } 739 showMenu(boolean visible)740 void showMenu(boolean visible) { 741 if (mMenu == null) { 742 Log.d(mTag, "showMenu called before Search MenuItem installed."); 743 return; 744 } 745 746 mMenu.setVisible(visible); 747 if (!visible) { 748 getDisplayState().currentSearch = null; 749 } 750 } 751 752 /** 753 * Cancels current search operation. 754 * @return True if it cancels search. False if it does not operate 755 * search currently. 756 */ cancelSearch()757 boolean cancelSearch() { 758 if (mActionBar.hasExpandedActionView()) { 759 mActionBar.collapseActionView(); 760 return true; 761 } 762 return false; 763 } 764 isSearching()765 boolean isSearching() { 766 return getDisplayState().currentSearch != null; 767 } 768 isExpanded()769 boolean isExpanded() { 770 return mSearchExpanded; 771 } 772 773 @Override onClose()774 public boolean onClose() { 775 mSearchExpanded = false; 776 if (mIgnoreNextClose) { 777 mIgnoreNextClose = false; 778 return false; 779 } 780 781 getDisplayState().currentSearch = null; 782 onCurrentDirectoryChanged(ANIM_NONE); 783 return false; 784 } 785 786 @Override onMenuItemActionExpand(MenuItem item)787 public boolean onMenuItemActionExpand(MenuItem item) { 788 mSearchExpanded = true; 789 updateActionBar(); 790 return true; 791 } 792 793 @Override onMenuItemActionCollapse(MenuItem item)794 public boolean onMenuItemActionCollapse(MenuItem item) { 795 mSearchExpanded = false; 796 if (mIgnoreNextCollapse) { 797 mIgnoreNextCollapse = false; 798 return true; 799 } 800 getDisplayState().currentSearch = null; 801 onCurrentDirectoryChanged(ANIM_NONE); 802 return true; 803 } 804 805 @Override onQueryTextSubmit(String query)806 public boolean onQueryTextSubmit(String query) { 807 mSearchExpanded = true; 808 getDisplayState().currentSearch = query; 809 mView.clearFocus(); 810 onCurrentDirectoryChanged(ANIM_NONE); 811 return true; 812 } 813 814 @Override onQueryTextChange(String newText)815 public boolean onQueryTextChange(String newText) { 816 return false; 817 } 818 819 @Override onActionViewCollapsed()820 public void onActionViewCollapsed() { 821 updateActionBar(); 822 } 823 } 824 } 825