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