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