diff --git a/.gitignore b/.gitignore index d5ca108..7e182c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - # built application files *.apk *.ap_ @@ -30,4 +29,4 @@ proguard/ .idea/ # Gradle files -build/ +build/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..434a389 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "AndroidSerialSQL"] + path = AndroidSerialSQL + url = git@github.com:emil10001/AndroidSerialSQL.git diff --git a/.gradle/1.7/taskArtifacts/cache.properties b/.gradle/1.7/taskArtifacts/cache.properties new file mode 100644 index 0000000..9f2eed0 --- /dev/null +++ b/.gradle/1.7/taskArtifacts/cache.properties @@ -0,0 +1 @@ +#Mon Sep 02 19:48:57 PDT 2013 diff --git a/.gradle/1.7/taskArtifacts/cache.properties.lock b/.gradle/1.7/taskArtifacts/cache.properties.lock new file mode 100644 index 0000000..40fdece --- /dev/null +++ b/.gradle/1.7/taskArtifacts/cache.properties.lock @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.gradle/1.7/taskArtifacts/fileHashes.bin b/.gradle/1.7/taskArtifacts/fileHashes.bin new file mode 100644 index 0000000..745959f Binary files /dev/null and b/.gradle/1.7/taskArtifacts/fileHashes.bin differ diff --git a/.gradle/1.7/taskArtifacts/fileSnapshots.bin b/.gradle/1.7/taskArtifacts/fileSnapshots.bin new file mode 100644 index 0000000..e6b1db5 Binary files /dev/null and b/.gradle/1.7/taskArtifacts/fileSnapshots.bin differ diff --git a/.gradle/1.7/taskArtifacts/outputFileStates.bin b/.gradle/1.7/taskArtifacts/outputFileStates.bin new file mode 100644 index 0000000..36e04e1 Binary files /dev/null and b/.gradle/1.7/taskArtifacts/outputFileStates.bin differ diff --git a/.gradle/1.7/taskArtifacts/taskArtifacts.bin b/.gradle/1.7/taskArtifacts/taskArtifacts.bin new file mode 100644 index 0000000..c90bfc8 Binary files /dev/null and b/.gradle/1.7/taskArtifacts/taskArtifacts.bin differ diff --git a/AndroidSerialSQL b/AndroidSerialSQL new file mode 160000 index 0000000..f953881 --- /dev/null +++ b/AndroidSerialSQL @@ -0,0 +1 @@ +Subproject commit f953881df56bd3ed6ef35ec630c07999ca6e9ace diff --git a/README.md b/README.md index ae5ef0e..e1c418f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,99 @@ # AndroidSerialSQL -## WARNING - DO NOT USE! +This project intends to solve the problem of concurrent write attempts from different threads to an Android SQLite database. -This project has not been tested, and probably doesn't do anything useful! \ No newline at end of file +## The Problem + +Here's a [blog post explaining the issue](http://touchlabblog.tumblr.com/post/24474398246/android-sqlite-locking), along with a choice quote: + +> If you try to write to the database from actual distinct connections at the same time, one will fail. It will not wait till the first is done and then write. It will simply not write your change. Worse, if you don’t call the right version of insert/update on the SQLiteDatabase, you won’t get an exception. You’ll just get a message in your LogCat, and that will be it. + +This goes farther than the singleton pattern of only getting one database object (or one writable database object) and implements a blocking queue with a thread pool executor, where the thread pool has a max size of one. This means that there will be a single thread that handles database write operations, and it will work through the backlog of requests that exists in the queue. + +In order to accomplish this, we cut off access to a writable version of the database outside of a couple abstract runnables, which are intended to be added to the queue. `WriterTask` and `UpgradeRunnable` are those specialized runnables, both of them hold a reference to a database, and have ways of grabbing a reference to the writable db. There are a couple of data structures dedicated to handling the database (or databases), and these special runnables. + +## Using this lib + +This is a standard Android library project, so if you're using Eclipse, or are familiar with using Android library projects, just do what you normally do. I should probably turn this into a jar at some point, but I'm lazy, and may not get around to it. Plus, if I did that, I'd want to make sure that it was polished enough to submit to Maven Central and all that jazz, but that's not where things are right now. + +**Use at your own risk!** Right now, this is more of a good starting point, and something to look at as a reference. Don't pull it into your project unless you plan on forking it and fixing problems as they appear. + +If you're using Gradle, you can do the following in the parent directory, or wherever you want to put your libraries: + + git submodule add git@github.com:emil10001/AndroidSerialSQL.git + +In your project's `settings.gradle`: + + include ':YourApp', ':AndroidSerialSQL' + +In your app's `build.gradle`: + + dependencies { + compile project(':AndroidSerialSQL') + } + +## Usage + +There are a few steps to get this up and running. It should all be fairly straght-forward. + +### 1 + +Create a defenition of your database. + + DefineDB myDB = new DefineDB("myDB", 1); + myDB.setTableDefenition("items", + "create table items " + + "( _id integer primary key autoincrement, " + + "item text);"); + +### 2 + +Use the defenition to open/create the database, and store it in a data structure for use. + + AccessDB.addDB(context, myDB); + +### 3 + +Insert an item into your database. + + AccessDB.addWriteTask(new WriterTask("myDB", callback) { + @Override + public void run() { + db.beginTransaction(); + try { + ContentValues values = new ContentValues(); + values.put("item", "five"); + db.insert(ITEMS, null, values); + db.setTransactionSuccessful(); + } catch (Exception ex) { + Log.e(TAG, "failed to insert", ex); + } finally { + db.endTransaction(); + } + callback.run(); + } + }); + +### 4 + +Retrieve things from the database. + + AccessDB.getReadableDB("myDB").query("items", null, + null, null, null, null, null); + +### 5 + +Handle upgrades by adding to the database defenition. + + myDB.setVersionUpgrade(2, new UpgradeRunnable() { + @Override + public void run() { + db.execSQL("create table two" + + "( _id integer primary key autoincrement, " + + "different_thing text);"); + } + }); + +## Sample implementation + +This is the sample branch for a working app that implements the library. diff --git a/SqlSample/build.gradle b/SqlSample/build.gradle new file mode 100644 index 0000000..277cd97 --- /dev/null +++ b/SqlSample/build.gradle @@ -0,0 +1,28 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:0.5.+' + } +} +apply plugin: 'android' + +repositories { + mavenCentral() +} + +android { + compileSdkVersion 18 + buildToolsVersion "18.0.1" + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 18 + } +} + +dependencies { + compile 'com.android.support:support-v4:18.0.0' + compile project(':AndroidSerialSQL') +} diff --git a/SqlSample/src/main/AndroidManifest.xml b/SqlSample/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bb2e48e --- /dev/null +++ b/SqlSample/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/SqlSample/src/main/ic_launcher-web.png b/SqlSample/src/main/ic_launcher-web.png new file mode 100644 index 0000000..4b14629 Binary files /dev/null and b/SqlSample/src/main/ic_launcher-web.png differ diff --git a/SqlSample/src/main/java/com/feigdev/sqlsample/DumbLoader.java b/SqlSample/src/main/java/com/feigdev/sqlsample/DumbLoader.java new file mode 100644 index 0000000..4c17158 --- /dev/null +++ b/SqlSample/src/main/java/com/feigdev/sqlsample/DumbLoader.java @@ -0,0 +1,25 @@ +package com.feigdev.sqlsample; + +import android.content.Context; +import android.database.Cursor; +import android.support.v4.content.CursorLoader; +import android.util.Log; + +/** + * Created by ejohn on 9/4/13. + */ +public class DumbLoader extends CursorLoader { + private static final String TAG = "DumbLoader"; + + public DumbLoader(Context context) { + super(context); + } + + @Override + public Cursor loadInBackground() { + Cursor c = DummyData.getItems(); + while (c.moveToNext()) + Log.d(TAG, "item: " + c.getInt(0) + " - " + c.getString(1)); + return c; + } +} diff --git a/SqlSample/src/main/java/com/feigdev/sqlsample/DummyAdapter.java b/SqlSample/src/main/java/com/feigdev/sqlsample/DummyAdapter.java new file mode 100644 index 0000000..741206b --- /dev/null +++ b/SqlSample/src/main/java/com/feigdev/sqlsample/DummyAdapter.java @@ -0,0 +1,16 @@ +package com.feigdev.sqlsample; + +import android.content.Context; +import android.database.Cursor; +import android.widget.SimpleCursorAdapter; + +/** + * Created by ejohn on 9/2/13. + */ +public class DummyAdapter extends SimpleCursorAdapter { + + public DummyAdapter(Context context, Cursor c) { + super(context, R.layout.list_item, c, new String[]{"_id", "item"}, new int[]{R.id.tv1, R.id.tv2}, 0); + } + +} diff --git a/SqlSample/src/main/java/com/feigdev/sqlsample/DummyData.java b/SqlSample/src/main/java/com/feigdev/sqlsample/DummyData.java new file mode 100644 index 0000000..6019d63 --- /dev/null +++ b/SqlSample/src/main/java/com/feigdev/sqlsample/DummyData.java @@ -0,0 +1,145 @@ +package com.feigdev.sqlsample; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.util.Log; + +import com.feigdev.androidserialsql.AccessDB; +import com.feigdev.androidserialsql.DefineDB; +import com.feigdev.androidserialsql.WriterTask; + +/** + * Created by ejohn on 9/2/13. + */ +public class DummyData { + private static final String TAG = "DummyData"; + private static final String DB_NAME = "DummyData"; + private static final int VERSION = 1; + + private static final String ITEMS = "items"; + private static final String ITEMS_TABLE_DEFINITION = "create table " + + ITEMS + "( _id integer primary key autoincrement, item text);"; + private final DefineDB myDB = new DefineDB(DB_NAME, VERSION); + + // uncomment to test upgrade +// private static final int VERSION = 2; +// private static final String TABLE_TWO = "two"; +// private static final String TABLE_TWO_DEFINITION = "create table " +// + TABLE_TWO + "( _id integer primary key autoincrement, item text);"; + + public DummyData(Context c, Runnable callback) { + myDB.setTableDefenition(ITEMS, ITEMS_TABLE_DEFINITION); + + // uncomment to test upgrade +// myDB.setTableDefenition(TABLE_TWO, TABLE_TWO_DEFINITION); +// myDB.setVersionUpgrade(VERSION, new UpgradeRunnable() { +// @Override +// public void run() { +// db.execSQL(TABLE_TWO_DEFINITION); +// Log.d(TAG,"onUpgrade"); +// } +// }); + + AccessDB.addDB(c, myDB); + AccessDB.addWriteTask(new WriterTask(DB_NAME, null) { + + @Override + public void run() { + db.beginTransaction(); + try { + for (int i = 0; i < 5; i++) { + String val; + switch (i) { + case 0: + val = "zero"; + break; + case 1: + val = "one"; + break; + case 2: + val = "two"; + break; + case 3: + val = "three"; + break; + case 4: + val = "four"; + break; + default: + val = "broken"; + break; + } + ContentValues values = new ContentValues(); + values.put("_id", i); + values.put("item", val); + Log.d(TAG, "insert " + values.toString()); + db.insert(ITEMS, null, values); + } + db.setTransactionSuccessful(); + } catch (Exception ex) { + Log.e(TAG, "failed to insert", ex); + } finally { + db.endTransaction(); + } + } + }); + AccessDB.addWriteTask(new WriterTask(DB_NAME, callback) { + + @Override + public void run() { + db.beginTransaction(); + try { + ContentValues values = new ContentValues(); + values.put("_id", 5); + values.put("item", "five"); + db.insert(ITEMS, null, values); + db.setTransactionSuccessful(); + } catch (Exception ex) { + Log.e(TAG, "failed to insert", ex); + } finally { + db.endTransaction(); + } + callback.run(); + } + }); + + // uncomment to test upgrade +// AccessDB.addWriteTask(new WriterTask(DB_NAME, callback) { +// +// @Override +// public void run() { +// db.beginTransaction(); +// try { +// ContentValues values = new ContentValues(); +// values.put("_id", 0); +// values.put("item", "zero"); +// db.insert(TABLE_TWO, null, values); +// db.setTransactionSuccessful(); +// } catch (Exception ex) { +// Log.e(TAG, "failed to insert", ex); +// } finally { +// db.endTransaction(); +// } +// callback.run(); +// } +// }); + + } + + static SQLiteDatabase getDB() { + return AccessDB.getReadableDB(DB_NAME); + } + + static Cursor getItems() { + return AccessDB.getReadableDB(DB_NAME).query(ITEMS, null, null, null, null, null, null); + } + + // uncomment to test upgrade +// Cursor getItemsTwo() { +// return AccessDB.getReadableDB(DB_NAME).query(TABLE_TWO, null, null, null, null, null, null); +// } + + +} diff --git a/SqlSample/src/main/java/com/feigdev/sqlsample/MainActivity.java b/SqlSample/src/main/java/com/feigdev/sqlsample/MainActivity.java new file mode 100644 index 0000000..cbe7267 --- /dev/null +++ b/SqlSample/src/main/java/com/feigdev/sqlsample/MainActivity.java @@ -0,0 +1,60 @@ +package com.feigdev.sqlsample; + +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.Loader; +import android.view.Menu; +import android.widget.ListView; +import android.widget.SimpleCursorAdapter; + +public class MainActivity extends FragmentActivity implements LoaderManager.LoaderCallbacks { + private static final String TAG = "MainActivity"; + private static final int LOADER_ID = 1932; + private Handler handler = new Handler(); + private ListView lv; + private SimpleCursorAdapter la; + private DummyData dd; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + lv = (ListView) findViewById(R.id.list); + Runnable callback = new Runnable() { + @Override + public void run() { + getSupportLoaderManager().getLoader(LOADER_ID).forceLoad(); + } + }; + + dd = new DummyData(this, callback); + la = new DummyAdapter(this, null); + getSupportLoaderManager().initLoader(LOADER_ID, null, this); + lv.setAdapter(la); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + return new DumbLoader(this); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + la.swapCursor(cursor); + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + la.swapCursor(null); + } +} diff --git a/SqlSample/src/main/res/drawable-hdpi/ic_launcher.png b/SqlSample/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..c1b44cc Binary files /dev/null and b/SqlSample/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/SqlSample/src/main/res/drawable-mdpi/ic_launcher.png b/SqlSample/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..a1bf709 Binary files /dev/null and b/SqlSample/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/SqlSample/src/main/res/drawable-xhdpi/ic_launcher.png b/SqlSample/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..3b5ae5b Binary files /dev/null and b/SqlSample/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/SqlSample/src/main/res/drawable-xxhdpi/ic_launcher.png b/SqlSample/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..b7df51b Binary files /dev/null and b/SqlSample/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/SqlSample/src/main/res/layout/activity_main.xml b/SqlSample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..18f825a --- /dev/null +++ b/SqlSample/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/SqlSample/src/main/res/layout/list_item.xml b/SqlSample/src/main/res/layout/list_item.xml new file mode 100644 index 0000000..7f0e7f3 --- /dev/null +++ b/SqlSample/src/main/res/layout/list_item.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/SqlSample/src/main/res/menu/main.xml b/SqlSample/src/main/res/menu/main.xml new file mode 100644 index 0000000..f3b10b6 --- /dev/null +++ b/SqlSample/src/main/res/menu/main.xml @@ -0,0 +1,6 @@ + + + diff --git a/SqlSample/src/main/res/values-sw600dp/dimens.xml b/SqlSample/src/main/res/values-sw600dp/dimens.xml new file mode 100755 index 0000000..886b05f --- /dev/null +++ b/SqlSample/src/main/res/values-sw600dp/dimens.xml @@ -0,0 +1,4 @@ + + + diff --git a/SqlSample/src/main/res/values-sw720dp-land/dimens.xml b/SqlSample/src/main/res/values-sw720dp-land/dimens.xml new file mode 100755 index 0000000..00059fc --- /dev/null +++ b/SqlSample/src/main/res/values-sw720dp-land/dimens.xml @@ -0,0 +1,5 @@ + + + 128dp + diff --git a/SqlSample/src/main/res/values-v14/styles.xml b/SqlSample/src/main/res/values-v14/styles.xml new file mode 100644 index 0000000..a91fd03 --- /dev/null +++ b/SqlSample/src/main/res/values-v14/styles.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/SqlSample/src/main/res/values/dimens.xml b/SqlSample/src/main/res/values/dimens.xml new file mode 100755 index 0000000..47c8224 --- /dev/null +++ b/SqlSample/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + diff --git a/SqlSample/src/main/res/values/strings.xml b/SqlSample/src/main/res/values/strings.xml new file mode 100644 index 0000000..5c9508e --- /dev/null +++ b/SqlSample/src/main/res/values/strings.xml @@ -0,0 +1,8 @@ + + + + SqlSample + Settings + Hello world! + + diff --git a/src/main/res/values/styles.xml b/SqlSample/src/main/res/values/styles.xml similarity index 91% rename from src/main/res/values/styles.xml rename to SqlSample/src/main/res/values/styles.xml index bf65b14..6ce89c7 100644 --- a/src/main/res/values/styles.xml +++ b/SqlSample/src/main/res/values/styles.xml @@ -4,7 +4,7 @@ Base application theme, dependent on API level. This theme is replaced by AppBaseTheme from res/values-vXX/styles.xml on newer devices. --> -