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