package com.code44.finance.data.providers; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.provider.BaseColumns; import android.text.TextUtils; import com.code44.finance.api.Api; import com.code44.finance.common.model.ModelState; import com.code44.finance.common.utils.StringUtils; import com.code44.finance.data.Query; import com.code44.finance.data.db.Column; import com.code44.finance.data.db.Tables; import com.code44.finance.data.model.SyncState; import com.code44.finance.utils.IOUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.inject.Inject; @SuppressWarnings("UnusedParameters") public abstract class BaseModelProvider extends BaseProvider { private static final int URI_ITEMS = 1; private static final int URI_ITEMS_ID = 2; @Inject Api api; public static Uri uriModels(Class<? extends BaseModelProvider> providerClass, String modelTable) { return Uri.parse(CONTENT_URI_BASE + getAuthority(providerClass) + "/" + modelTable); } public static Uri uriModel(Class<? extends BaseModelProvider> providerClass, String modelTable, String modelServerId) { return Uri.withAppendedPath(uriModels(providerClass, modelTable), modelServerId); } @Override public boolean onCreate() { super.onCreate(); final String authority = getAuthority(); final String mainTable = getModelTable(); uriMatcher.addURI(authority, mainTable, URI_ITEMS); uriMatcher.addURI(authority, mainTable + "/*", URI_ITEMS_ID); return true; } @Override public String getType(Uri uri) { switch (uriMatcher.match(uri)) { case URI_ITEMS: return TYPE_LIST_BASE + getModelTable(); case URI_ITEMS_ID: return TYPE_ITEM_BASE + getModelTable(); default: throw new IllegalArgumentException("Unsupported URI: " + uri); } } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { final Cursor cursor; final int uriId = uriMatcher.match(uri); switch (uriId) { case URI_ITEMS: cursor = queryItems(uri, projection, selection, selectionArgs, sortOrder); break; case URI_ITEMS_ID: cursor = queryItem(uri, projection, selection, selectionArgs, sortOrder); break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } final Context context = getContext(); if (context != null) { cursor.setNotificationUri(context.getContentResolver(), uri); } cursor.moveToFirst(); return cursor; } @Override public Uri insert(Uri uri, ContentValues values) { final String serverId = values.getAsString(getIdColumn().getName()); if (StringUtils.isEmpty(serverId)) { throw new IllegalArgumentException("Server Id cannot be empty."); } final SQLiteDatabase database = getDatabase(); final int uriId = uriMatcher.match(uri); switch (uriId) { case URI_ITEMS: try { database.beginTransaction(); final Map<String, Object> extras = new HashMap<>(); onBeforeInsertItem(uri, values, serverId, extras); insertItem(uri, values, serverId); onAfterInsertItem(uri, values, serverId, extras); database.setTransactionSuccessful(); } finally { database.endTransaction(); } break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } ProviderUtils.notifyChangeIfNecessary(getContext(), uri); ProviderUtils.notifyUris(getContext(), getOtherUrisToNotify()); api.sync(); return Uri.withAppendedPath(uri, serverId); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int count; final SQLiteDatabase database = getDatabase(); final int uriId = uriMatcher.match(uri); switch (uriId) { case URI_ITEMS: try { database.beginTransaction(); final Map<String, Object> extras = new HashMap<>(); onBeforeUpdateItems(uri, values, selection, selectionArgs, extras); count = updateItems(uri, values, selection, selectionArgs); onAfterUpdateItems(uri, values, selection, selectionArgs, extras); database.setTransactionSuccessful(); } finally { database.endTransaction(); } break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } ProviderUtils.notifyChangeIfNecessary(getContext(), uri); ProviderUtils.notifyUris(getContext(), getOtherUrisToNotify()); api.sync(); return count; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int count; final SQLiteDatabase database = getDatabase(); final int uriId = uriMatcher.match(uri); final ModelState modelState; switch (uriId) { case URI_ITEMS: final String deleteMode = uri.getQueryParameter(ProviderUtils.QueryParameterKey.DELETE_MODE.getKeyName()); if (TextUtils.isEmpty(deleteMode)) { throw new IllegalArgumentException("Uri " + uri + " must have query parameter " + ProviderUtils.QueryParameterKey.DELETE_MODE.getKeyName()); } switch (deleteMode) { case "delete": modelState = ModelState.DeletedUndo; break; case "undo": modelState = ModelState.Normal; break; case "commit": modelState = ModelState.Deleted; break; default: throw new IllegalArgumentException(ProviderUtils.QueryParameterKey.DELETE_MODE.getKeyName() + "=" + deleteMode + " is not supported."); } try { database.beginTransaction(); final Map<String, Object> extras = new HashMap<>(); onBeforeDeleteItems(uri, selection, selectionArgs, modelState, extras); count = deleteItems(uri, selection, selectionArgs, modelState); onAfterDeleteItems(uri, selection, selectionArgs, modelState, extras); database.setTransactionSuccessful(); } finally { database.endTransaction(); } break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } ProviderUtils.notifyChangeIfNecessary(getContext(), uri); ProviderUtils.notifyUris(getContext(), getOtherUrisToNotify()); if (modelState != ModelState.DeletedUndo) { api.sync(); } return count; } @Override public int bulkInsert(Uri uri, @SuppressWarnings("NullableProblems") ContentValues[] valuesArray) { int count; final SQLiteDatabase database = getDatabase(); final int uriId = uriMatcher.match(uri); switch (uriId) { case URI_ITEMS: try { database.beginTransaction(); final Map<String, Object> extras = new HashMap<>(); onBeforeBulkInsertItems(uri, valuesArray, extras); count = bulkInsertItems(uri, valuesArray, extras); onAfterBulkInsertItems(uri, valuesArray, extras); database.setTransactionSuccessful(); } finally { database.endTransaction(); } break; default: throw new IllegalArgumentException("Unsupported URI: " + uri); } ProviderUtils.notifyChangeIfNecessary(getContext(), uri); ProviderUtils.notifyUris(getContext(), getOtherUrisToNotify()); return count; } protected abstract String getModelTable(); protected abstract String getQueryTables(Uri uri); protected abstract Column getIdColumn(); protected Cursor queryItems(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(getQueryTables(uri)); final SQLiteDatabase database = getDatabase(); return qb.query(database, projection, selection, selectionArgs, null, null, sortOrder); } protected Cursor queryItem(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); qb.setTables(getQueryTables(uri)); qb.appendWhere(getIdColumn() + "='" + uri.getPathSegments().get(1) + "'"); final SQLiteDatabase database = getDatabase(); return qb.query(database, projection, selection, selectionArgs, null, null, sortOrder); } protected void onBeforeInsertItem(Uri uri, ContentValues values, String serverId, Map<String, Object> outExtras) { } protected long insertItem(Uri uri, ContentValues values, String serverId) { if (values.containsKey(BaseColumns._ID)) { values.put(getModelTable() + "_" + Tables.SUFFIX_SYNC_STATE, SyncState.LocalChanges.asInt()); } else { values.put(getModelTable() + "_" + Tables.SUFFIX_SYNC_STATE, SyncState.None.asInt()); } final SQLiteDatabase database = getDatabase(); return ProviderUtils.doUpdateOrInsert(database, getModelTable(), values, true); } protected void onAfterInsertItem(Uri uri, ContentValues values, String serverId, Map<String, Object> extras) { } protected void onBeforeUpdateItems(Uri uri, ContentValues values, String selection, String[] selectionArgs, Map<String, Object> outExtras) { } protected int updateItems(Uri uri, ContentValues values, String selection, String[] selectionArgs) { values.put(getModelTable() + "_" + Tables.SUFFIX_SYNC_STATE, SyncState.LocalChanges.asInt()); return getDatabase().update(getModelTable(), values, selection, selectionArgs); } protected void onAfterUpdateItems(Uri uri, ContentValues values, String selection, String[] selectionArgs, Map<String, Object> extras) { } protected void onBeforeDeleteItems(Uri uri, String selection, String[] selectionArgs, ModelState modelState, Map<String, Object> outExtras) { } protected int deleteItems(Uri uri, String selection, String[] selectionArgs, ModelState modelState) { final ContentValues values = new ContentValues(); values.put(getModelTable() + "_" + Tables.SUFFIX_MODEL_STATE, modelState.asInt()); if (modelState == ModelState.Deleted) { values.put(getModelTable() + "_" + Tables.SUFFIX_SYNC_STATE, SyncState.LocalChanges.asInt()); } final String whereClause; final String[] whereArgs; if (modelState == ModelState.DeletedUndo) { whereClause = selection; whereArgs = selectionArgs; } else { whereClause = getModelTable() + "_" + Tables.SUFFIX_MODEL_STATE + "=?"; whereArgs = new String[]{String.valueOf(ModelState.DeletedUndo.asInt())}; } return getDatabase().update(getModelTable(), values, whereClause, whereArgs); } protected void onAfterDeleteItems(Uri uri, String selection, String[] selectionArgs, ModelState modelState, Map<String, Object> extras) { } protected void onBeforeBulkInsertItems(Uri uri, ContentValues[] valuesArray, Map<String, Object> outExtras) { } protected int bulkInsertItems(Uri uri, ContentValues[] valuesArray, Map<String, Object> extras) { int count = 0; final SQLiteDatabase database = getDatabase(); final String tableName = getModelTable(); for (final ContentValues values : valuesArray) { onBeforeBulkInsertIteration(uri, values, extras); ProviderUtils.doUpdateOrInsert(database, tableName, values, false); onAfterBulkInsertIteration(uri, values, extras); count++; } return count; } protected void onBeforeBulkInsertIteration(Uri uri, ContentValues values, Map<String, Object> extras) { } protected void onAfterBulkInsertIteration(Uri uri, ContentValues values, Map<String, Object> extras) { } protected void onAfterBulkInsertItems(Uri uri, ContentValues[] valuesArray, Map<String, Object> extras) { } protected Uri[] getOtherUrisToNotify() { return null; } protected List<String> getIdList(Column serverIdColumn, String selection, String[] selectionArgs) { final List<String> affectedIds = new ArrayList<>(); final Query query = Query.create().projection(serverIdColumn.getName()); if (!TextUtils.isEmpty(selection)) { query.selection(selection); } if (selectionArgs != null && selectionArgs.length > 0) { query.args(selectionArgs); } final Cursor cursor = query.from(getDatabase(), serverIdColumn.getTableName()).execute(); try { if (cursor != null && cursor.moveToFirst()) { do { int iServerId = cursor.getColumnIndex(serverIdColumn.getName()); affectedIds.add(cursor.getString(iServerId)); } while (cursor.moveToNext()); } } finally { IOUtils.closeQuietly(cursor); } return affectedIds; } protected Uri uriForDeleteFromItemState(Uri uri, ModelState modelState) { final String deleteMode; switch (modelState) { case Normal: deleteMode = "undo"; break; case DeletedUndo: deleteMode = "delete"; break; case Deleted: deleteMode = "commit"; break; default: throw new IllegalArgumentException("ModelState " + modelState + " is not supported for delete."); } return ProviderUtils.withQueryParameter(uri, ProviderUtils.QueryParameterKey.DELETE_MODE, deleteMode); } }