/* * Copyright (C) 2012 - 2013 Niall 'Rivernile' Scott * * This software is provided 'as-is', without any express or implied * warranty. In no event will the authors or contributors be held liable for * any damages arising from the use of this software. * * The aforementioned copyright holder(s) hereby grant you a * non-transferrable right to use this software for any purpose (including * commercial applications), and to modify it and redistribute it, subject to * the following conditions: * * 1. This notice may not be removed or altered from any file it appears in. * * 2. Any modifications made to this software, except those defined in * clause 3 of this agreement, must be released under this license, and * the source code of any modifications must be made available on a * publically accessible (and locateable) website, or sent to the * original author of this software. * * 3. Software modifications that do not alter the functionality of the * software but are simply adaptations to a specific environment are * exempt from clause 2. */ package uk.org.rivernile.edinburghbustracker.android; import android.app.SearchManager; import android.content.Context; import android.content.SearchRecentSuggestionsProvider; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.location.Location; import android.location.LocationManager; import android.net.Uri; import android.os.Build; import android.provider.BaseColumns; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * This ContentProvider extends the SearchRecentSuggestionsProvider and quite * frankly, abuses it. The purpose of this is to return recent search items * and search suggestions based on bus stops and location to the search * controls. * * @author Niall Scott */ public class MapSearchSuggestionsProvider extends SearchRecentSuggestionsProvider { /** The authority to use. */ public static final String AUTHORITY = "uk.org.rivernile." + "edinburghbustracker.android.MapSearchSuggestionsProvider"; /** The database modes. */ public static final int MODE = DATABASE_MODE_QUERIES | DATABASE_MODE_2LINES; private static final String[] COLUMNS; private static final boolean SUPPORTS_ICON = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; private LocationManager locMan; static { if (SUPPORTS_ICON) { COLUMNS = new String[] { SearchManager.SUGGEST_COLUMN_FORMAT, SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_TEXT_2, SearchManager.SUGGEST_COLUMN_QUERY, BaseColumns._ID }; } else { COLUMNS = new String[] { SearchManager.SUGGEST_COLUMN_FORMAT, SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_TEXT_2, SearchManager.SUGGEST_COLUMN_QUERY, BaseColumns._ID }; } } /** * Create a new MapSearchSuggestionsProvider. As per the API documentation, * this sets up the suggestions. */ public MapSearchSuggestionsProvider() { setupSuggestions(AUTHORITY, MODE); } /** * {@inheritDoc} */ @Override public boolean onCreate() { // Get an instance of the LocationManager. locMan = (LocationManager)getContext().getSystemService( Context.LOCATION_SERVICE); return super.onCreate(); } /** * {@inheritDoc} */ @Override public Cursor query(final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder) { // Get the recent search terms first, then merge later. final Cursor recentCursor = super.query(uri, projection, selection, selectionArgs, sortOrder); // If there's no selection args, then just return the recent searches // cursor. if(selectionArgs == null || selectionArgs.length == 0 || selectionArgs[0] == null || selectionArgs[0].length() == 0) { return recentCursor; } final MatrixCursor cursor = new MatrixCursor(COLUMNS); final String query = selectionArgs[0]; // Get the last known device location. final Location location = getLastLocation(); final BusStopDatabase bsd = BusStopDatabase.getInstance(getContext()); final ArrayList<SearchResult> results = new ArrayList<SearchResult>(); final StringBuilder sb = new StringBuilder(); // The float array is for getting distances. float[] distance = new float[1]; // This is so that the _id column is unique. final int recentLastIndex = recentCursor.getCount(); SearchResult result; String locality; // Synchronize so that requests aren't made during database updates. synchronized(bsd) { final Cursor c = bsd.searchDatabase(query); if(c != null) { while(c.moveToNext()) { // Populate bus stop records. result = new SearchResult(); result.stopCode = c.getString(1); result.latitude = c.getDouble(3); result.longitude = c.getDouble(4); result.orientation = c.getInt(5); locality = c.getString(6); result.services = bsd.getBusServicesForStopAsString( result.stopCode); if(location != null) { // If the location is known, get the distance to the bus // stop. Location.distanceBetween(result.latitude, result.longitude, location.getLatitude(), location.getLongitude(), distance); result.distance = distance[0]; } // If there's locality information, append it. if(locality != null) { result.stopName = c.getString(2) + ", " + locality; } else { result.stopName = c.getString(2); } results.add(result); } c.close(); } } // Sort the list by distance ascending. Collections.sort(results); final int count = results.size(); Object[] row; int drawable; for(int i = 0; i < count; i++) { // Loop though all of the results and add them to the MatrixCursor. result = results.get(i); sb.append(result.stopName).append(' ').append('(') .append(result.stopCode).append(')'); if (SUPPORTS_ICON) { switch(result.orientation) { case 1: drawable = R.drawable.ic_map_busstopne; break; case 2: drawable = R.drawable.ic_map_busstope; break; case 3: drawable = R.drawable.ic_map_busstopse; break; case 4: drawable = R.drawable.ic_map_busstops; break; case 5: drawable = R.drawable.ic_map_busstopsw; break; case 6: drawable = R.drawable.ic_map_busstopw; break; case 7: drawable = R.drawable.ic_map_busstopnw; break; case 0: default: drawable = R.drawable.ic_map_busstopn; break; } row = new Object[] { null, "android.resource://" + getContext().getPackageName() + '/' + drawable, sb.toString(), result.services, result.stopCode, (recentLastIndex + i) }; } else { row = new Object[] { null, sb.toString(), result.services, result.stopCode, (recentLastIndex + i) }; } cursor.addRow(row); sb.setLength(0); } // Merge the recent suggestions and suggestions together to form a // single Cursor. return new MergeCursor(new Cursor[] { recentCursor, cursor }); } /** * Get the last known location of the device. * * @return The last known location of the device. */ private Location getLastLocation() { final List<String> providers = locMan.getAllProviders(); Location temp, bestLocation = null; // Loop through all location providers. for(String provider : providers) { temp = locMan.getLastKnownLocation(provider); // If there's no best location, set the best location to be that of // the current provider. if(bestLocation == null) { bestLocation = temp; } else { if(temp != null) { // If this provider's location is newer than the best // provider's, then go with that. if(temp.getTime() > bestLocation.getTime()) { bestLocation = temp; } } } } return bestLocation; } /** * A SearchResult object holds data on a bus stop or location result. */ private static class SearchResult implements Comparable<SearchResult> { public byte type; public double latitude; public double longitude; public float distance = Float.MAX_VALUE; public String stopCode; public String stopName; public String services; public int orientation; /** * {@inheritDoc} */ @Override public int compareTo(final SearchResult another) { // Order by distance ascending. if(distance == another.distance) return 0; if(distance > another.distance) { return 1; } else { return -1; } } } }