/*
|
* Copyright (C) 2007 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package android.widget;
|
|
import android.annotation.UnsupportedAppUsage;
|
import android.os.Handler;
|
import android.os.HandlerThread;
|
import android.os.Looper;
|
import android.os.Message;
|
import android.util.Log;
|
|
/**
|
* <p>A filter constrains data with a filtering pattern.</p>
|
*
|
* <p>Filters are usually created by {@link android.widget.Filterable}
|
* classes.</p>
|
*
|
* <p>Filtering operations performed by calling {@link #filter(CharSequence)} or
|
* {@link #filter(CharSequence, android.widget.Filter.FilterListener)} are
|
* performed asynchronously. When these methods are called, a filtering request
|
* is posted in a request queue and processed later. Any call to one of these
|
* methods will cancel any previous non-executed filtering request.</p>
|
*
|
* @see android.widget.Filterable
|
*/
|
public abstract class Filter {
|
private static final String LOG_TAG = "Filter";
|
|
private static final String THREAD_NAME = "Filter";
|
private static final int FILTER_TOKEN = 0xD0D0F00D;
|
private static final int FINISH_TOKEN = 0xDEADBEEF;
|
|
private Handler mThreadHandler;
|
private Handler mResultHandler;
|
|
private Delayer mDelayer;
|
|
private final Object mLock = new Object();
|
|
/**
|
* <p>Creates a new asynchronous filter.</p>
|
*/
|
public Filter() {
|
mResultHandler = new ResultsHandler();
|
}
|
|
/**
|
* Provide an interface that decides how long to delay the message for a given query. Useful
|
* for heuristics such as posting a delay for the delete key to avoid doing any work while the
|
* user holds down the delete key.
|
*
|
* @param delayer The delayer.
|
* @hide
|
*/
|
@UnsupportedAppUsage
|
public void setDelayer(Delayer delayer) {
|
synchronized (mLock) {
|
mDelayer = delayer;
|
}
|
}
|
|
/**
|
* <p>Starts an asynchronous filtering operation. Calling this method
|
* cancels all previous non-executed filtering requests and posts a new
|
* filtering request that will be executed later.</p>
|
*
|
* @param constraint the constraint used to filter the data
|
*
|
* @see #filter(CharSequence, android.widget.Filter.FilterListener)
|
*/
|
public final void filter(CharSequence constraint) {
|
filter(constraint, null);
|
}
|
|
/**
|
* <p>Starts an asynchronous filtering operation. Calling this method
|
* cancels all previous non-executed filtering requests and posts a new
|
* filtering request that will be executed later.</p>
|
*
|
* <p>Upon completion, the listener is notified.</p>
|
*
|
* @param constraint the constraint used to filter the data
|
* @param listener a listener notified upon completion of the operation
|
*
|
* @see #filter(CharSequence)
|
* @see #performFiltering(CharSequence)
|
* @see #publishResults(CharSequence, android.widget.Filter.FilterResults)
|
*/
|
public final void filter(CharSequence constraint, FilterListener listener) {
|
synchronized (mLock) {
|
if (mThreadHandler == null) {
|
HandlerThread thread = new HandlerThread(
|
THREAD_NAME, android.os.Process.THREAD_PRIORITY_BACKGROUND);
|
thread.start();
|
mThreadHandler = new RequestHandler(thread.getLooper());
|
}
|
|
final long delay = (mDelayer == null) ? 0 : mDelayer.getPostingDelay(constraint);
|
|
Message message = mThreadHandler.obtainMessage(FILTER_TOKEN);
|
|
RequestArguments args = new RequestArguments();
|
// make sure we use an immutable copy of the constraint, so that
|
// it doesn't change while the filter operation is in progress
|
args.constraint = constraint != null ? constraint.toString() : null;
|
args.listener = listener;
|
message.obj = args;
|
|
mThreadHandler.removeMessages(FILTER_TOKEN);
|
mThreadHandler.removeMessages(FINISH_TOKEN);
|
mThreadHandler.sendMessageDelayed(message, delay);
|
}
|
}
|
|
/**
|
* <p>Invoked in a worker thread to filter the data according to the
|
* constraint. Subclasses must implement this method to perform the
|
* filtering operation. Results computed by the filtering operation
|
* must be returned as a {@link android.widget.Filter.FilterResults} that
|
* will then be published in the UI thread through
|
* {@link #publishResults(CharSequence,
|
* android.widget.Filter.FilterResults)}.</p>
|
*
|
* <p><strong>Contract:</strong> When the constraint is null, the original
|
* data must be restored.</p>
|
*
|
* @param constraint the constraint used to filter the data
|
* @return the results of the filtering operation
|
*
|
* @see #filter(CharSequence, android.widget.Filter.FilterListener)
|
* @see #publishResults(CharSequence, android.widget.Filter.FilterResults)
|
* @see android.widget.Filter.FilterResults
|
*/
|
protected abstract FilterResults performFiltering(CharSequence constraint);
|
|
/**
|
* <p>Invoked in the UI thread to publish the filtering results in the
|
* user interface. Subclasses must implement this method to display the
|
* results computed in {@link #performFiltering}.</p>
|
*
|
* @param constraint the constraint used to filter the data
|
* @param results the results of the filtering operation
|
*
|
* @see #filter(CharSequence, android.widget.Filter.FilterListener)
|
* @see #performFiltering(CharSequence)
|
* @see android.widget.Filter.FilterResults
|
*/
|
protected abstract void publishResults(CharSequence constraint,
|
FilterResults results);
|
|
/**
|
* <p>Converts a value from the filtered set into a CharSequence. Subclasses
|
* should override this method to convert their results. The default
|
* implementation returns an empty String for null values or the default
|
* String representation of the value.</p>
|
*
|
* @param resultValue the value to convert to a CharSequence
|
* @return a CharSequence representing the value
|
*/
|
public CharSequence convertResultToString(Object resultValue) {
|
return resultValue == null ? "" : resultValue.toString();
|
}
|
|
/**
|
* <p>Holds the results of a filtering operation. The results are the values
|
* computed by the filtering operation and the number of these values.</p>
|
*/
|
protected static class FilterResults {
|
public FilterResults() {
|
// nothing to see here
|
}
|
|
/**
|
* <p>Contains all the values computed by the filtering operation.</p>
|
*/
|
public Object values;
|
|
/**
|
* <p>Contains the number of values computed by the filtering
|
* operation.</p>
|
*/
|
public int count;
|
}
|
|
/**
|
* <p>Listener used to receive a notification upon completion of a filtering
|
* operation.</p>
|
*/
|
public static interface FilterListener {
|
/**
|
* <p>Notifies the end of a filtering operation.</p>
|
*
|
* @param count the number of values computed by the filter
|
*/
|
public void onFilterComplete(int count);
|
}
|
|
/**
|
* <p>Worker thread handler. When a new filtering request is posted from
|
* {@link android.widget.Filter#filter(CharSequence, android.widget.Filter.FilterListener)},
|
* it is sent to this handler.</p>
|
*/
|
private class RequestHandler extends Handler {
|
public RequestHandler(Looper looper) {
|
super(looper);
|
}
|
|
/**
|
* <p>Handles filtering requests by calling
|
* {@link Filter#performFiltering} and then sending a message
|
* with the results to the results handler.</p>
|
*
|
* @param msg the filtering request
|
*/
|
public void handleMessage(Message msg) {
|
int what = msg.what;
|
Message message;
|
switch (what) {
|
case FILTER_TOKEN:
|
RequestArguments args = (RequestArguments) msg.obj;
|
try {
|
args.results = performFiltering(args.constraint);
|
} catch (Exception e) {
|
args.results = new FilterResults();
|
Log.w(LOG_TAG, "An exception occured during performFiltering()!", e);
|
} finally {
|
message = mResultHandler.obtainMessage(what);
|
message.obj = args;
|
message.sendToTarget();
|
}
|
|
synchronized (mLock) {
|
if (mThreadHandler != null) {
|
Message finishMessage = mThreadHandler.obtainMessage(FINISH_TOKEN);
|
mThreadHandler.sendMessageDelayed(finishMessage, 3000);
|
}
|
}
|
break;
|
case FINISH_TOKEN:
|
synchronized (mLock) {
|
if (mThreadHandler != null) {
|
mThreadHandler.getLooper().quit();
|
mThreadHandler = null;
|
}
|
}
|
break;
|
}
|
}
|
}
|
|
/**
|
* <p>Handles the results of a filtering operation. The results are
|
* handled in the UI thread.</p>
|
*/
|
private class ResultsHandler extends Handler {
|
/**
|
* <p>Messages received from the request handler are processed in the
|
* UI thread. The processing involves calling
|
* {@link Filter#publishResults(CharSequence,
|
* android.widget.Filter.FilterResults)}
|
* to post the results back in the UI and then notifying the listener,
|
* if any.</p>
|
*
|
* @param msg the filtering results
|
*/
|
@Override
|
public void handleMessage(Message msg) {
|
RequestArguments args = (RequestArguments) msg.obj;
|
|
publishResults(args.constraint, args.results);
|
if (args.listener != null) {
|
int count = args.results != null ? args.results.count : -1;
|
args.listener.onFilterComplete(count);
|
}
|
}
|
}
|
|
/**
|
* <p>Holds the arguments of a filtering request as well as the results
|
* of the request.</p>
|
*/
|
private static class RequestArguments {
|
/**
|
* <p>The constraint used to filter the data.</p>
|
*/
|
CharSequence constraint;
|
|
/**
|
* <p>The listener to notify upon completion. Can be null.</p>
|
*/
|
FilterListener listener;
|
|
/**
|
* <p>The results of the filtering operation.</p>
|
*/
|
FilterResults results;
|
}
|
|
/**
|
* @hide
|
*/
|
public interface Delayer {
|
|
/**
|
* @param constraint The constraint passed to {@link Filter#filter(CharSequence)}
|
* @return The delay that should be used for
|
* {@link Handler#sendMessageDelayed(android.os.Message, long)}
|
*/
|
long getPostingDelay(CharSequence constraint);
|
}
|
}
|