package org.fdroid.fdroid.data; import android.annotation.TargetApi; import android.content.ContentProvider; import android.content.ContentProviderOperation; import android.content.ContentProviderResult; import android.content.ContentValues; import android.content.Context; import android.content.OperationApplicationException; import android.content.UriMatcher; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Build; import android.support.annotation.NonNull; import org.fdroid.fdroid.Utils; import java.util.ArrayList; import java.util.HashSet; import java.util.Map; import java.util.Set; public abstract class FDroidProvider extends ContentProvider { private static final String TAG = "FDroidProvider"; static final String AUTHORITY = "org.fdroid.fdroid.data"; static final int CODE_LIST = 1; static final int CODE_SINGLE = 2; private static DBHelper dbHelper; private boolean isApplyingBatch; protected abstract String getTableName(); protected abstract String getProviderName(); /** * Should always be the same as the provider:name in the AndroidManifest */ public final String getName() { return AUTHORITY + "." + getProviderName(); } /** * Tells us if we are in the middle of a batch of operations. Allows us to * decide not to notify the content resolver of changes, * every single time we do something during many operations. * Based on http://stackoverflow.com/a/15886915. */ protected final boolean isApplyingBatch() { return this.isApplyingBatch; } @NonNull @Override public ContentProviderResult[] applyBatch(@NonNull ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { ContentProviderResult[] result = null; isApplyingBatch = true; final SQLiteDatabase db = db(); db.beginTransaction(); try { result = super.applyBatch(operations); db.setTransactionSuccessful(); } finally { db.endTransaction(); isApplyingBatch = false; } return result; } /** * Only used for testing. Not quite sure how to mock a singleton variable like this. */ public static void clearDbHelperSingleton() { dbHelper = null; } private static synchronized DBHelper getOrCreateDb(Context context) { if (dbHelper == null) { Utils.debugLog(TAG, "First time accessing database, creating new helper"); dbHelper = new DBHelper(context); } return dbHelper; } @Override public boolean onCreate() { return true; } protected final synchronized SQLiteDatabase db() { return getOrCreateDb(getContext().getApplicationContext()).getWritableDatabase(); } @Override public String getType(@NonNull Uri uri) { String type; switch (getMatcher().match(uri)) { case CODE_LIST: type = "dir"; break; case CODE_SINGLE: default: type = "item"; break; } return "vnd.android.cursor." + type + "/vnd." + AUTHORITY + "." + getProviderName(); } protected abstract UriMatcher getMatcher(); protected static String generateQuestionMarksForInClause(int num) { StringBuilder sb = new StringBuilder(num * 2); for (int i = 0; i < num; i++) { if (i != 0) { sb.append(','); } sb.append('?'); } return sb.toString(); } @TargetApi(11) private Set<String> getKeySet(ContentValues values) { if (Build.VERSION.SDK_INT >= 11) { return values.keySet(); } Set<String> keySet = new HashSet<>(); for (Map.Entry<String, Object> item : values.valueSet()) { String key = item.getKey(); keySet.add(key); } return keySet; } protected void validateFields(String[] validFields, ContentValues values) throws IllegalArgumentException { for (final String key : getKeySet(values)) { boolean isValid = false; for (final String validKey : validFields) { if (validKey.equals(key)) { isValid = true; break; } } if (!isValid) { throw new IllegalArgumentException( "Cannot save field '" + key + "' to provider " + getProviderName()); } } } /** * Helper function to be used when you need to know the primary key from the package table * when all you have is the package name. */ protected static String getPackageIdFromPackageNameQuery() { return "SELECT " + Schema.PackageTable.Cols.ROW_ID + " FROM " + Schema.PackageTable.NAME + " WHERE " + Schema.PackageTable.Cols.PACKAGE_NAME + " = ?"; } }