package org.mtransit.android.provider; import java.net.HttpURLConnection; import java.net.SocketException; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Locale; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLHandshakeException; import org.json.JSONArray; import org.json.JSONObject; import org.mtransit.android.R; import org.mtransit.android.commons.ArrayUtils; import org.mtransit.android.commons.FileUtils; import org.mtransit.android.commons.LocaleUtils; import org.mtransit.android.commons.LocationUtils; import org.mtransit.android.commons.MTLog; import org.mtransit.android.commons.SqlUtils; import org.mtransit.android.commons.TimeUtils; import org.mtransit.android.commons.UriUtils; import org.mtransit.android.commons.data.POI.POIUtils; import org.mtransit.android.commons.provider.AgencyProvider; import org.mtransit.android.commons.provider.ContentProviderConstants; import org.mtransit.android.commons.provider.MTSQLiteOpenHelper; import org.mtransit.android.commons.provider.POIProvider; import org.mtransit.android.commons.provider.POIProviderContract; import org.mtransit.android.data.Place; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.MatrixCursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.support.v4.util.ArrayMap; import android.text.TextUtils; public class PlaceProvider extends AgencyProvider implements POIProviderContract { private static final String TAG = PlaceProvider.class.getSimpleName(); @Override public String getLogTag() { return TAG; } private static UriMatcher uriMatcher = null; /** * Override if multiple {@link PlaceProvider} implementations in same app. */ private static UriMatcher getURIMATCHER(Context context) { if (uriMatcher == null) { uriMatcher = getNewUriMatcher(getAUTHORITY(context)); } return uriMatcher; } public static UriMatcher getNewUriMatcher(String authority) { UriMatcher URI_MATCHER = AgencyProvider.getNewUriMatcher(authority); POIProvider.append(URI_MATCHER, authority); return URI_MATCHER; } @Override public UriMatcher getURI_MATCHER() { return getURIMATCHER(getContext()); } @Override public UriMatcher getAgencyUriMatcher() { return getURIMATCHER(getContext()); } @Override public Cursor getSearchSuggest(String query) { return null; // TODO implement Place/Query auto-complete } @Override public ArrayMap<String, String> getSearchSuggestProjectionMap() { return null; // TODO implement Place/Query auto-complete } @Override public String getSearchSuggestTable() { return null; // TODO implement Place/Query auto-complete } @Override public String getPOITable() { return PlaceDbHelper.T_PLACE; } private static final String[] PROJECTION_PLACE = new String[] { POIProviderContract.Columns.T_POI_K_SCORE_META_OPT, // PlaceColumns.T_PLACE_K_PROVIDER_ID, PlaceColumns.T_PLACE_K_LANG, PlaceColumns.T_PLACE_K_READ_AT_IN_MS }; public static final String[] PROJECTION_PLACE_POI = ArrayUtils.addAll(POIProvider.PROJECTION_POI, PROJECTION_PLACE); @Override public String[] getPOIProjection() { return PROJECTION_PLACE_POI; } private static String googlePlacesApiKey = null; /** * Override if multiple {@link PlaceProvider} implementations in same app. */ public static String getGOOGLE_PLACES_API_KEY(Context context) { if (googlePlacesApiKey == null) { googlePlacesApiKey = context.getResources().getString(R.string.google_places_api_key); } return googlePlacesApiKey; } private static final String TEXT_SEARCH_URL_PART_1_BEFORE_KEY = "https://maps.googleapis.com/maps/api/place/textsearch/json?key="; private static final String TEXT_SEARCH_URL_PART_2_BEFORE_LANG = "&language="; private static final String TEXT_SEARCH_URL_PART_3_BEFORE_LOCATION = "&location="; private static final String TEXT_SEARCH_URL_PART_4_BEFORE_RADIUS = "&radius="; private static final String TEXT_SEARCH_URL_PART_5_BEFORE_QUERY = "&query="; private static final String TEXT_SEARCH_URL_LANG_DEFAULT = "en"; private static final String TEXT_SEARCH_URL_LANG_FRENCH = "fr"; private static final int TEXT_SEARCH_URL_RADIUS_IN_METERS_DEFAULT = 50000; // max = 50000 private static String getTextSearchUrlString(Context context, Double optLat, Double optLng, Integer optRadiusInMeters, String[] searchKeywords) { StringBuilder sb = new StringBuilder(); sb.append(TEXT_SEARCH_URL_PART_1_BEFORE_KEY).append(getGOOGLE_PLACES_API_KEY(context)); sb.append(TEXT_SEARCH_URL_PART_2_BEFORE_LANG).append(LocaleUtils.isFR() ? TEXT_SEARCH_URL_LANG_FRENCH : TEXT_SEARCH_URL_LANG_DEFAULT); if (optLat != null && optLng != null) { sb.append(TEXT_SEARCH_URL_PART_3_BEFORE_LOCATION).append(optLat).append(',').append(optLng); sb.append(TEXT_SEARCH_URL_PART_4_BEFORE_RADIUS).append(optRadiusInMeters == null ? TEXT_SEARCH_URL_RADIUS_IN_METERS_DEFAULT : optRadiusInMeters); } if (ArrayUtils.getSize(searchKeywords) != 0 && !TextUtils.isEmpty(searchKeywords[0])) { sb.append(TEXT_SEARCH_URL_PART_5_BEFORE_QUERY); boolean isFirstKeyword = true; for (String searchKeyword : searchKeywords) { if (TextUtils.isEmpty(searchKeyword)) { continue; } String[] keywords = searchKeyword.toLowerCase(Locale.ENGLISH).split(ContentProviderConstants.SEARCH_SPLIT_ON); for (String keyword : keywords) { if (TextUtils.isEmpty(searchKeyword)) { continue; } if (!isFirstKeyword) { sb.append('+'); } sb.append(keyword); isFirstKeyword = false; } } } return sb.toString(); } private static final long POI_MAX_VALIDITY_IN_MS = Long.MAX_VALUE; private static final long POI_VALIDITY_IN_MS = Long.MAX_VALUE; @Override public long getPOIMaxValidityInMs() { return POI_MAX_VALIDITY_IN_MS; } @Override public long getPOIValidityInMs() { return POI_VALIDITY_IN_MS; } @Override public Cursor getPOI(POIProviderContract.Filter poiFilter) { if (poiFilter == null) { return null; } String url; if (POIProviderContract.Filter.isAreaFilter(poiFilter)) { return ContentProviderConstants.EMPTY_CURSOR; // empty cursor = processed } else if (POIProviderContract.Filter.isSearchKeywords(poiFilter)) { Double lat = poiFilter.getExtraDouble("lat", null); Double lng = poiFilter.getExtraDouble("lng", null); url = getTextSearchUrlString(getContext(), lat, lng, null, poiFilter.getSearchKeywords()); return getTextSearchResults(url); } else if (POIProviderContract.Filter.isUUIDFilter(poiFilter)) { return ContentProviderConstants.EMPTY_CURSOR; // empty cursor = processed } else if (POIProviderContract.Filter.isSQLSelection(poiFilter)) { return ContentProviderConstants.EMPTY_CURSOR; // empty cursor = processed } else { MTLog.w(this, "Unexpected POI filter '%s'!", poiFilter); return null; } } private Cursor getTextSearchResults(String urlString) { try { MTLog.i(this, "Loading from '%s'...", urlString); URL url = new URL(urlString); URLConnection urlc = url.openConnection(); HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) urlc; switch (httpsUrlConnection.getResponseCode()) { case HttpURLConnection.HTTP_OK: long newLastUpdateInMs = TimeUtils.currentTimeMillis(); String jsonString = FileUtils.getString(urlc.getInputStream()); String lang = LocaleUtils.isFR() ? Locale.FRENCH.getLanguage() : Locale.ENGLISH.getLanguage(); return parseTextSearchJson(jsonString, getAUTHORITY(getContext()), lang, newLastUpdateInMs); default: MTLog.w(this, "ERROR: HTTP URL-Connection Response Code %s (Message: %s)", httpsUrlConnection.getResponseCode(), httpsUrlConnection.getResponseMessage()); return null; } } catch (SSLHandshakeException sslhe) { MTLog.w(this, sslhe, "SSL error!"); return null; } catch (UnknownHostException uhe) { if (MTLog.isLoggable(android.util.Log.DEBUG)) { MTLog.w(this, uhe, "No Internet Connection!"); } else { MTLog.w(this, "No Internet Connection!"); } return null; } catch (SocketException se) { MTLog.w(TAG, se, "No Internet Connection!"); return null; } catch (Exception e) { MTLog.e(TAG, e, "INTERNAL ERROR: Unknown Exception"); return null; } } private static final String JSON_RESULTS = "results"; private static final String JSON_NAME = "name"; private static final String JSON_PLACE_ID = "place_id"; private static final String JSON_GEOMETRY = "geometry"; private static final String JSON_LOCATION = "location"; private static final String JSON_LAT = "lat"; private static final String JSON_LNG = "lng"; private Cursor parseTextSearchJson(String jsonString, String authority, String lang, long nowInMs) { try { ArrayList<Place> result = new ArrayList<Place>(); JSONObject json = jsonString == null ? null : new JSONObject(jsonString); if (json != null && json.has(JSON_RESULTS)) { int score = 1000; JSONArray jResults = json.getJSONArray(JSON_RESULTS); for (int i = 0; i < jResults.length(); i++) { try { JSONObject jResult = jResults.getJSONObject(i); String name = jResult.getString(JSON_NAME); String placeId = jResult.getString(JSON_PLACE_ID); JSONObject jGeometry = jResult.getJSONObject(JSON_GEOMETRY); JSONObject jLocation = jGeometry.getJSONObject(JSON_LOCATION); Place place = new Place(authority, placeId, lang, nowInMs); place.setName(name); place.setLat(jLocation.getDouble(JSON_LAT)); place.setLng(jLocation.getDouble(JSON_LNG)); place.setScore(score--); result.add(place); } catch (Exception e) { MTLog.w(this, e, "Error while parsing JSON result '%s'!", i); } } } return getTextSearchResults(result); } catch (Exception e) { MTLog.w(this, e, "Error while parsing JSON '%s'!", jsonString); return null; } } private Cursor getTextSearchResults(ArrayList<Place> places) { MatrixCursor cursor = new MatrixCursor(getPOIProjection()); if (places != null) { for (Place place : places) { cursor.addRow(new Object[] { // place.getUUID(), place.getDataSourceTypeId(), place.getId(), place.getName(), place.getLat(), place.getLng(), // place.getType(), place.getStatusType(), place.getActionsType(), // place.getScore(), // place.getProviderId(), place.getLang(), place.getReadAtInMs() // }); } } return cursor; } @Override public Cursor getPOIFromDB(POIProviderContract.Filter poiFilter) { return null; } @Override public LocationUtils.Area getAgencyArea(Context context) { return LocationUtils.THE_WORLD; } private static String authority = null; /** * Override if multiple {@link PlaceProvider} implementations in same app. */ public static String getAUTHORITY(Context context) { if (authority == null) { authority = context.getResources().getString(R.string.place_authority); } return authority; } private static Uri authorityUri = null; /** * Override if multiple {@link PlaceProvider} implementations in same app. */ public static Uri getAUTHORITYURI(Context context) { if (authorityUri == null) { authorityUri = UriUtils.newContentUri(getAUTHORITY(context)); } return authorityUri; } /** * Override if multiple {@link PlaceProvider} implementations in same app. */ @Override public String getAgencyColorString(Context context) { return null; // default } @Override public int getAgencyLabelResId() { return R.string.place_label; } @Override public int getAgencyShortNameResId() { return R.string.place_short_name; } /** * Override if multiple {@link PlaceProvider} in same app. */ public String getDbName() { return PlaceDbHelper.DB_NAME; } @Override public boolean isAgencyDeployed() { return SqlUtils.isDbExist(getContext(), getDbName()); } @Override public boolean isAgencySetupRequired() { boolean setupRequired = false; if (currentDbVersion > 0 && currentDbVersion != getCurrentDbVersion()) { setupRequired = true; // live update required => update } else if (!SqlUtils.isDbExist(getContext(), getDbName())) { setupRequired = true; // not deployed => initialization } else if (SqlUtils.getCurrentDbVersion(getContext(), getDbName()) != getCurrentDbVersion()) { setupRequired = true; // update required => update } return setupRequired; } @Override public void ping() { // do nothing } private static ArrayMap<String, String> poiProjectionMap; @Override public ArrayMap<String, String> getPOIProjectionMap() { if (poiProjectionMap == null) { poiProjectionMap = getNewPoiProjectionMap(getAUTHORITY(getContext())); } return poiProjectionMap; } @Override public boolean onCreateMT() { ping(); return true; } @Override public Cursor queryMT(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { try { Cursor cursor = super.queryMT(uri, projection, selection, selectionArgs, sortOrder); if (cursor != null) { return cursor; } cursor = POIProvider.queryS(this, uri, selection); if (cursor != null) { return cursor; } 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; } } @Override public String getSortOrder(Uri uri) { String sortOrder = POIProvider.getSortOrderS(this, uri); if (sortOrder != null) { return sortOrder; } return super.getSortOrder(uri); } @Override public String getTypeMT(Uri uri) { String type = POIProvider.getTypeS(this, uri); if (type != null) { return type; } return super.getTypeMT(uri); } @Override public int updateMT(Uri uri, ContentValues values, String selection, String[] selectionArgs) { MTLog.w(this, "The update method is not available."); return 0; } @Override public Uri insertMT(Uri uri, ContentValues values) { MTLog.w(this, "The insert method is not available."); return null; } @Override public int deleteMT(Uri uri, String selection, String[] selectionArgs) { MTLog.w(this, "The delete method is not available."); return 0; } public static ArrayMap<String, String> getNewPoiProjectionMap(String authority) { // @formatter:off return SqlUtils.ProjectionMapBuilder.getNew() // .appendValue(SqlUtils.concatenate( // SqlUtils.escapeString(POIUtils.UID_SEPARATOR), // SqlUtils.escapeString(authority), // SqlUtils.getTableColumn(PlaceDbHelper.T_PLACE, PlaceDbHelper.T_PLACE_K_PROVIDER_ID) // ), POIProviderContract.Columns.T_POI_K_UUID_META) // .appendValue(Place.DST_ID, POIProviderContract.Columns.T_POI_K_DST_ID_META) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_ID, POIProviderContract.Columns.T_POI_K_ID) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_NAME, POIProviderContract.Columns.T_POI_K_NAME) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_LAT, POIProviderContract.Columns.T_POI_K_LAT) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_LNG, POIProviderContract.Columns.T_POI_K_LNG) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_TYPE, POIProviderContract.Columns.T_POI_K_TYPE) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_STATUS_TYPE, POIProviderContract.Columns.T_POI_K_STATUS_TYPE) // .appendTableColumn(POIProvider.POIDbHelper.T_POI, POIProvider.POIDbHelper.T_POI_K_ACTIONS_TYPE, POIProviderContract.Columns.T_POI_K_ACTIONS_TYPE) // // .appendTableColumn(PlaceDbHelper.T_PLACE, PlaceDbHelper.T_PLACE_K_PROVIDER_ID, PlaceColumns.T_PLACE_K_PROVIDER_ID) // .appendTableColumn(PlaceDbHelper.T_PLACE, PlaceDbHelper.T_PLACE_K_LANG, PlaceColumns.T_PLACE_K_LANG) // .appendTableColumn(PlaceDbHelper.T_PLACE, PlaceDbHelper.T_PLACE_K_READ_AT_IN_MS, PlaceColumns.T_PLACE_K_READ_AT_IN_MS) // .build(); // @formatter:on } @Override public SQLiteOpenHelper getDBHelper() { return getDBHelper(getContext()); } private static PlaceDbHelper dbHelper; private static int currentDbVersion = -1; private PlaceDbHelper getDBHelper(Context context) { if (dbHelper == null) { // initialize dbHelper = getNewDbHelper(context); currentDbVersion = getCurrentDbVersion(); } else { // reset try { if (currentDbVersion != getCurrentDbVersion()) { dbHelper.close(); dbHelper = null; return getDBHelper(context); } } catch (Exception e) { // reset MTLog.d(this, e, "Can't check DB version!"); } } return dbHelper; } /** * Override if multiple {@link PlaceProvider} implementations in same app. */ public PlaceDbHelper getNewDbHelper(Context context) { return new PlaceDbHelper(context.getApplicationContext()); } /** * Override if multiple {@link PlaceProvider} implementations in same app. */ private int getCurrentDbVersion() { return PlaceDbHelper.getDbVersion(); } @Override public int getAgencyVersion() { return getCurrentDbVersion(); } private static class PlaceDbHelper extends MTSQLiteOpenHelper { private static final String TAG = PlaceDbHelper.class.getSimpleName(); @Override public String getLogTag() { return TAG; } /** * Override if multiple {@link PlaceDbHelper} in same app. */ public static final String DB_NAME = "place.db"; /** * Override if multiple {@link PlaceDbHelper} in same app. */ public static final int DB_VERSION = 2; public static final String T_PLACE = POIProvider.POIDbHelper.T_POI; public static final String T_PLACE_K_PROVIDER_ID = POIProvider.POIDbHelper.getFkColumnName("provider_id"); public static final String T_PLACE_K_LANG = POIProvider.POIDbHelper.getFkColumnName("lang"); public static final String T_PLACE_K_READ_AT_IN_MS = POIProvider.POIDbHelper.getFkColumnName("read_at_in_ms"); private static final String T_PLACE_SQL_CREATE = POIProvider.POIDbHelper.getSqlCreateBuilder(T_PLACE) // .appendColumn(T_PLACE_K_PROVIDER_ID, SqlUtils.TXT) // .appendColumn(T_PLACE_K_LANG, SqlUtils.TXT) // .appendColumn(T_PLACE_K_READ_AT_IN_MS, SqlUtils.INT) // .build(); private static final String T_PLACE_SQL_DROP = SqlUtils.getSQLDropIfExistsQuery(T_PLACE); /** * Override if multiple {@link PlaceDbHelper} in same app. */ public static int getDbVersion() { return DB_VERSION; } public PlaceDbHelper(Context context) { super(context, DB_NAME, null, getDbVersion()); } @Override public void onCreateMT(SQLiteDatabase db) { initAllDbTables(db); } @Override public void onUpgradeMT(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(T_PLACE_SQL_DROP); initAllDbTables(db); } private void initAllDbTables(SQLiteDatabase db) { db.execSQL(T_PLACE_SQL_CREATE); } } public static class PlaceColumns { public static final String T_PLACE_K_PROVIDER_ID = POIProviderContract.Columns.getFkColumnName("provider_id"); public static final String T_PLACE_K_LANG = POIProviderContract.Columns.getFkColumnName("lang"); public static final String T_PLACE_K_READ_AT_IN_MS = POIProviderContract.Columns.getFkColumnName("read_at_in_ms"); } }