Home | History | Annotate | Download | only in documentsui
      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 
     79     public abstract State getDisplayState();
     80     public abstract void onDocumentPicked(DocumentInfo doc);
     81     public abstract void onDocumentsPicked(List<DocumentInfo> docs);
     82     abstract void onTaskFinished(Uri... uris);
     83     abstract void onDirectoryChanged(int anim);
     84     abstract void updateActionBar();
     85     abstract void saveStackBlocking();
     86 
     87     public BaseActivity(String tag) {
     88         mTag = tag;
     89     }
     90 
     91     @Override
     92     public void onCreate(Bundle icicle) {
     93         super.onCreate(icicle);
     94         mRoots = DocumentsApplication.getRootsCache(this);
     95         mSearchManager = new SearchManager();
     96     }
     97 
     98     @Override
     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
    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
    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 
    159     void onStackRestored(boolean restored, boolean external) {}
    160 
    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 
    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
    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      */
    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 
    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 
    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 
    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
    361         public int describeContents() {
    362             return 0;
    363         }
    364 
    365         @Override
    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 
    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 
    423     void setDisplayFileSize(boolean display) {
    424         LocalPreferences.setDisplayFileSize(this, display);
    425         getDisplayState().showSize = display;
    426         DirectoryFragment.get(getFragmentManager()).onDisplayStateChanged();
    427         invalidateOptionsMenu();
    428     }
    429 
    430     void onStateChanged() {
    431         invalidateOptionsMenu();
    432     }
    433 
    434     /**
    435      * Set state sort order based on explicit user action.
    436      */
    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      */
    445     void setUserMode(int mode) {
    446         getDisplayState().userMode = mode;
    447         DirectoryFragment.get(getFragmentManager()).onUserModeChanged();
    448     }
    449 
    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
    458     protected void onSaveInstanceState(Bundle state) {
    459         super.onSaveInstanceState(state);
    460         state.putParcelable(EXTRA_STATE, getDisplayState());
    461     }
    462 
    463     @Override
    464     protected void onRestoreInstanceState(Bundle state) {
    465         super.onRestoreInstanceState(state);
    466     }
    467 
    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 
    477     public DocumentInfo getCurrentDirectory() {
    478         return getDisplayState().stack.peek();
    479     }
    480 
    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 
    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 
    508         public PickRootTask(RootInfo root) {
    509             mRoot = root;
    510         }
    511 
    512         @Override
    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
    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
    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
    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
    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
    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
    621         public int getCount() {
    622             return getDisplayState().stack.size();
    623         }
    624 
    625         @Override
    626         public DocumentInfo getItem(int position) {
    627             State state = getDisplayState();
    628             return state.stack.get(state.stack.size() - position - 1);
    629         }
    630 
    631         @Override
    632         public long getItemId(int position) {
    633             return position;
    634         }
    635 
    636         @Override
    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
    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 
    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          */
    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 
    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          */
    757         boolean cancelSearch() {
    758             if (mActionBar.hasExpandedActionView()) {
    759                 mActionBar.collapseActionView();
    760                 return true;
    761             }
    762             return false;
    763         }
    764 
    765         boolean isSearching() {
    766             return getDisplayState().currentSearch != null;
    767         }
    768 
    769         boolean isExpanded() {
    770             return mSearchExpanded;
    771         }
    772 
    773         @Override
    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
    787         public boolean onMenuItemActionExpand(MenuItem item) {
    788             mSearchExpanded = true;
    789             updateActionBar();
    790             return true;
    791         }
    792 
    793         @Override
    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
    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
    815         public boolean onQueryTextChange(String newText) {
    816             return false;
    817         }
    818 
    819         @Override
    820         public void onActionViewCollapsed() {
    821             updateActionBar();
    822         }
    823     }
    824 }
    825