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