package org.mtransit.android.provider; import java.util.ArrayList; import java.util.HashSet; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.mtransit.android.commons.MTLog; import org.mtransit.android.commons.RuntimeUtils; import org.mtransit.android.commons.SqlUtils; import org.mtransit.android.commons.provider.ContentProviderConstants; import org.mtransit.android.commons.provider.MTSearchRecentSuggestionsProvider; import org.mtransit.android.commons.task.MTCallable; import org.mtransit.android.data.AgencyProperties; import org.mtransit.android.data.DataSourceManager; import org.mtransit.android.data.DataSourceProvider; import android.annotation.SuppressLint; import android.app.SearchManager; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; import android.provider.BaseColumns; import android.text.TextUtils; @SuppressLint("Registered") public class SearchSuggestProvider extends MTSearchRecentSuggestionsProvider { private static final String TAG = SearchSuggestProvider.class.getSimpleName(); @Override public String getLogTag() { return TAG; } public final static String AUTHORITY = "org.mtransit.android.provider.search.suggest"; public final static int MODE = DATABASE_MODE_QUERIES; private static final UriMatcher URI_MATCHER = getNewUriMatcher(AUTHORITY); public static UriMatcher getNewUriMatcher(String authority) { UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); URI_MATCHER.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY, ContentProviderConstants.SEARCH_SUGGEST_EMPTY); URI_MATCHER.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", ContentProviderConstants.SEARCH_SUGGEST_QUERY); return URI_MATCHER; } private static final String[] SEARCH_SUGGEST_COLUMNS = new String[] { BaseColumns._ID, SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_QUERY, }; private static final String SEARCH_SUGGEST_ICON = "android.resource://system/" + android.R.drawable.ic_menu_search; private static final String RECENT_SEARCH_SUGGEST_ICON = "android.resource://system/" + android.R.drawable.ic_menu_recent_history; public SearchSuggestProvider() { setupSuggestions(AUTHORITY, MODE); } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Cursor cursor = null; String query; try { switch (URI_MATCHER.match(uri)) { case ContentProviderConstants.SEARCH_SUGGEST_EMPTY: query = selectionArgs == null || selectionArgs.length == 0 ? null : selectionArgs[0]; cursor = super.query(uri, projection, selection, selectionArgs, sortOrder); return getSearchSuggest(query, cursor); case ContentProviderConstants.SEARCH_SUGGEST_QUERY: query = uri.getLastPathSegment(); cursor = super.query(uri, projection, selection, selectionArgs, sortOrder); return getSearchSuggest(query, cursor); default: throw new IllegalArgumentException(String.format("Unknown URI (query): '%s'", uri)); } } catch (Exception e) { MTLog.w(this, e, "Error while resolving query %s!", uri); return null; } finally { SqlUtils.close(cursor); } } private Cursor getSearchSuggest(String query, Cursor recentSearchCursor) { clearAllTasks(); HashSet<String> recentSearchSuggestions = DataSourceManager.getSearchSuggest(recentSearchCursor); HashSet<String> suggestions = new HashSet<String>(); if (!TextUtils.isEmpty(query)) { ArrayList<AgencyProperties> agencies = DataSourceProvider.get(getContext()).getAllAgencies(getContext()); ArrayList<Future<HashSet<String>>> taskList = new ArrayList<Future<HashSet<String>>>(); if (agencies != null) { for (AgencyProperties agency : agencies) { FindSearchSuggestTask task = new FindSearchSuggestTask(getContext(), agency, query); taskList.add(getFetchSuggestExecutor().submit(task)); } } for (Future<HashSet<String>> future : taskList) { try { HashSet<String> agencySuggestions = future.get(); if (agencySuggestions != null) { suggestions.addAll(agencySuggestions); } } catch (Exception e) { MTLog.w(this, e, "Error while loading in background!"); } } clearAllTasks(); } MatrixCursor cursor = new MatrixCursor(SEARCH_SUGGEST_COLUMNS); int autoIncId = 0; if (recentSearchSuggestions != null) { for (String suggestion : recentSearchSuggestions) { cursor.addRow(new Object[] { autoIncId++, RECENT_SEARCH_SUGGEST_ICON, suggestion, suggestion }); } } for (String suggestion : suggestions) { if (recentSearchSuggestions != null && recentSearchSuggestions.contains(suggestion)) { continue; // skip } cursor.addRow(new Object[] { autoIncId++, SEARCH_SUGGEST_ICON, suggestion, suggestion }); } return cursor; } private static final int CORE_POOL_SIZE = RuntimeUtils.NUMBER_OF_CORES; private static final int MAX_POOL_SIZE = RuntimeUtils.NUMBER_OF_CORES; private ThreadPoolExecutor fetchSuggestExecutor; public ThreadPoolExecutor getFetchSuggestExecutor() { if (this.fetchSuggestExecutor == null) { this.fetchSuggestExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAX_POOL_SIZE, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>()); } return fetchSuggestExecutor; } public void clearAllTasks() { if (this.fetchSuggestExecutor != null) { this.fetchSuggestExecutor.shutdown(); this.fetchSuggestExecutor = null; } } private static class FindSearchSuggestTask extends MTCallable<HashSet<String>> { private static final String TAG = SearchSuggestProvider.class.getSimpleName() + ">" + FindSearchSuggestTask.class.getSimpleName(); @Override public String getLogTag() { return TAG; } private Context context; private AgencyProperties agency; private String query; public FindSearchSuggestTask(Context context, AgencyProperties agency, String query) { this.context = context; this.agency = agency; this.query = query; } @Override public HashSet<String> callMT() throws Exception { return DataSourceManager.findSearchSuggest(this.context, this.agency.getAuthority(), this.query); } } }