package boston.Bus.Map.provider; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.HashMap; import java.util.List; import java.util.zip.GZIPInputStream; import com.google.common.io.ByteStreams; import com.schneeloch.bostonbusmap_library.database.Schema; import com.schneeloch.bostonbusmap_library.math.Geometry; import com.schneeloch.bostonbusmap_library.util.Constants; import com.schneeloch.bostonbusmap_library.util.LogUtil; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.content.UriMatcher; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.preference.PreferenceManager; public class DatabaseContentProvider extends ContentProvider { private static final UriMatcher uriMatcher; public static final String AUTHORITY = "com.bostonbusmap.databaseprovider"; private static final String STOPS_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.stop"; public static final Uri STOPS_URI = Uri.parse("content://" + AUTHORITY + "/stops"); private static final int STOPS = 2; private static final String ROUTES_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.route"; public static final Uri ROUTES_URI = Uri.parse("content://" + AUTHORITY + "/routes"); private static final int ROUTES = 3; private static final String STOPS_ROUTES_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.stop_route"; public static final Uri STOPS_ROUTES_URI = Uri.parse("content://" + AUTHORITY + "/stops_routes"); private static final int STOPS_ROUTES = 4; private static final String STOPS_STOPS_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.stop_stop"; public static final Uri STOPS_STOPS_URI = Uri.parse("content://" + AUTHORITY + "/stops_stops"); private static final int STOPS_STOPS = 5; private static final String STOPS_LOOKUP_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.stop_lookup"; public static final Uri STOPS_LOOKUP_URI = Uri.parse("content://" + AUTHORITY + "/stops_lookup"); private static final int STOPS_LOOKUP = 6; private static final String DIRECTIONS_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.direction"; public static final Uri DIRECTIONS_URI = Uri.parse("content://" + AUTHORITY + "/directions"); private static final int DIRECTIONS = 7; private static final String DIRECTIONS_STOPS_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.direction_stop"; public static final Uri DIRECTIONS_STOPS_URI = Uri.parse("content://" + AUTHORITY + "/directions_stops"); private static final int DIRECTIONS_STOPS = 8; private static final String STOPS_LOOKUP_2_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.stop_lookup_2"; public static final Uri STOPS_LOOKUP_2_URI = Uri.parse("content://" + AUTHORITY + "/stops_lookup_2"); private static final int STOPS_LOOKUP_2 = 9; private static final String STOPS_LOOKUP_3_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.stop_lookup_3"; public static final Uri STOPS_LOOKUP_3_URI = Uri.parse("content://" + AUTHORITY + "/stops_lookup_3"); private static final int STOPS_LOOKUP_3 = 10; private static final String STOPS_WITH_DISTANCE_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.stop_with_distance"; public static final Uri STOPS_WITH_DISTANCE_URI = Uri.parse("content://" + AUTHORITY + "/stops_with_distance"); private static final int STOPS_WITH_DISTANCE = 11; private static final String STOPS_AND_ROUTES_WITH_DISTANCE_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap.stop_and_route_with_distance"; public static final Uri STOPS_AND_ROUTES_WITH_DISTANCE_URI = Uri.parse("content://" + AUTHORITY + "/stops_and_routes_with_distance"); private static final int STOPS_AND_ROUTES_WITH_DISTANCE = 17; private static final String ROUTES_AND_BOUNDS_TYPE = "vnd.android.cursor.dir/vnd.bostonbusmap/route_and_bound"; public static final Uri ROUTES_AND_BOUNDS_URI = Uri.parse("content://" + AUTHORITY + "/routes_and_bounds"); private static final int ROUTES_AND_BOUNDS = 18; private final static String DATABASE_VERSION_KEY = "DB_VERSION"; public static final String distanceKey = "distance"; /** * Handles the database which stores route information * * @author schneg * */ public static class DatabaseHelper extends SQLiteOpenHelper { private Context context; /** * Get the application's version code and use it to keep track of database versions. * This means the database will be assumed to have changed on each update, which will cause this * database to be recreated (the favorites database should be left alone though) */ public static int getVersionCode(Context context) { try { return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode; } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException(e); } } /** * Don't call this, use getInstance instead * @param context */ private DatabaseHelper(Context context) { super(context, Schema.dbName, null, getVersionCode(context)); this.context = context; } @Override public SQLiteDatabase getReadableDatabase() { return getWritableDatabase(); } @Override public SQLiteDatabase getWritableDatabase() { SQLiteDatabase db; SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); int version = preferences.getInt(DATABASE_VERSION_KEY, -1); if (version != getVersionCode(context)) { try { copyDatabase(); preferences.edit().putInt(DATABASE_VERSION_KEY, getVersionCode(context)).commit(); } catch (IOException e) { LogUtil.e(e); return null; } db = super.getWritableDatabase(); } else { db = super.getWritableDatabase(); } return db; } private void copyDatabase() throws IOException { InputStream in = null; GZIPInputStream stream = null; OutputStream outputStream = null; try { in = context.getResources().openRawResource(boston.Bus.Map.R.raw.databasegz); stream = new GZIPInputStream(in); SQLiteDatabase tempDatabase = null; try { tempDatabase = super.getWritableDatabase(); } catch (Exception e) { // ignore } finally { if (tempDatabase != null) { tempDatabase.close(); } } // overwrite database with prepopulated database outputStream = new FileOutputStream(context.getDatabasePath(Schema.dbName)); ByteStreams.copy(stream, outputStream); } finally { try { if (in != null) { in.close(); } if (stream != null) { stream.close(); } if (outputStream != null) { outputStream.close(); } } catch (IOException e) { LogUtil.e(e); } } } @Override public void onCreate(SQLiteDatabase db) { } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { } } private DatabaseHelper helper; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(AUTHORITY, "stops", STOPS); uriMatcher.addURI(AUTHORITY, "routes", ROUTES); uriMatcher.addURI(AUTHORITY, "stops_routes", STOPS_ROUTES); uriMatcher.addURI(AUTHORITY, "stops_stops", STOPS_STOPS); uriMatcher.addURI(AUTHORITY, "stops_lookup", STOPS_LOOKUP); uriMatcher.addURI(AUTHORITY, "directions", DIRECTIONS); uriMatcher.addURI(AUTHORITY, "directions_stops", DIRECTIONS_STOPS); uriMatcher.addURI(AUTHORITY, "stops_lookup_2", STOPS_LOOKUP_2); uriMatcher.addURI(AUTHORITY, "stops_lookup_3", STOPS_LOOKUP_3); uriMatcher.addURI(AUTHORITY, "stops_with_distance/*/*/#", STOPS_WITH_DISTANCE); uriMatcher.addURI(AUTHORITY, "stops_and_routes_with_distance/*/*/#", STOPS_AND_ROUTES_WITH_DISTANCE); uriMatcher.addURI(AUTHORITY, "routes_and_bounds", ROUTES_AND_BOUNDS); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { SQLiteDatabase db = helper.getWritableDatabase(); int count; switch (uriMatcher.match(uri)) { case STOPS: count = db.delete(Schema.Stops.table, selection, selectionArgs); break; case ROUTES: count = db.delete(Schema.Routes.table, selection, selectionArgs); break; case STOPS_ROUTES: count = db.delete(Schema.Stopmapping.table, selection, selectionArgs); break; case DIRECTIONS: count = db.delete(Schema.Directions.table, selection, selectionArgs); break; case DIRECTIONS_STOPS: count = db.delete(Schema.DirectionsStops.table, selection, selectionArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } @Override public String getType(Uri uri) { switch (uriMatcher.match(uri)) { case STOPS: return STOPS_TYPE; case ROUTES: return ROUTES_TYPE; case STOPS_ROUTES: return STOPS_ROUTES_TYPE; case STOPS_STOPS: return STOPS_STOPS_TYPE; case STOPS_LOOKUP: return STOPS_LOOKUP_TYPE; case DIRECTIONS: return DIRECTIONS_TYPE; case DIRECTIONS_STOPS: return DIRECTIONS_STOPS_TYPE; case STOPS_LOOKUP_2: return STOPS_LOOKUP_2_TYPE; case STOPS_LOOKUP_3: return STOPS_LOOKUP_3_TYPE; case STOPS_WITH_DISTANCE: return STOPS_WITH_DISTANCE_TYPE; case STOPS_AND_ROUTES_WITH_DISTANCE: return STOPS_AND_ROUTES_WITH_DISTANCE_TYPE; case ROUTES_AND_BOUNDS: return ROUTES_AND_BOUNDS_TYPE; default: throw new IllegalArgumentException("Unknown URI " + uri); } } @Override public Uri insert(Uri uri, ContentValues values) { int match = uriMatcher.match(uri); switch (match) { case STOPS: case ROUTES: case STOPS_ROUTES: case DIRECTIONS: case DIRECTIONS_STOPS: break; default: throw new IllegalArgumentException("Unknown URI " + uri); } SQLiteDatabase db = helper.getWritableDatabase(); switch (match) { case STOPS: { long rowId = db.replace(Schema.Stops.table, null, values); if (rowId >= 0) { getContext().getContentResolver().notifyChange(uri, null); return STOPS_URI; } } break; case ROUTES: { long rowId = db.replace(Schema.Routes.table, null, values); if (rowId >= 0) { getContext().getContentResolver().notifyChange(uri, null); return ROUTES_URI; } } break; case STOPS_ROUTES: { long rowId = db.replace(Schema.Stopmapping.table, null, values); if (rowId >= 0) { getContext().getContentResolver().notifyChange(uri, null); return STOPS_ROUTES_URI; } } break; case DIRECTIONS: { long rowId = db.replace(Schema.Directions.table, null, values); if (rowId >= 0) { getContext().getContentResolver().notifyChange(uri, null); return DIRECTIONS_URI; } } break; case DIRECTIONS_STOPS: { long rowId = db.replace(Schema.DirectionsStops.table, null, values); if (rowId >= 0) { getContext().getContentResolver().notifyChange(uri, null); return DIRECTIONS_STOPS_URI; } } break; } throw new SQLException("Failed to insert row into " + uri); } @Override public boolean onCreate() { helper = new DatabaseHelper(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder builder = new SQLiteQueryBuilder(); String limit = null; switch (uriMatcher.match(uri)) { case STOPS: builder.setTables(Schema.Stops.table); break; case ROUTES: builder.setTables(Schema.Routes.table); break; case STOPS_ROUTES: builder.setTables(Schema.Stopmapping.table); break; case STOPS_STOPS: builder.setTables(Schema.Stops.table + " as s1, " + Schema.Stops.table + " as s2"); break; case STOPS_LOOKUP: builder.setTables(Schema.Stops.table + " JOIN " + Schema.Stopmapping.table + " AS sm1 ON (" + Schema.Stops.tagColumnOnTable + " = sm1." + Schema.Stopmapping.tagColumn + ")" + " JOIN " + Schema.Stopmapping.table + " AS sm2 ON (" + Schema.Stops.tagColumnOnTable + " = sm2." + Schema.Stopmapping.tagColumn + ")" ); break; case DIRECTIONS: builder.setTables(Schema.Directions.table); break; case DIRECTIONS_STOPS: builder.setTables(Schema.DirectionsStops.table); break; case STOPS_LOOKUP_2: builder.setTables(Schema.Stops.table + " JOIN " + Schema.Stopmapping.table + " AS sm1 ON (" + Schema.Stops.tagColumnOnTable + " = sm1." + Schema.Stopmapping.tagColumn + ")" + " JOIN " + Schema.Routes.table + " AS r1 ON (sm1." + Schema.Stopmapping.routeColumn + " = r1." + Schema.Routes.routeColumn + ")"); break; case STOPS_LOOKUP_3: builder.setTables(Schema.Stops.table + " JOIN " + Schema.Stopmapping.table + " ON (" + Schema.Stops.tagColumnOnTable + " = " + Schema.Stopmapping.tagColumnOnTable + ") "); break; case STOPS_WITH_DISTANCE: { List<String> pathSegments = uri.getPathSegments(); double currentLat = Integer.parseInt(pathSegments.get(1)) * Constants.InvE6; double currentLon = Integer.parseInt(pathSegments.get(2)) * Constants.InvE6; limit = pathSegments.get(3); builder.setTables(Schema.Stops.table); HashMap<String, String> projectionMap = new HashMap<String, String>(); projectionMap.put(Schema.Stops.tagColumnOnTable, Schema.Stops.tagColumnOnTable); double lonFactor = Math.cos(currentLat * Geometry.degreesToRadians); String latDiff = "(" + Schema.Stops.latColumn + " - " + currentLat + ")"; String lonDiff = "((" + Schema.Stops.lonColumn + " - " + currentLon + ")*" + lonFactor + ")"; projectionMap.put(distanceKey, latDiff + "*" + latDiff + " + " + lonDiff + "*" + lonDiff + " AS " + distanceKey); builder.setProjectionMap(projectionMap); } break; case STOPS_AND_ROUTES_WITH_DISTANCE: { List<String> pathSegments = uri.getPathSegments(); double currentLat = Integer.parseInt(pathSegments.get(1)) * Constants.InvE6; double currentLon = Integer.parseInt(pathSegments.get(2)) * Constants.InvE6; limit = pathSegments.get(3); builder.setTables(Schema.Stops.table + " JOIN " + Schema.Stopmapping.table + " ON " + Schema.Stops.tagColumnOnTable + " = " + Schema.Stopmapping.tagColumnOnTable); HashMap<String, String> projectionMap = new HashMap<String, String>(); projectionMap.put(Schema.Stops.tagColumnOnTable, Schema.Stops.tagColumnOnTable); double lonFactor = Math.cos(currentLat * Geometry.degreesToRadians); String latDiff = "(" + Schema.Stops.latColumn + " - " + currentLat + ")"; String lonDiff = "((" + Schema.Stops.lonColumn + " - " + currentLon + ")*" + lonFactor + ")"; projectionMap.put(distanceKey, latDiff + "*" + latDiff + " + " + lonDiff + "*" + lonDiff + " AS " + distanceKey); builder.setProjectionMap(projectionMap); } break; case ROUTES_AND_BOUNDS: builder.setTables(Schema.Routes.table + " LEFT OUTER JOIN " + Schema.Bounds.table + " ON " + Schema.Routes.routeColumnOnTable + " = " + Schema.Bounds.routeColumnOnTable); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } //Log.v("BostonBusMap", builder.buildQuery(projection, selection, null, null, null, sortOrder, limit)); SQLiteDatabase db = helper.getReadableDatabase(); Cursor c = builder.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit); c.setNotificationUri(getContext().getContentResolver(), uri); return c; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { SQLiteDatabase db = helper.getWritableDatabase(); int count; switch (uriMatcher.match(uri)) { case STOPS: count = db.update(Schema.Stops.table, values, selection, selectionArgs); break; case ROUTES: count = db.update(Schema.Routes.table, values, selection, selectionArgs); break; case STOPS_ROUTES: count = db.update(Schema.Stopmapping.table, values, selection, selectionArgs); break; case DIRECTIONS: count = db.update(Schema.Directions.table, values, selection, selectionArgs); break; case DIRECTIONS_STOPS: count = db.update(Schema.DirectionsStops.table, values, selection, selectionArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } }