• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.SharedMinimal.DEBUG;
20 
21 import android.annotation.Nullable;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.os.Looper;
25 import android.provider.DocumentsContract.Root;
26 import android.text.TextUtils;
27 import android.util.Log;
28 import android.view.Menu;
29 import android.view.MenuItem;
30 import android.view.MenuItem.OnActionExpandListener;
31 import android.view.View;
32 import android.view.View.OnClickListener;
33 import android.view.View.OnFocusChangeListener;
34 import android.widget.SearchView;
35 import android.widget.SearchView.OnQueryTextListener;
36 
37 import com.android.documentsui.R;
38 import com.android.documentsui.base.DocumentInfo;
39 import com.android.documentsui.base.DocumentStack;
40 import com.android.documentsui.base.EventHandler;
41 import com.android.documentsui.base.RootInfo;
42 import com.android.documentsui.base.Shared;
43 import com.android.internal.annotations.GuardedBy;
44 import com.android.internal.annotations.VisibleForTesting;
45 
46 import java.util.Timer;
47 import java.util.TimerTask;
48 
49 /**
50  * Manages searching UI behavior.
51  */
52 public class SearchViewManager implements
53         SearchView.OnCloseListener, OnQueryTextListener, OnClickListener, OnFocusChangeListener,
54         OnActionExpandListener {
55 
56     private static final String TAG = "SearchManager";
57 
58     // How long we wait after the user finishes typing before kicking off a search.
59     public static final int SEARCH_DELAY_MS = 750;
60 
61     private final SearchManagerListener mListener;
62     private final EventHandler<String> mCommandProcessor;
63     private final Timer mTimer;
64     private final Handler mUiHandler;
65 
66     private final Object mSearchLock;
67     @GuardedBy("mSearchLock")
68     private @Nullable Runnable mQueuedSearchRunnable;
69     @GuardedBy("mSearchLock")
70     private @Nullable TimerTask mQueuedSearchTask;
71     private @Nullable String mCurrentSearch;
72     private boolean mSearchExpanded;
73     private boolean mIgnoreNextClose;
74     private boolean mFullBar;
75 
76     private Menu mMenu;
77     private MenuItem mMenuItem;
78     private SearchView mSearchView;
79 
SearchViewManager( SearchManagerListener listener, EventHandler<String> commandProcessor, @Nullable Bundle savedState)80     public SearchViewManager(
81             SearchManagerListener listener,
82             EventHandler<String> commandProcessor,
83             @Nullable Bundle savedState) {
84         this(listener, commandProcessor, savedState, new Timer(),
85                 new Handler(Looper.getMainLooper()));
86     }
87 
88     @VisibleForTesting
SearchViewManager( SearchManagerListener listener, EventHandler<String> commandProcessor, @Nullable Bundle savedState, Timer timer, Handler handler)89     protected SearchViewManager(
90             SearchManagerListener listener,
91             EventHandler<String> commandProcessor,
92             @Nullable Bundle savedState,
93             Timer timer,
94             Handler handler) {
95         assert (listener != null);
96         assert (commandProcessor != null);
97 
98         mSearchLock = new Object();
99         mListener = listener;
100         mCommandProcessor = commandProcessor;
101         mTimer = timer;
102         mUiHandler = handler;
103         mCurrentSearch = savedState != null ? savedState.getString(Shared.EXTRA_QUERY) : null;
104     }
105 
install(Menu menu, boolean isFullBarSearch)106     public void install(Menu menu, boolean isFullBarSearch) {
107         mMenu = menu;
108         mMenuItem = mMenu.findItem(R.id.option_menu_search);
109         mSearchView = (SearchView) mMenuItem.getActionView();
110 
111         mSearchView.setOnQueryTextListener(this);
112         mSearchView.setOnCloseListener(this);
113         mSearchView.setOnSearchClickListener(this);
114         mSearchView.setOnQueryTextFocusChangeListener(this);
115 
116         mFullBar = isFullBarSearch;
117         if (mFullBar) {
118             mMenuItem.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
119                     | MenuItem.SHOW_AS_ACTION_ALWAYS);
120             mMenuItem.setOnActionExpandListener(this);
121             mSearchView.setMaxWidth(Integer.MAX_VALUE);
122         }
123 
124         restoreSearch();
125     }
126 
127     /**
128      * Used to hide menu icons, when the search is being restored. Needed because search restoration
129      * is done before onPrepareOptionsMenu(Menu menu) that is overriding the icons visibility.
130      */
updateMenu()131     public void updateMenu() {
132         if (isSearching() && mFullBar) {
133             mMenu.setGroupVisible(R.id.group_hide_when_searching, false);
134         }
135     }
136 
137     /**
138      * @param stack New stack.
139      */
update(DocumentStack stack)140     public void update(DocumentStack stack) {
141         if (mMenuItem == null) {
142             if (DEBUG) Log.d(TAG, "update called before Search MenuItem installed.");
143             return;
144         }
145 
146         if (mCurrentSearch != null) {
147             mMenuItem.expandActionView();
148 
149             mSearchView.setIconified(false);
150             mSearchView.clearFocus();
151             mSearchView.setQuery(mCurrentSearch, false);
152         } else {
153             mSearchView.clearFocus();
154             if (!mSearchView.isIconified()) {
155                 mIgnoreNextClose = true;
156                 mSearchView.setIconified(true);
157             }
158 
159             if (mMenuItem.isActionViewExpanded()) {
160                 mMenuItem.collapseActionView();
161             }
162         }
163 
164         showMenu(stack);
165     }
166 
showMenu(@ullable DocumentStack stack)167     public void showMenu(@Nullable DocumentStack stack) {
168         final DocumentInfo cwd = stack != null ? stack.peek() : null;
169 
170         boolean supportsSearch = true;
171 
172         // Searching in archives is not enabled, as archives are backed by
173         // a different provider than the root provider.
174         if (cwd != null && cwd.isInArchive()) {
175             supportsSearch = false;
176         }
177 
178         final RootInfo root = stack != null ? stack.getRoot() : null;
179         if (root == null || (root.flags & Root.FLAG_SUPPORTS_SEARCH) == 0) {
180             supportsSearch = false;
181         }
182 
183         if (mMenuItem == null) {
184             if (DEBUG) Log.d(TAG, "showMenu called before Search MenuItem installed.");
185             return;
186         }
187 
188         if (!supportsSearch) {
189             mCurrentSearch = null;
190         }
191 
192         mMenuItem.setVisible(supportsSearch);
193     }
194 
195     /**
196      * Cancels current search operation. Triggers clearing and collapsing the SearchView.
197      *
198      * @return True if it cancels search. False if it does not operate search currently.
199      */
cancelSearch()200     public boolean cancelSearch() {
201         if (isExpanded() || isSearching()) {
202             cancelQueuedSearch();
203             // If the query string is not empty search view won't get iconified
204             mSearchView.setQuery("", false);
205 
206             if (mFullBar) {
207                onClose();
208             } else {
209                 // Causes calling onClose(). onClose() is triggering directory content update.
210                 mSearchView.setIconified(true);
211             }
212             return true;
213         }
214         return false;
215     }
216 
cancelQueuedSearch()217     private void cancelQueuedSearch() {
218         synchronized (mSearchLock) {
219             if (mQueuedSearchTask != null) {
220                 mQueuedSearchTask.cancel();
221             }
222             mQueuedSearchTask = null;
223             mUiHandler.removeCallbacks(mQueuedSearchRunnable);
224             mQueuedSearchRunnable = null;
225         }
226     }
227 
228     /**
229      * Sets search view into the searching state. Used to restore state after device orientation
230      * change.
231      */
restoreSearch()232     private void restoreSearch() {
233         if (isSearching()) {
234             if (mFullBar) {
235                 mMenuItem.expandActionView();
236             } else {
237                 mSearchView.setIconified(false);
238             }
239             onSearchExpanded();
240             mSearchView.setQuery(mCurrentSearch, false);
241             mSearchView.clearFocus();
242         }
243     }
244 
onSearchExpanded()245     private void onSearchExpanded() {
246         mSearchExpanded = true;
247         if (mFullBar) {
248             mMenu.setGroupVisible(R.id.group_hide_when_searching, false);
249         }
250 
251         mListener.onSearchViewChanged(true);
252     }
253 
254     /**
255      * Clears the search. Triggers refreshing of the directory content.
256      * @return True if the default behavior of clearing/dismissing SearchView should be overridden.
257      *         False otherwise.
258      */
259     @Override
onClose()260     public boolean onClose() {
261         mSearchExpanded = false;
262         if (mIgnoreNextClose) {
263             mIgnoreNextClose = false;
264             return false;
265         }
266 
267         // Refresh the directory if a search was done
268         if (mCurrentSearch != null) {
269             mCurrentSearch = null;
270             mListener.onSearchChanged(mCurrentSearch);
271         }
272 
273         if (mFullBar) {
274             mMenuItem.collapseActionView();
275         }
276         mListener.onSearchFinished();
277 
278         mListener.onSearchViewChanged(false);
279 
280         return false;
281     }
282 
283     /**
284      * Called when owning activity is saving state to be used to restore state during creation.
285      * @param state Bundle to save state too
286      */
onSaveInstanceState(Bundle state)287     public void onSaveInstanceState(Bundle state) {
288         state.putString(Shared.EXTRA_QUERY, mCurrentSearch);
289     }
290 
291     /**
292      * Sets mSearchExpanded. Called when search icon is clicked to start search for both search view
293      * modes.
294      */
295     @Override
onClick(View v)296     public void onClick(View v) {
297         onSearchExpanded();
298     }
299 
300     @Override
onQueryTextSubmit(String query)301     public boolean onQueryTextSubmit(String query) {
302 
303         if (mCommandProcessor.accept(query)) {
304             mSearchView.setQuery("", false);
305         } else {
306             cancelQueuedSearch();
307             // Don't kick off a search if we've already finished it.
308             if (mCurrentSearch != query) {
309                 mCurrentSearch = query;
310                 mListener.onSearchChanged(mCurrentSearch);
311             }
312             mSearchView.clearFocus();
313         }
314 
315         return true;
316     }
317 
318     /**
319      * Used to detect and handle back button pressed event when search is expanded.
320      */
321     @Override
onFocusChange(View v, boolean hasFocus)322     public void onFocusChange(View v, boolean hasFocus) {
323         if (!hasFocus) {
324             if (mCurrentSearch == null) {
325                 mSearchView.setIconified(true);
326             } else if (TextUtils.isEmpty(mSearchView.getQuery())) {
327                 cancelSearch();
328             }
329         }
330     }
331 
332     @VisibleForTesting
createSearchTask(String newText)333     protected TimerTask createSearchTask(String newText) {
334         return new TimerTask() {
335             @Override
336             public void run() {
337                 // Do the actual work on the main looper.
338                 synchronized (mSearchLock) {
339                     mQueuedSearchRunnable = () -> {
340                         mCurrentSearch = newText;
341                         if (mCurrentSearch != null && mCurrentSearch.isEmpty()) {
342                             mCurrentSearch = null;
343                         }
344                         mListener.onSearchChanged(mCurrentSearch);
345                     };
346                     mUiHandler.post(mQueuedSearchRunnable);
347                 }
348             }
349         };
350     }
351 
352     @Override
353     public boolean onQueryTextChange(String newText) {
354         cancelQueuedSearch();
355         synchronized (mSearchLock) {
356             mQueuedSearchTask = createSearchTask(newText);
357 
358             mTimer.schedule(mQueuedSearchTask, SEARCH_DELAY_MS);
359         }
360         return true;
361     }
362 
363     @Override
364     public boolean onMenuItemActionCollapse(MenuItem item) {
365         mMenu.setGroupVisible(R.id.group_hide_when_searching, true);
366 
367         // Handles case when search view is collapsed by using the arrow on the left of the bar
368         if (isExpanded() || isSearching()) {
369             cancelSearch();
370             return false;
371         }
372         return true;
373     }
374 
375     @Override
376     public boolean onMenuItemActionExpand(MenuItem item) {
377         return true;
378     }
379 
380     public String getCurrentSearch() {
381         return mCurrentSearch;
382     }
383 
384     public boolean isSearching() {
385         return mCurrentSearch != null;
386     }
387 
388     public boolean isExpanded() {
389         return mSearchExpanded;
390     }
391 
392     public interface SearchManagerListener {
393         void onSearchChanged(@Nullable String query);
394         void onSearchFinished();
395         void onSearchViewChanged(boolean opened);
396     }
397 }
398