1 /*
2  * Copyright (C) 2007 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 android.widget;
18 
19 import android.compat.annotation.UnsupportedAppUsage;
20 import android.os.Handler;
21 import android.os.HandlerThread;
22 import android.os.Looper;
23 import android.os.Message;
24 import android.util.Log;
25 
26 /**
27  * <p>A filter constrains data with a filtering pattern.</p>
28  *
29  * <p>Filters are usually created by {@link android.widget.Filterable}
30  * classes.</p>
31  *
32  * <p>Filtering operations performed by calling {@link #filter(CharSequence)} or
33  * {@link #filter(CharSequence, android.widget.Filter.FilterListener)} are
34  * performed asynchronously. When these methods are called, a filtering request
35  * is posted in a request queue and processed later. Any call to one of these
36  * methods will cancel any previous non-executed filtering request.</p>
37  *
38  * @see android.widget.Filterable
39  */
40 public abstract class Filter {
41     private static final String LOG_TAG = "Filter";
42 
43     private static final String THREAD_NAME = "Filter";
44     private static final int FILTER_TOKEN = 0xD0D0F00D;
45     private static final int FINISH_TOKEN = 0xDEADBEEF;
46 
47     private Handler mThreadHandler;
48     private Handler mResultHandler;
49 
50     private Delayer mDelayer;
51 
52     private final Object mLock = new Object();
53 
54     /**
55      * <p>Creates a new asynchronous filter.</p>
56      */
Filter()57     public Filter() {
58         mResultHandler = new ResultsHandler();
59     }
60 
61     /**
62      * Provide an interface that decides how long to delay the message for a given query.  Useful
63      * for heuristics such as posting a delay for the delete key to avoid doing any work while the
64      * user holds down the delete key.
65      *
66      * @param delayer The delayer.
67      * @hide
68      */
69     @UnsupportedAppUsage
setDelayer(Delayer delayer)70     public void setDelayer(Delayer delayer) {
71         synchronized (mLock) {
72             mDelayer = delayer;
73         }
74     }
75 
76     /**
77      * <p>Starts an asynchronous filtering operation. Calling this method
78      * cancels all previous non-executed filtering requests and posts a new
79      * filtering request that will be executed later.</p>
80      *
81      * @param constraint the constraint used to filter the data
82      *
83      * @see #filter(CharSequence, android.widget.Filter.FilterListener)
84      */
filter(CharSequence constraint)85     public final void filter(CharSequence constraint) {
86         filter(constraint, null);
87     }
88 
89     /**
90      * <p>Starts an asynchronous filtering operation. Calling this method
91      * cancels all previous non-executed filtering requests and posts a new
92      * filtering request that will be executed later.</p>
93      *
94      * <p>Upon completion, the listener is notified.</p>
95      *
96      * @param constraint the constraint used to filter the data
97      * @param listener a listener notified upon completion of the operation
98      *
99      * @see #filter(CharSequence)
100      * @see #performFiltering(CharSequence)
101      * @see #publishResults(CharSequence, android.widget.Filter.FilterResults)
102      */
filter(CharSequence constraint, FilterListener listener)103     public final void filter(CharSequence constraint, FilterListener listener) {
104         synchronized (mLock) {
105             if (mThreadHandler == null) {
106                 HandlerThread thread = new HandlerThread(
107                         THREAD_NAME, android.os.Process.THREAD_PRIORITY_BACKGROUND);
108                 thread.start();
109                 mThreadHandler = new RequestHandler(thread.getLooper());
110             }
111 
112             final long delay = (mDelayer == null) ? 0 : mDelayer.getPostingDelay(constraint);
113 
114             Message message = mThreadHandler.obtainMessage(FILTER_TOKEN);
115 
116             RequestArguments args = new RequestArguments();
117             // make sure we use an immutable copy of the constraint, so that
118             // it doesn't change while the filter operation is in progress
119             args.constraint = constraint != null ? constraint.toString() : null;
120             args.listener = listener;
121             message.obj = args;
122 
123             mThreadHandler.removeMessages(FILTER_TOKEN);
124             mThreadHandler.removeMessages(FINISH_TOKEN);
125             mThreadHandler.sendMessageDelayed(message, delay);
126         }
127     }
128 
129     /**
130      * <p>Invoked in a worker thread to filter the data according to the
131      * constraint. Subclasses must implement this method to perform the
132      * filtering operation. Results computed by the filtering operation
133      * must be returned as a {@link android.widget.Filter.FilterResults} that
134      * will then be published in the UI thread through
135      * {@link #publishResults(CharSequence,
136      * android.widget.Filter.FilterResults)}.</p>
137      *
138      * <p><strong>Contract:</strong> When the constraint is null, the original
139      * data must be restored.</p>
140      *
141      * @param constraint the constraint used to filter the data
142      * @return the results of the filtering operation
143      *
144      * @see #filter(CharSequence, android.widget.Filter.FilterListener)
145      * @see #publishResults(CharSequence, android.widget.Filter.FilterResults)
146      * @see android.widget.Filter.FilterResults
147      */
performFiltering(CharSequence constraint)148     protected abstract FilterResults performFiltering(CharSequence constraint);
149 
150     /**
151      * <p>Invoked in the UI thread to publish the filtering results in the
152      * user interface. Subclasses must implement this method to display the
153      * results computed in {@link #performFiltering}.</p>
154      *
155      * @param constraint the constraint used to filter the data
156      * @param results the results of the filtering operation
157      *
158      * @see #filter(CharSequence, android.widget.Filter.FilterListener)
159      * @see #performFiltering(CharSequence)
160      * @see android.widget.Filter.FilterResults
161      */
publishResults(CharSequence constraint, FilterResults results)162     protected abstract void publishResults(CharSequence constraint,
163             FilterResults results);
164 
165     /**
166      * <p>Converts a value from the filtered set into a CharSequence. Subclasses
167      * should override this method to convert their results. The default
168      * implementation returns an empty String for null values or the default
169      * String representation of the value.</p>
170      *
171      * @param resultValue the value to convert to a CharSequence
172      * @return a CharSequence representing the value
173      */
convertResultToString(Object resultValue)174     public CharSequence convertResultToString(Object resultValue) {
175         return resultValue == null ? "" : resultValue.toString();
176     }
177 
178     /**
179      * <p>Holds the results of a filtering operation. The results are the values
180      * computed by the filtering operation and the number of these values.</p>
181      */
182     protected static class FilterResults {
FilterResults()183         public FilterResults() {
184             // nothing to see here
185         }
186 
187         /**
188          * <p>Contains all the values computed by the filtering operation.</p>
189          */
190         public Object values;
191 
192         /**
193          * <p>Contains the number of values computed by the filtering
194          * operation.</p>
195          */
196         public int count;
197     }
198 
199     /**
200      * <p>Listener used to receive a notification upon completion of a filtering
201      * operation.</p>
202      */
203     public static interface FilterListener {
204         /**
205          * <p>Notifies the end of a filtering operation.</p>
206          *
207          * @param count the number of values computed by the filter
208          */
onFilterComplete(int count)209         public void onFilterComplete(int count);
210     }
211 
212     /**
213      * <p>Worker thread handler. When a new filtering request is posted from
214      * {@link android.widget.Filter#filter(CharSequence, android.widget.Filter.FilterListener)},
215      * it is sent to this handler.</p>
216      */
217     private class RequestHandler extends Handler {
RequestHandler(Looper looper)218         public RequestHandler(Looper looper) {
219             super(looper);
220         }
221 
222         /**
223          * <p>Handles filtering requests by calling
224          * {@link Filter#performFiltering} and then sending a message
225          * with the results to the results handler.</p>
226          *
227          * @param msg the filtering request
228          */
handleMessage(Message msg)229         public void handleMessage(Message msg) {
230             int what = msg.what;
231             Message message;
232             switch (what) {
233                 case FILTER_TOKEN:
234                     RequestArguments args = (RequestArguments) msg.obj;
235                     try {
236                         args.results = performFiltering(args.constraint);
237                     } catch (Exception e) {
238                         args.results = new FilterResults();
239                         Log.w(LOG_TAG, "An exception occured during performFiltering()!", e);
240                     } finally {
241                         message = mResultHandler.obtainMessage(what);
242                         message.obj = args;
243                         message.sendToTarget();
244                     }
245 
246                     synchronized (mLock) {
247                         if (mThreadHandler != null) {
248                             Message finishMessage = mThreadHandler.obtainMessage(FINISH_TOKEN);
249                             mThreadHandler.sendMessageDelayed(finishMessage, 3000);
250                         }
251                     }
252                     break;
253                 case FINISH_TOKEN:
254                     synchronized (mLock) {
255                         if (mThreadHandler != null) {
256                             mThreadHandler.getLooper().quit();
257                             mThreadHandler = null;
258                         }
259                     }
260                     break;
261             }
262         }
263     }
264 
265     /**
266      * <p>Handles the results of a filtering operation. The results are
267      * handled in the UI thread.</p>
268      */
269     private class ResultsHandler extends Handler {
270         /**
271          * <p>Messages received from the request handler are processed in the
272          * UI thread. The processing involves calling
273          * {@link Filter#publishResults(CharSequence,
274          * android.widget.Filter.FilterResults)}
275          * to post the results back in the UI and then notifying the listener,
276          * if any.</p>
277          *
278          * @param msg the filtering results
279          */
280         @Override
handleMessage(Message msg)281         public void handleMessage(Message msg) {
282             RequestArguments args = (RequestArguments) msg.obj;
283 
284             publishResults(args.constraint, args.results);
285             if (args.listener != null) {
286                 int count = args.results != null ? args.results.count : -1;
287                 args.listener.onFilterComplete(count);
288             }
289         }
290     }
291 
292     /**
293      * <p>Holds the arguments of a filtering request as well as the results
294      * of the request.</p>
295      */
296     private static class RequestArguments {
297         /**
298          * <p>The constraint used to filter the data.</p>
299          */
300         CharSequence constraint;
301 
302         /**
303          * <p>The listener to notify upon completion. Can be null.</p>
304          */
305         FilterListener listener;
306 
307         /**
308          * <p>The results of the filtering operation.</p>
309          */
310         FilterResults results;
311     }
312 
313     /**
314      * @hide
315      */
316     public interface Delayer {
317 
318         /**
319          * @param constraint The constraint passed to {@link Filter#filter(CharSequence)}
320          * @return The delay that should be used for
321          *         {@link Handler#sendMessageDelayed(android.os.Message, long)}
322          */
getPostingDelay(CharSequence constraint)323         long getPostingDelay(CharSequence constraint);
324     }
325 }
326