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