package com.truckmuncher.app.data; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Intent; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteStatement; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import com.truckmuncher.app.App; import com.truckmuncher.app.common.LoggerStarter; import com.truckmuncher.app.data.sql.Tables; import com.truckmuncher.app.menu.MenuUpdateService; import javax.inject.Inject; import hugo.weaving.DebugLog; import timber.log.Timber; public class TruckMuncherContentProvider extends ContentProvider { public static final String METHOD_UPDATE_INACTIVE_TRUCKS = "method_update_inactive_trucks"; public static final String METHOD_UPDATE_SEARCH_RESULTS = "method_update_search_results"; public static final String METHOD_CLEAR_SEARCH_RESULTS = "method_clear_search_results"; public static final String ARG_ID_ARRAY = "arg_id_list"; private static final int CATEGORY = 10; private static final int MENU_ITEM = 20; private static final int TRUCK = 30; private static final int TRUCK_STATE = 31; private static final int TRUCK_PROPERTIES = 32; private static final int MENU = 40; private static final UriMatcher uriMatcher = buildUriMatcher(); // TODO this is not OK. We need push messages private static boolean hasAlreadySyncedMenuThisSession = false; @Inject SQLiteOpenHelper database; private static UriMatcher buildUriMatcher() { UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); String authority = PublicContract.CONTENT_AUTHORITY; matcher.addURI(authority, "/category", CATEGORY); matcher.addURI(authority, "/menu_item", MENU_ITEM); matcher.addURI(authority, "/truck", TRUCK); matcher.addURI(authority, "/truck_state", TRUCK_STATE); matcher.addURI(authority, "/truck_properties", TRUCK_PROPERTIES); matcher.addURI(authority, "/menu", MENU); return matcher; } @Override public boolean onCreate() { LoggerStarter.start(); return true; } @Override public Bundle call(@NonNull String method, String arg, Bundle extras) { if (database == null) { App.get(getContext()).inject(this); } SQLiteDatabase db = database.getReadableDatabase(); SQLiteStatement statement; String[] ids = extras != null ? extras.getStringArray(ARG_ID_ARRAY) : new String[]{}; switch (method) { case METHOD_UPDATE_INACTIVE_TRUCKS: statement = db.compileStatement("UPDATE " + Tables.TRUCK_STATE + " SET " + PublicContract.Truck.IS_SERVING + " = CASE WHEN " + PublicContract.Truck.ID + " not in (" + generatePlaceholders(ids.length) + ") THEN 0 ELSE 1 END;"); for (int i = 0; i < ids.length; i++) { statement.bindString(i + 1, ids[i]); } break; case METHOD_UPDATE_SEARCH_RESULTS: statement = db.compileStatement("UPDATE " + Tables.TRUCK_STATE + " SET " + PublicContract.Truck.MATCHED_SEARCH + " = CASE WHEN " + PublicContract.Truck.ID + " not in (" + generatePlaceholders(ids.length) + ") THEN 0 ELSE 1 END;"); for (int i = 0; i < ids.length; i++) { statement.bindString(i + 1, ids[i]); } break; case METHOD_CLEAR_SEARCH_RESULTS: statement = db.compileStatement("UPDATE " + Tables.TRUCK_STATE + " SET " + PublicContract.Truck.MATCHED_SEARCH + " = 1"); break; default: return super.call(method, arg, extras); } statement.execute(); getContext().getContentResolver().notifyChange(PublicContract.TRUCK_URI, null); return null; } @DebugLog @Override @NonNull public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if (database == null) { App.get(getContext()).inject(this); } SQLiteDatabase db = database.getReadableDatabase(); String tableName; switch (uriMatcher.match(uri)) { case CATEGORY: tableName = Tables.CATEGORY; break; case MENU_ITEM: tableName = Tables.MENU_ITEM; break; case TRUCK: tableName = Tables.TRUCK; break; case TRUCK_STATE: tableName = Tables.TRUCK_STATE; break; case TRUCK_PROPERTIES: tableName = Tables.TRUCK_PROPERTIES; break; case MENU: tableName = Tables.MENU; // TODO replace with a push notification to spawn this sync if (Contract.isSyncFromNetwork(uri) && !hasAlreadySyncedMenuThisSession) { getContext().startService(new Intent(getContext(), MenuUpdateService.class)); hasAlreadySyncedMenuThisSession = true; } break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } Cursor retCursor = db.query(tableName, projection, selection, selectionArgs, null, null, sortOrder); retCursor.setNotificationUri(getContext().getContentResolver(), uri); return retCursor; } @DebugLog @Override @NonNull public String getType(@NonNull Uri uri) { switch (uriMatcher.match(uri)) { case CATEGORY: return PublicContract.URI_TYPE_CATEGORY; case MENU_ITEM: return PublicContract.URI_TYPE_MENU_ITEM; case TRUCK: return PublicContract.URI_TYPE_TRUCK; case MENU: return PublicContract.URI_TYPE_MENU; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } } /** * @return null if the insert failed. Otherwise the same uri as was provided. */ @DebugLog @Override public Uri insert(@NonNull Uri uri, ContentValues values) { throw new UnsupportedOperationException("Not yet implemented. Uri: " + uri); } @DebugLog @Override public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { if (database == null) { App.get(getContext()).inject(this); } SQLiteDatabase db = database.getWritableDatabase(); int rowsDeleted; switch (uriMatcher.match(uri)) { case TRUCK_STATE: rowsDeleted = db.delete(Tables.TRUCK_STATE, selection, selectionArgs); break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } // Because a null deletes all rows if (selection == null || rowsDeleted != 0 && !Contract.isSuppressNotify(uri)) { getContext().getContentResolver().notifyChange(uri, null, Contract.isSyncToNetwork(uri)); } return rowsDeleted; } @DebugLog @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { if (database == null) { App.get(getContext()).inject(this); } SQLiteDatabase db = database.getWritableDatabase(); int rowsUpdated; switch (uriMatcher.match(uri)) { case TRUCK_STATE: rowsUpdated = db.update(Tables.TRUCK_STATE, values, selection, selectionArgs); break; case MENU_ITEM: rowsUpdated = db.update(Tables.MENU_ITEM, values, selection, selectionArgs); break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } // Notify and sync as appropriate. This is defined by the Uri if (rowsUpdated != 0 && !Contract.isSuppressNotify(uri)) { getContext().getContentResolver().notifyChange(uri, null, Contract.isSyncToNetwork(uri)); } return rowsUpdated; } @DebugLog @Override public int bulkInsert(Uri uri, @NonNull ContentValues[] valuesList) { if (database == null) { App.get(getContext()).inject(this); } SQLiteDatabase db = database.getWritableDatabase(); int returnCount = 0; boolean suppressNotification = Contract.isSuppressNotify(uri); String tableName; switch (uriMatcher.match(uri)) { case MENU_ITEM: tableName = Tables.MENU_ITEM; break; case CATEGORY: tableName = Tables.CATEGORY; break; case TRUCK_STATE: tableName = Tables.TRUCK_STATE; uri = suppressNotification ? Contract.suppressNotify(PublicContract.TRUCK_URI) : PublicContract.TRUCK_URI; break; default: Timber.w(new UnsupportedOperationException(), "Attempting a bulk insert for an unsupported URI, %s. Falling back to normal inserts...", uri); returnCount = super.bulkInsert(uri, valuesList); tableName = null; } if (tableName != null) { // We have bulkInsert support for the uri db.beginTransaction(); try { for (ContentValues values : valuesList) { long rowId = db.replace(tableName, null, values); if (rowId != -1) { returnCount++; } } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } if (returnCount > 0 && !suppressNotification) { getContext().getContentResolver().notifyChange(uri, null, Contract.isSyncToNetwork(uri)); } return returnCount; } private String generatePlaceholders(int number) { if (number < 1) { return ""; } else { StringBuilder builder = new StringBuilder(number * 2 - 1); builder.append("?"); for (int i = 1; i < number; i++) { builder.append(",?"); } return builder.toString(); } } }