package droidkit.sqlite; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; import android.content.pm.ProviderInfo; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.provider.BaseColumns; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import droidkit.io.IOUtils; import droidkit.util.Dynamic; import droidkit.util.DynamicException; /** * @author Daniel Serdyukov */ public class SQLiteProvider extends ContentProvider { public static final String SCHEME = "content"; public static final String DATABASE = "application.db"; public static final String WHERE_ID_EQ = BaseColumns._ID + " = ?"; static final String GROUP_BY = "groupBy"; static final String HAVING = "having"; static final String LIMIT = "limit"; private static final int URI_MATCH_ALL = 1; private static final int URI_MATCH_ID = 2; private static final int DATABASE_VERSION = 1; private static final String MIME_DIR = "vnd.android.cursor.dir/"; private static final String MIME_ITEM = "vnd.android.cursor.item/"; private static final Map<Uri, String> TABLE_NAMES = new ConcurrentHashMap<>(); private static final Map<Uri, Uri> BASE_URIS = new ConcurrentHashMap<>(); private static final String SQLITE_SCHEMA_IMPL = "droidkit.sqlite.SQLiteSchemaImpl"; private SQLiteHelper mHelper; private static int matchUri(@NonNull Uri uri) { final List<String> pathSegments = uri.getPathSegments(); final int pathSegmentsSize = pathSegments.size(); if (pathSegmentsSize == 1) { return URI_MATCH_ALL; } else if (pathSegmentsSize == 2 && TextUtils.isDigitsOnly(pathSegments.get(1))) { return URI_MATCH_ID; } throw new SQLiteException("Unknown uri '" + uri + "'"); } @Override public void attachInfo(Context context, ProviderInfo info) { super.attachInfo(context, info); SQLite.attach(info); } @Override public boolean onCreate() { try { mHelper = new SQLiteHelper(getContext(), getDatabaseName(), getDatabaseVersion(), Dynamic.<SQLiteSchema>init(SQLITE_SCHEMA_IMPL)); } catch (DynamicException e) { throw new SQLiteException(e); } return true; } @Override public Cursor query(@NonNull Uri uri, @Nullable String[] columns, @Nullable String where, @Nullable String[] whereArgs, @Nullable String orderBy) { final int match = matchUri(uri); final String tableName = getTableName(uri); final SQLiteDatabase db = mHelper.getReadableDatabase(); final Cursor cursor; if (match == URI_MATCH_ID) { cursor = db.query(tableName, columns, BaseColumns._ID + "=?", new String[]{uri.getLastPathSegment()}, null, null, orderBy); } else { cursor = db.query(tableName, columns, where, whereArgs, uri.getQueryParameter(GROUP_BY), uri.getQueryParameter(HAVING), orderBy, uri.getQueryParameter(LIMIT)); } cursor.setNotificationUri(getContext().getContentResolver(), uri); return cursor; } @Override public String getType(@NonNull Uri uri) { if (matchUri(uri) == URI_MATCH_ID) { return MIME_ITEM + getTableName(uri); } return MIME_DIR + getTableName(uri); } @Override public Uri insert(@NonNull Uri uri, @NonNull ContentValues values) { final SQLiteDatabase db = mHelper.getWritableDatabase(); if (db.inTransaction()) { db.insert(getTableName(uri), BaseColumns._ID, values); return uri; } final int match = matchUri(uri); final long rowId = db.insert(getTableName(uri), BaseColumns._ID, values); if (match == URI_MATCH_ID) { onInsert(getBaseUri(uri), rowId); return uri; } else { onInsert(uri, rowId); return ContentUris.withAppendedId(uri, rowId); } } @Override public int delete(@NonNull Uri uri, @Nullable String where, @Nullable String[] whereArgs) { final String tableName = getTableName(uri); final SQLiteDatabase db = mHelper.getWritableDatabase(); if (db.inTransaction()) { return db.delete(tableName, where, whereArgs); } final int match = matchUri(uri); final int affectedRows; final Uri baseUri; if (match == URI_MATCH_ID) { baseUri = getBaseUri(uri); affectedRows = db.delete(tableName, WHERE_ID_EQ, new String[]{uri.getLastPathSegment()}); } else { baseUri = uri; affectedRows = db.delete(tableName, where, whereArgs); } if (affectedRows > 0) { onDelete(baseUri, affectedRows); } return affectedRows; } @Override public int update(@NonNull Uri uri, @NonNull ContentValues values, @Nullable String where, @Nullable String[] whereArgs) { final String tableName = getTableName(uri); final SQLiteDatabase db = mHelper.getWritableDatabase(); if (db.inTransaction()) { return db.update(tableName, values, where, whereArgs); } final int match = matchUri(uri); final int affectedRows; final Uri baseUri; if (match == URI_MATCH_ID) { baseUri = getBaseUri(uri); affectedRows = db.update(tableName, values, WHERE_ID_EQ, new String[]{uri.getLastPathSegment()}); } else { baseUri = uri; affectedRows = db.update(tableName, values, where, whereArgs); } if (affectedRows > 0) { onDelete(baseUri, affectedRows); } return affectedRows; } @Override public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { if (matchUri(uri) == URI_MATCH_ALL) { final SQLiteDatabase db = mHelper.getWritableDatabase(); db.beginTransactionNonExclusive(); try { final int insertedRows = super.bulkInsert(uri, values); db.setTransactionSuccessful(); if (insertedRows > 0) { onChange(uri, insertedRows); } return insertedRows; } finally { db.endTransaction(); } } throw new SQLiteException("Unable to bulkInsert into " + uri); } @Override public ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { final SQLiteDatabase db = mHelper.getWritableDatabase(); db.beginTransactionNonExclusive(); try { final int opSize = operations.size(); final ContentProviderResult[] results = new ContentProviderResult[opSize]; final Set<Uri> notifyUris = new HashSet<>(opSize); for (int i = 0; i < opSize; ++i) { final ContentProviderResult result = operations.get(i).apply(this, results, i); if (result.uri != null) { if (matchUri(result.uri) == URI_MATCH_ID) { notifyUris.add(getBaseUri(result.uri)); } else { notifyUris.add(result.uri); } } } db.setTransactionSuccessful(); for (final Uri baseUri : notifyUris) { onChange(baseUri, opSize); } return results; } finally { db.endTransaction(); } } @Nullable protected String getDatabaseName() { return DATABASE; } protected int getDatabaseVersion() { return DATABASE_VERSION; } @SuppressWarnings("unused") protected void onChange(@NonNull Uri baseUri, int affectedRows) { getContext().getContentResolver().notifyChange(baseUri, null, false); } @SuppressWarnings("unused") protected void onInsert(@NonNull Uri baseUri, long rowid) { onChange(baseUri, 1); } @SuppressWarnings("unused") protected void onUpdate(@NonNull Uri baseUri, int affectedRows) { onChange(baseUri, affectedRows); } @SuppressWarnings("unused") protected void onDelete(@NonNull Uri baseUri, int affectedRows) { onChange(baseUri, affectedRows); } void clearDatabase() { final Cursor cursor = mHelper.getReadableDatabase().rawQuery("SELECT name FROM sqlite_master" + " WHERE type='table'" + " AND name <> 'android_metadata'", null); final List<String> tables = new ArrayList<>(); try { if (cursor.moveToFirst()) { do { tables.add(cursor.getString(0)); } while (cursor.moveToNext()); } } finally { IOUtils.closeQuietly(cursor); } final SQLiteDatabase db = mHelper.getWritableDatabase(); db.beginTransaction(); try { for (final String table : tables) { db.execSQL("DELETE FROM " + table + ";"); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } @NonNull private String getTableName(@NonNull Uri uri) { String tableName = TABLE_NAMES.get(uri); if (tableName == null) { tableName = uri.getPathSegments().get(0); TABLE_NAMES.put(uri, tableName); } return tableName; } @NonNull private Uri getBaseUri(@NonNull Uri uri) { Uri baseUri = BASE_URIS.get(uri); if (baseUri == null) { baseUri = new Uri.Builder() .scheme(uri.getScheme()) .authority(uri.getAuthority()) .appendPath(getTableName(uri)) .build(); BASE_URIS.put(uri, baseUri); } return baseUri; } }