Singleton database instance to prevent locking exceptions.

Previously, all of the various subclasses of FDroidProvider would create
their own database open helper in their respective `onCreate()` methods.
This seemed to be the cause of the multiple database locked exceptions.

Various online articles/SO posts/etc helped come to this conclusion:
 * http://stackoverflow.com/a/3689883
 * http://stackoverflow.com/a/8888606
 * https://web.archive.org/web/20150709074733/http://www.dmytrodanylyk.com/pages/blog/concurrent-database.html

This should fix #265.

In the process, also did away with the two `read()` and `write()` methods
that returned a "readable" and "writeable" database respectively. It turns
out that it doesn't quite do what I originally thought. There is not much
benefit to specifying to the database helper that you want a readable/writeable
database. In fact, it is often the case that a call to `read()` would most
likely have returned the same instance that is returned by `write()`. The
semantics of them were therefore broken, and they've been replaced with
`db()`.
This commit is contained in:
Peter Serwylo 2016-01-25 08:02:46 +11:00
parent 426e3da139
commit 4957bcb812
7 changed files with 55 additions and 48 deletions

View File

@ -486,7 +486,7 @@ public class ApkProvider extends FDroidProvider {
queryBuilder.addSelection(query.getSelection()); queryBuilder.addSelection(query.getSelection());
queryBuilder.addOrderBy(sortOrder); queryBuilder.addOrderBy(sortOrder);
Cursor cursor = read().rawQuery(queryBuilder.toString(), query.getArgs()); Cursor cursor = db().rawQuery(queryBuilder.toString(), query.getArgs());
cursor.setNotificationUri(getContext().getContentResolver(), uri); cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor; return cursor;
} }
@ -507,7 +507,7 @@ public class ApkProvider extends FDroidProvider {
public Uri insert(Uri uri, ContentValues values) { public Uri insert(Uri uri, ContentValues values) {
removeRepoFields(values); removeRepoFields(values);
validateFields(DataColumns.ALL, values); validateFields(DataColumns.ALL, values);
write().insertOrThrow(getTableName(), null, values); db().insertOrThrow(getTableName(), null, values);
if (!isApplyingBatch()) { if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
} }
@ -553,7 +553,7 @@ public class ApkProvider extends FDroidProvider {
throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri);
} }
int rowsAffected = write().delete(getTableName(), query.getSelection(), query.getArgs()); int rowsAffected = db().delete(getTableName(), query.getSelection(), query.getArgs());
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
return rowsAffected; return rowsAffected;
@ -574,7 +574,7 @@ public class ApkProvider extends FDroidProvider {
QuerySelection query = new QuerySelection(where, whereArgs); QuerySelection query = new QuerySelection(where, whereArgs);
query = query.add(querySingle(uri)); query = query.add(querySingle(uri));
int numRows = write().update(getTableName(), values, query.getSelection(), query.getArgs()); int numRows = db().update(getTableName(), values, query.getSelection(), query.getArgs());
if (!isApplyingBatch()) { if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
} }

View File

@ -822,7 +822,7 @@ public class AppProvider extends FDroidProvider {
query.addFields(projection); // TODO: Make the order of addFields/addSelection not dependent on each other... query.addFields(projection); // TODO: Make the order of addFields/addSelection not dependent on each other...
query.addOrderBy(sortOrder); query.addOrderBy(sortOrder);
Cursor cursor = read().rawQuery(query.toString(), selection.getArgs()); Cursor cursor = db().rawQuery(query.toString(), selection.getArgs());
cursor.setNotificationUri(getContext().getContentResolver(), uri); cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor; return cursor;
} }
@ -842,14 +842,14 @@ public class AppProvider extends FDroidProvider {
} }
int count = write().delete(getTableName(), query.getSelection(), query.getArgs()); int count = db().delete(getTableName(), query.getSelection(), query.getArgs());
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
return count; return count;
} }
@Override @Override
public Uri insert(Uri uri, ContentValues values) { public Uri insert(Uri uri, ContentValues values) {
write().insertOrThrow(getTableName(), null, values); db().insertOrThrow(getTableName(), null, values);
if (!isApplyingBatch()) { if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
} }
@ -873,7 +873,7 @@ public class AppProvider extends FDroidProvider {
throw new UnsupportedOperationException("Update not supported for " + uri + "."); throw new UnsupportedOperationException("Update not supported for " + uri + ".");
} }
int count = write().update(getTableName(), values, query.getSelection(), query.getArgs()); int count = db().update(getTableName(), values, query.getSelection(), query.getArgs());
if (!isApplyingBatch()) { if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
} }
@ -884,7 +884,7 @@ public class AppProvider extends FDroidProvider {
updateCompatibleFlags(); updateCompatibleFlags();
updateSuggestedFromUpstream(); updateSuggestedFromUpstream();
updateSuggestedFromLatest(); updateSuggestedFromLatest();
updateIconUrls(getContext(), write(), getTableName(), getApkTableName()); updateIconUrls(getContext(), db(), getTableName(), getApkTableName());
} }
/** /**
@ -911,7 +911,7 @@ public class AppProvider extends FDroidProvider {
" FROM " + apk + " FROM " + apk +
" WHERE " + apk + ".id = " + app + ".id );"; " WHERE " + apk + ".id = " + app + ".id );";
write().execSQL(updateSql); db().execSQL(updateSql);
} }
/** /**
@ -952,7 +952,7 @@ public class AppProvider extends FDroidProvider {
" ( " + app + ".compatible = 0 OR " + apk + ".compatible = 1 ) ) " + " ( " + app + ".compatible = 0 OR " + apk + ".compatible = 1 ) ) " +
" WHERE upstreamVercode > 0 "; " WHERE upstreamVercode > 0 ";
write().execSQL(updateSql); db().execSQL(updateSql);
} }
/** /**
@ -990,7 +990,7 @@ public class AppProvider extends FDroidProvider {
" ( " + app + ".compatible = 0 OR " + apk + ".compatible = 1 ) ) " + " ( " + app + ".compatible = 0 OR " + apk + ".compatible = 1 ) ) " +
" WHERE upstreamVercode = 0 OR upstreamVercode IS NULL OR suggestedVercode IS NULL "; " WHERE upstreamVercode = 0 OR upstreamVercode IS NULL OR suggestedVercode IS NULL ";
write().execSQL(updateSql); db().execSQL(updateSql);
} }
/** /**

View File

@ -5,12 +5,15 @@ import android.content.ContentProvider;
import android.content.ContentProviderOperation; import android.content.ContentProviderOperation;
import android.content.ContentProviderResult; import android.content.ContentProviderResult;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException; import android.content.OperationApplicationException;
import android.content.UriMatcher; import android.content.UriMatcher;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import org.fdroid.fdroid.Utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
@ -18,12 +21,14 @@ import java.util.Set;
public abstract class FDroidProvider extends ContentProvider { public abstract class FDroidProvider extends ContentProvider {
private static final String TAG = "FDroidProvider";
public static final String AUTHORITY = "org.fdroid.fdroid.data"; public static final String AUTHORITY = "org.fdroid.fdroid.data";
protected static final int CODE_LIST = 1; protected static final int CODE_LIST = 1;
protected static final int CODE_SINGLE = 2; protected static final int CODE_SINGLE = 2;
private DBHelper dbHelper; private static DBHelper dbHelper;
private boolean isApplyingBatch; private boolean isApplyingBatch;
@ -53,33 +58,33 @@ public abstract class FDroidProvider extends ContentProvider {
throws OperationApplicationException { throws OperationApplicationException {
ContentProviderResult[] result = null; ContentProviderResult[] result = null;
isApplyingBatch = true; isApplyingBatch = true;
write().beginTransaction(); final SQLiteDatabase db = db();
db.beginTransaction();
try { try {
result = super.applyBatch(operations); result = super.applyBatch(operations);
write().setTransactionSuccessful(); db.setTransactionSuccessful();
} finally { } finally {
write().endTransaction(); db.endTransaction();
isApplyingBatch = false; isApplyingBatch = false;
} }
return result; return result;
} }
@Override private static synchronized DBHelper getOrCreateDb(Context context) {
public boolean onCreate() { if (dbHelper == null) {
dbHelper = new DBHelper(getContext()); Utils.debugLog(TAG, "First time accessing database, creating new helper");
return true; dbHelper = new DBHelper(context);
} }
protected final DBHelper db() {
return dbHelper; return dbHelper;
} }
protected final SQLiteDatabase read() { @Override
return db().getReadableDatabase(); public boolean onCreate() {
return true;
} }
protected final SQLiteDatabase write() { protected final synchronized SQLiteDatabase db() {
return db().getWritableDatabase(); return getOrCreateDb(getContext()).getWritableDatabase();
} }
@Override @Override

View File

@ -180,7 +180,7 @@ public class InstalledAppProvider extends FDroidProvider {
throw new UnsupportedOperationException(message); throw new UnsupportedOperationException(message);
} }
Cursor cursor = read().query(getTableName(), projection, selection.getSelection(), selection.getArgs(), null, null, sortOrder); Cursor cursor = db().query(getTableName(), projection, selection.getSelection(), selection.getArgs(), null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri); cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor; return cursor;
} }
@ -195,7 +195,7 @@ public class InstalledAppProvider extends FDroidProvider {
QuerySelection query = new QuerySelection(where, whereArgs); QuerySelection query = new QuerySelection(where, whereArgs);
query = query.add(queryApp(uri.getLastPathSegment())); query = query.add(queryApp(uri.getLastPathSegment()));
int count = write().delete(getTableName(), query.getSelection(), query.getArgs()); int count = db().delete(getTableName(), query.getSelection(), query.getArgs());
if (!isApplyingBatch()) { if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
} }
@ -210,7 +210,7 @@ public class InstalledAppProvider extends FDroidProvider {
} }
verifyVersionNameNotNull(values); verifyVersionNameNotNull(values);
write().replaceOrThrow(getTableName(), null, values); db().replaceOrThrow(getTableName(), null, values);
if (!isApplyingBatch()) { if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
} }

View File

@ -305,8 +305,7 @@ public class RepoProvider extends FDroidProvider {
throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri); throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri);
} }
Cursor cursor = read().query(getTableName(), projection, selection, Cursor cursor = db().query(getTableName(), projection, selection, selectionArgs, null, null, sortOrder);
selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri); cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor; return cursor;
} }
@ -342,7 +341,7 @@ public class RepoProvider extends FDroidProvider {
values.put(DataColumns.NAME, Repo.addressToName(address)); values.put(DataColumns.NAME, Repo.addressToName(address));
} }
long id = write().insertOrThrow(getTableName(), null, values); long id = db().insertOrThrow(getTableName(), null, values);
Utils.debugLog(TAG, "Inserted repo. Notifying provider change: '" + uri + "'."); Utils.debugLog(TAG, "Inserted repo. Notifying provider change: '" + uri + "'.");
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
return getContentUri(id); return getContentUri(id);
@ -366,7 +365,7 @@ public class RepoProvider extends FDroidProvider {
throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri); throw new UnsupportedOperationException("Invalid URI for repo content provider: " + uri);
} }
int rowsAffected = write().delete(getTableName(), where, whereArgs); int rowsAffected = db().delete(getTableName(), where, whereArgs);
Utils.debugLog(TAG, "Deleted repos. Notifying provider change: '" + uri + "'."); Utils.debugLog(TAG, "Deleted repos. Notifying provider change: '" + uri + "'.");
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
return rowsAffected; return rowsAffected;
@ -374,7 +373,7 @@ public class RepoProvider extends FDroidProvider {
@Override @Override
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
int numRows = write().update(getTableName(), values, where, whereArgs); int numRows = db().update(getTableName(), values, where, whereArgs);
Utils.debugLog(TAG, "Updated repo. Notifying provider change: '" + uri + "'."); Utils.debugLog(TAG, "Updated repo. Notifying provider change: '" + uri + "'.");
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
return numRows; return numRows;

View File

@ -3,6 +3,7 @@ package org.fdroid.fdroid.data;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.UriMatcher; import android.content.UriMatcher;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
@ -112,7 +113,7 @@ public class TempApkProvider extends ApkProvider {
throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri); throw new UnsupportedOperationException("Invalid URI for apk content provider: " + uri);
} }
int rowsAffected = write().delete(getTableName(), query.getSelection(), query.getArgs()); int rowsAffected = db().delete(getTableName(), query.getSelection(), query.getArgs());
if (!isApplyingBatch()) { if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
} }
@ -121,11 +122,12 @@ public class TempApkProvider extends ApkProvider {
} }
private void initTable() { private void initTable() {
write().execSQL("DROP TABLE IF EXISTS " + getTableName()); final SQLiteDatabase db = db();
write().execSQL("CREATE TABLE " + getTableName() + " AS SELECT * FROM " + DBHelper.TABLE_APK); db.execSQL("DROP TABLE IF EXISTS " + getTableName());
write().execSQL("CREATE INDEX IF NOT EXISTS apk_vercode on " + getTableName() + " (vercode);"); db.execSQL("CREATE TABLE " + getTableName() + " AS SELECT * FROM " + DBHelper.TABLE_APK);
write().execSQL("CREATE INDEX IF NOT EXISTS apk_id on " + getTableName() + " (id);"); db.execSQL("CREATE INDEX IF NOT EXISTS apk_vercode on " + getTableName() + " (vercode);");
write().execSQL("CREATE INDEX IF NOT EXISTS apk_compatible ON " + getTableName() + " (compatible);"); db.execSQL("CREATE INDEX IF NOT EXISTS apk_id on " + getTableName() + " (id);");
db.execSQL("CREATE INDEX IF NOT EXISTS apk_compatible ON " + getTableName() + " (compatible);");
} }
} }

View File

@ -104,7 +104,7 @@ public class TempAppProvider extends AppProvider {
throw new UnsupportedOperationException("Update not supported for " + uri + "."); throw new UnsupportedOperationException("Update not supported for " + uri + ".");
} }
int count = write().update(getTableName(), values, query.getSelection(), query.getArgs()); int count = db().update(getTableName(), values, query.getSelection(), query.getArgs());
if (!isApplyingBatch()) { if (!isApplyingBatch()) {
getContext().getContentResolver().notifyChange(uri, null); getContext().getContentResolver().notifyChange(uri, null);
} }
@ -112,15 +112,16 @@ public class TempAppProvider extends AppProvider {
} }
private void initTable() { private void initTable() {
write().execSQL("DROP TABLE IF EXISTS " + getTableName()); final SQLiteDatabase db = db();
write().execSQL("CREATE TABLE " + getTableName() + " AS SELECT * FROM " + DBHelper.TABLE_APP); db.execSQL("DROP TABLE IF EXISTS " + getTableName());
write().execSQL("CREATE INDEX IF NOT EXISTS app_id ON " + getTableName() + " (id);"); db.execSQL("CREATE TABLE " + getTableName() + " AS SELECT * FROM " + DBHelper.TABLE_APP);
write().execSQL("CREATE INDEX IF NOT EXISTS app_upstreamVercode ON " + getTableName() + " (upstreamVercode);"); db.execSQL("CREATE INDEX IF NOT EXISTS app_id ON " + getTableName() + " (id);");
write().execSQL("CREATE INDEX IF NOT EXISTS app_compatible ON " + getTableName() + " (compatible);"); db.execSQL("CREATE INDEX IF NOT EXISTS app_upstreamVercode ON " + getTableName() + " (upstreamVercode);");
db.execSQL("CREATE INDEX IF NOT EXISTS app_compatible ON " + getTableName() + " (compatible);");
} }
private void commitTable() { private void commitTable() {
final SQLiteDatabase db = write(); final SQLiteDatabase db = db();
try { try {
db.beginTransaction(); db.beginTransaction();