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