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 17 package android.widget; 18 19 import android.os.Handler; 20 import android.os.HandlerThread; 21 import android.os.Looper; 22 import android.os.Message; 23 import 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 */ 39 public 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