Home | History | Annotate | Download | only in widget
      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