1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.documentsui.queries;
18 
19 import static com.android.documentsui.base.Shared.DEBUG;
20 
21 import android.annotation.Nullable;
22 import android.os.Bundle;
23 import android.provider.DocumentsContract.Root;
24 import android.text.TextUtils;
25 import android.util.Log;
26 import android.view.Menu;
27 import android.view.MenuItem;
28 import android.view.MenuItem.OnActionExpandListener;
29 import android.view.View;
30 import android.view.View.OnClickListener;
31 import android.view.View.OnFocusChangeListener;
32 import android.widget.SearchView;
33 import android.widget.SearchView.OnQueryTextListener;
34 
35 import com.android.documentsui.R;
36 import com.android.documentsui.base.DocumentInfo;
37 import com.android.documentsui.base.DocumentStack;
38 import com.android.documentsui.base.RootInfo;
39 import com.android.documentsui.base.Shared;
40 
41 /**
42  * Manages searching UI behavior.
43  */
44 public class SearchViewManager implements
45         SearchView.OnCloseListener, OnQueryTextListener, OnClickListener, OnFocusChangeListener,
46         OnActionExpandListener {
47 
48     private static final String TAG = "SearchManager";
49 
50     private final SearchManagerListener mListener;
51     private final CommandInterceptor mCommandProcessor;
52 
53     private @Nullable String mCurrentSearch;
54     private boolean mSearchExpanded;
55     private boolean mIgnoreNextClose;
56     private boolean mFullBar;
57 
58     private Menu mMenu;
59     private MenuItem mMenuItem;
60     private SearchView mSearchView;
61 
SearchViewManager( SearchManagerListener listener, CommandInterceptor commandProcessor, @Nullable Bundle savedState)62     public SearchViewManager(
63             SearchManagerListener listener,
64             CommandInterceptor commandProcessor,
65             @Nullable Bundle savedState) {
66 
67         assert (listener != null);
68         assert (commandProcessor != null);
69 
70         mListener = listener;
71         mCommandProcessor = commandProcessor;
72         mCurrentSearch = savedState != null ? savedState.getString(Shared.EXTRA_QUERY) : null;
73     }
74 
install(Menu menu, boolean isFullBarSearch)75     public void install(Menu menu, boolean isFullBarSearch) {
76         mMenu = menu;
77         mMenuItem = mMenu.findItem(R.id.menu_search);
78         mSearchView = (SearchView) mMenuItem.getActionView();
79 
80         mSearchView.setOnQueryTextListener(this);
81         mSearchView.setOnCloseListener(this);
82         mSearchView.setOnSearchClickListener(this);
83         mSearchView.setOnQueryTextFocusChangeListener(this);
84 
85         mFullBar = isFullBarSearch;
86         if (mFullBar) {
87             mMenuItem.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
88                     | MenuItem.SHOW_AS_ACTION_ALWAYS);
89             mMenuItem.setOnActionExpandListener(this);
90             mSearchView.setMaxWidth(Integer.MAX_VALUE);
91         }
92 
93         restoreSearch();
94     }
95 
96     /**
97      * Used to hide menu icons, when the search is being restored. Needed because search restoration
98      * is done before onPrepareOptionsMenu(Menu menu) that is overriding the icons visibility.
99      */
updateMenu()100     public void updateMenu() {
101         if (isSearching() && mFullBar) {
102             mMenu.setGroupVisible(R.id.group_hide_when_searching, false);
103         }
104     }
105 
106     /**
107      * @param stack New stack.
108      */
update(DocumentStack stack)109     public void update(DocumentStack stack) {
110         if (mMenuItem == null) {
111             if (DEBUG) Log.d(TAG, "update called before Search MenuItem installed.");
112             return;
113         }
114 
115         if (mCurrentSearch != null) {
116             mMenuItem.expandActionView();
117 
118             mSearchView.setIconified(false);
119             mSearchView.clearFocus();
120             mSearchView.setQuery(mCurrentSearch, false);
121         } else {
122             mSearchView.clearFocus();
123             if (!mSearchView.isIconified()) {
124                 mIgnoreNextClose = true;
125                 mSearchView.setIconified(true);
126             }
127 
128             if (mMenuItem.isActionViewExpanded()) {
129                 mMenuItem.collapseActionView();
130             }
131         }
132 
133         showMenu(stack);
134     }
135 
showMenu(@ullable DocumentStack stack)136     public void showMenu(@Nullable DocumentStack stack) {
137         final DocumentInfo cwd = stack != null ? stack.peek() : null;
138 
139         boolean supportsSearch = true;
140 
141         // Searching in archives is not enabled, as archives are backed by
142         // a different provider than the root provider.
143         if (cwd != null && cwd.isInArchive()) {
144             supportsSearch = false;
145         }
146 
147         final RootInfo root = stack != null ? stack.getRoot() : null;
148         if (root == null || (root.flags & Root.FLAG_SUPPORTS_SEARCH) == 0) {
149             supportsSearch = false;
150         }
151 
152         if (mMenuItem == null) {
153             if (DEBUG) Log.d(TAG, "showMenu called before Search MenuItem installed.");
154             return;
155         }
156 
157         if (!supportsSearch) {
158             mCurrentSearch = null;
159         }
160 
161         mMenuItem.setVisible(supportsSearch);
162     }
163 
164     /**
165      * Cancels current search operation. Triggers clearing and collapsing the SearchView.
166      *
167      * @return True if it cancels search. False if it does not operate search currently.
168      */
cancelSearch()169     public boolean cancelSearch() {
170         if (isExpanded() || isSearching()) {
171             // If the query string is not empty search view won't get iconified
172             mSearchView.setQuery("", false);
173 
174             if (mFullBar) {
175                onClose();
176             } else {
177                 // Causes calling onClose(). onClose() is triggering directory content update.
178                 mSearchView.setIconified(true);
179             }
180             return true;
181         }
182         return false;
183     }
184 
185     /**
186      * Sets search view into the searching state. Used to restore state after device orientation
187      * change.
188      */
restoreSearch()189     private void restoreSearch() {
190         if (isSearching()) {
191             if(mFullBar) {
192                 mMenuItem.expandActionView();
193             } else {
194                 mSearchView.setIconified(false);
195             }
196             onSearchExpanded();
197             mSearchView.setQuery(mCurrentSearch, false);
198             mSearchView.clearFocus();
199         }
200     }
201 
onSearchExpanded()202     private void onSearchExpanded() {
203         mSearchExpanded = true;
204         if(mFullBar) {
205             mMenu.setGroupVisible(R.id.group_hide_when_searching, false);
206         }
207 
208         mListener.onSearchViewChanged(true);
209     }
210 
211     /**
212      * Clears the search. Triggers refreshing of the directory content.
213      * @return True if the default behavior of clearing/dismissing SearchView should be overridden.
214      *         False otherwise.
215      */
216     @Override
onClose()217     public boolean onClose() {
218         mSearchExpanded = false;
219         if (mIgnoreNextClose) {
220             mIgnoreNextClose = false;
221             return false;
222         }
223 
224         // Refresh the directory if a search was done
225         if (mCurrentSearch != null) {
226             mCurrentSearch = null;
227             mListener.onSearchChanged(mCurrentSearch);
228         }
229 
230         if(mFullBar) {
231             mMenuItem.collapseActionView();
232         }
233         mListener.onSearchFinished();
234 
235         mListener.onSearchViewChanged(false);
236 
237         return false;
238     }
239 
240     /**
241      * Called when owning activity is saving state to be used to restore state during creation.
242      * @param state Bundle to save state too
243      */
onSaveInstanceState(Bundle state)244     public void onSaveInstanceState(Bundle state) {
245         state.putString(Shared.EXTRA_QUERY, mCurrentSearch);
246     }
247 
248     /**
249      * Sets mSearchExpanded. Called when search icon is clicked to start search for both search view
250      * modes.
251      */
252     @Override
onClick(View v)253     public void onClick(View v) {
254         onSearchExpanded();
255     }
256 
257     @Override
onQueryTextSubmit(String query)258     public boolean onQueryTextSubmit(String query) {
259 
260         if (mCommandProcessor.accept(query)) {
261             mSearchView.setQuery("", false);
262         } else {
263             mCurrentSearch = query;
264             mSearchView.clearFocus();
265             mListener.onSearchChanged(mCurrentSearch);
266         }
267 
268         return true;
269     }
270 
271     /**
272      * Used to detect and handle back button pressed event when search is expanded.
273      */
274     @Override
onFocusChange(View v, boolean hasFocus)275     public void onFocusChange(View v, boolean hasFocus) {
276         if (!hasFocus) {
277             if (mCurrentSearch == null) {
278                 mSearchView.setIconified(true);
279             } else if (TextUtils.isEmpty(mSearchView.getQuery())) {
280                 cancelSearch();
281             }
282         }
283     }
284 
285     @Override
onQueryTextChange(String newText)286     public boolean onQueryTextChange(String newText) {
287         return false;
288     }
289 
290     @Override
onMenuItemActionCollapse(MenuItem item)291     public boolean onMenuItemActionCollapse(MenuItem item) {
292         mMenu.setGroupVisible(R.id.group_hide_when_searching, true);
293 
294         // Handles case when search view is collapsed by using the arrow on the left of the bar
295         if (isExpanded() || isSearching()) {
296             cancelSearch();
297             return false;
298         }
299         return true;
300     }
301 
302     @Override
onMenuItemActionExpand(MenuItem item)303     public boolean onMenuItemActionExpand(MenuItem item) {
304         return true;
305     }
306 
getCurrentSearch()307     public String getCurrentSearch() {
308         return mCurrentSearch;
309     }
310 
isSearching()311     public boolean isSearching() {
312         return mCurrentSearch != null;
313     }
314 
isExpanded()315     public boolean isExpanded() {
316         return mSearchExpanded;
317     }
318 
319     public interface SearchManagerListener {
onSearchChanged(@ullable String query)320         void onSearchChanged(@Nullable String query);
onSearchFinished()321         void onSearchFinished();
onSearchViewChanged(boolean opened)322         void onSearchViewChanged(boolean opened);
323     }
324 }
325