diff --git a/app/src/main/java/android/widget/Filter.java b/app/src/main/java/android/widget/Filter.java new file mode 100644 index 00000000..07fa3986 --- /dev/null +++ b/app/src/main/java/android/widget/Filter.java @@ -0,0 +1,321 @@ +/* + * 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.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +/** + * Copy of android.widget.Filter, exposing the hidden setDelayer() method. + * + *

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/adapter/PlaceAutocompleteAdapter.kt b/app/src/main/java/net/vonforst/evmap/adapter/PlaceAutocompleteAdapter.kt index 76c5076c..aa4b4256 100644 --- a/app/src/main/java/net/vonforst/evmap/adapter/PlaceAutocompleteAdapter.kt +++ b/app/src/main/java/net/vonforst/evmap/adapter/PlaceAutocompleteAdapter.kt @@ -4,16 +4,14 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.BaseAdapter -import android.widget.Filter -import android.widget.Filterable -import android.widget.ImageView +import android.widget.* import androidx.databinding.DataBindingUtil import net.vonforst.evmap.R import net.vonforst.evmap.autocomplete.* import net.vonforst.evmap.containsAny import net.vonforst.evmap.databinding.ItemAutocompleteResultBinding import net.vonforst.evmap.isDarkMode +import net.vonforst.evmap.storage.PreferenceDataSource class PlaceAutocompleteAdapter(val context: Context) : BaseAdapter(), Filterable { private var resultList: List? = null @@ -84,6 +82,15 @@ class PlaceAutocompleteAdapter(val context: Context) : BaseAdapter(), Filterable override fun getFilter(): Filter { return object : Filter() { + var delaySet = false + + init { + if (PreferenceDataSource(context).searchProvider == "mapbox") { + // set delay to 500 ms to reduce paid Mapbox API requests + this.setDelayer { 500L } + } + } + override fun publishResults(constraint: CharSequence?, results: FilterResults?) { if (results != null && results.count > 0) { notifyDataSetChanged() @@ -102,13 +109,18 @@ class PlaceAutocompleteAdapter(val context: Context) : BaseAdapter(), Filterable break } catch (e: ApiUnavailableException) { e.printStackTrace() - } catch (e: Exception) { - break } } filterResults.values = resultList filterResults.count = resultList!!.size } + + + if (currentProvider is MapboxAutocompleteProvider && !delaySet) { + // set delay to 500 ms to reduce paid Mapbox API requests + this.setDelayer { 500L } + } + return filterResults } }