A filter constrains data with a filtering pattern.
+ * + *Filters are usually created by {@link android.widget.Filterable} + * classes.
+ * + *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.
+ * + * @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(); + + /** + *Creates a new asynchronous filter.
+ */ + 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 + */ + public void setDelayer(Delayer delayer) { + synchronized (mLock) { + mDelayer = delayer; + } + } + + /** + *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.
+ * + * @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); + } + + /** + *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.
+ * + *Upon completion, the listener is notified.
+ * + * @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); + } + } + + /** + *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)}.
+ * + *Contract: When the constraint is null, the original + * data must be restored.
+ * + * @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); + + /** + *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}.
+ * + * @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); + + /** + *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.
+ * + * @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(); + } + + /** + *Holds the results of a filtering operation. The results are the values + * computed by the filtering operation and the number of these values.
+ */ + protected static class FilterResults { + public FilterResults() { + // nothing to see here + } + + /** + *Contains all the values computed by the filtering operation.
+ */ + public Object values; + + /** + *Contains the number of values computed by the filtering + * operation.
+ */ + public int count; + } + + /** + *Listener used to receive a notification upon completion of a filtering + * operation.
+ */ + public static interface FilterListener { + /** + *Notifies the end of a filtering operation.
+ * + * @param count the number of values computed by the filter + */ + public void onFilterComplete(int count); + } + + /** + *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.
+ */ + private class RequestHandler extends Handler { + public RequestHandler(Looper looper) { + super(looper); + } + + /** + *Handles filtering requests by calling + * {@link Filter#performFiltering} and then sending a message + * with the results to the results handler.
+ * + * @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; + } + } + } + + /** + *Handles the results of a filtering operation. The results are + * handled in the UI thread.
+ */ + private class ResultsHandler extends Handler { + /** + *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.
+ * + * @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); + } + } + } + + /** + *Holds the arguments of a filtering request as well as the results + * of the request.
+ */ + private static class RequestArguments { + /** + *The constraint used to filter the data.
+ */ + CharSequence constraint; + + /** + *The listener to notify upon completion. Can be null.
+ */ + FilterListener listener; + + /** + *The results of the filtering operation.
+ */ + 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); + } +} diff --git a/app/src/main/java/net/vonforst/evmap/Utils.kt b/app/src/main/java/net/vonforst/evmap/Utils.kt index 397e0d0e..db236f78 100644 --- a/app/src/main/java/net/vonforst/evmap/Utils.kt +++ b/app/src/main/java/net/vonforst/evmap/Utils.kt @@ -1,5 +1,7 @@ package net.vonforst.evmap +import android.content.Context +import android.content.res.Configuration import android.graphics.Typeface import android.os.Bundle import android.text.* @@ -9,6 +11,7 @@ import androidx.lifecycle.Observer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext +import java.util.* fun Bundle.optDouble(name: String): Double? { if (!this.containsKey(name)) return null @@ -80,6 +83,8 @@ fun max(a: Int?, b: Int?): Int? { } } +fun