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