Skip to content

Commit

Permalink
Integration of pre-populated database support for Android & iOS (ref: #…
Browse files Browse the repository at this point in the history
…10/#172)

Updates to readme to remove support for SQLCipher from this project
  • Loading branch information
Chris Brody committed Feb 17, 2015
1 parent 9669bfc commit 070a0d7
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 33 deletions.
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ License for iOS version: MIT only
## Status

- Please use the [Cordova-SQLitePlugin forum](http://groups.google.com/group/Cordova-SQLitePlugin) or [raise a new issue](https://github.com/brodysoft/Cordova-SQLitePlugin/issues/new) for community support
- Commercial support is available for SQLCipher integration with Android & iOS versions
- SQLCipher integration is not supported by this project, will be supported in a separate project.

## Announcements

- New `openDatabase` and `deleteDatabase` `location` option to select database location (iOS *only*) and disable iCloud backup
- Pre-populated databases support for Android & iOS is now integrated, usage described below
- Fixes to work with PouchDB by [@nolanlawson](https://github.com/nolanlawson)

## Highlights
Expand All @@ -24,8 +25,8 @@ License for iOS version: MIT only
- As described in [this posting](http://brodyspark.blogspot.com/2012/12/cordovaphonegap-sqlite-plugins-offer.html):
- Keeps sqlite database in a user data location that is known, can be reconfigured, and iOS will be backed up by iCloud.
- No 5MB maximum, more information at: http://www.sqlite.org/limits.html
- Android & iOS working with [SQLCipher](http://sqlcipher.net) for encryption (see below)
- Android is supported back to SDK 10 (a.k.a. Gingerbread, Android 2.3.3); Support for older versions is available upon request.
- Pre-populated database option (usage described below)

## Some apps using Cordova/PhoneGap SQLitePlugin

Expand All @@ -52,16 +53,8 @@ License for iOS version: MIT only

## Other versions

- Pre-populated database support for Android & iOS: https://github.com/RikshaDriver/Cordova-PrePopulated-SQLitePlugin
- Original version for iOS (with a different API): https://github.com/davibe/Phonegap-SQLitePlugin

## Using with SQLCipher

- for Android version: [this blog posting](http://brodyspark.blogspot.com/2012/12/using-sqlcipher-for-android-with.html) & [enhancements to SQLCipher db classes for Android](http://brodyspark.blogspot.com/2012/12/enhancements-to-sqlcipher-db-classes.html)
- for iOS version: [this posting](http://brodyspark.blogspot.com/2012/12/integrating-sqlcipher-with.html)

**NOTE:** This documentation is out-of-date and to be replaced very soon.

# Usage

The idea is to emulate the HTML5 SQL API as closely as possible. The only major change is to use window.sqlitePlugin.openDatabase() (or sqlitePlugin.openDatabase()) instead of window.openDatabase(). If you see any other major change please report it, it is probably a bug.
Expand Down Expand Up @@ -92,6 +85,19 @@ function onDeviceReady() {

**NOTE:** The database file name should include the extension, if desired.

### Pre-populated database

For Android & iOS (*only*): put the database file in the `www` directory and open the database like:

```js
var db = window.sqlitePlugin.openDatabase({name: "my.db", createFromLocation: 1});
```

**IMPORTANT NOTES:**

- Put the pre-populated database file in the `www` subdirectory. This should work well with using the Cordova CLI to support both Android & iOS versions.
- The pre-populated database file name must match **exactly** the file name given in `openDatabase`. The automatic extension has been completely eliminated.

## Background processing

The threading model depens on which version is used:
Expand Down Expand Up @@ -360,10 +366,6 @@ If you have any questions about the plugin please post it to the [Cordova-SQLite

**Low priority:** issues with the API or application integration will be given lower priority until the Cordova 3.0 integration is finished for Windows Phone 8. Pull requests are very welcome for these kinds of issues.

## Professional support

Available for integration with SQLCipher.

# Unit test(s)

Unit testing is done in `test-www/`. To run the tests from *nix shell, simply do either:
Expand Down
3 changes: 3 additions & 0 deletions SQLitePlugin.coffee.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,9 @@
dblocation = if !!openargs.location then dblocations[openargs.location] else null
openargs.dblocation = dblocation || dblocations[0]
if !!openargs.createFromLocation and openargs.createFromLocation == 1
openargs.createFromResource = "1"
new SQLitePlugin openargs, okcb, errorcb
deleteDb: (first, success, error) ->
Expand Down
71 changes: 64 additions & 7 deletions src/android/org/pgsqlite/SQLitePlugin.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2013, Chris Brody
* Copyright (c) 2012-2015, Chris Brody
* Copyright (c) 2005-2010, Nitobi Software Inc.
* Copyright (c) 2010, IBM Corporation
*/
Expand Down Expand Up @@ -33,6 +33,11 @@
import org.json.JSONException;
import org.json.JSONObject;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;

public class SQLitePlugin extends CordovaPlugin {

private static final Pattern FIRST_WORD = Pattern.compile("^\\s*(\\S+)",
Expand Down Expand Up @@ -99,7 +104,7 @@ private boolean executeAndPossiblyThrow(Action action, JSONArray args, CallbackC
o = args.getJSONObject(0);
dbname = o.getString("name");
// open database and start reading its queue
this.startDatabase(dbname, cbc);
this.startDatabase(dbname, o.has("createFromResource"), cbc);
break;

case close:
Expand Down Expand Up @@ -193,7 +198,7 @@ public void onDestroy() {
// LOCAL METHODS
// --------------------------------------------------------------------------

private void startDatabase(String dbname, CallbackContext cbc) {
private void startDatabase(String dbname, boolean createFromAssets, CallbackContext cbc) {
// TODO: is it an issue that we can orphan an existing thread? What should we do here?
// If we re-use the existing DBRunner it might be in the process of closing...
DBRunner r = dbrmap.get(dbname);
Expand All @@ -205,7 +210,7 @@ private void startDatabase(String dbname, CallbackContext cbc) {
// than orphaning the old DBRunner.
cbc.success();
} else {
r = new DBRunner(dbname, cbc);
r = new DBRunner(dbname, createFromAssets, cbc);
dbrmap.put(dbname, r);
this.cordova.getThreadPool().execute(r);
}
Expand All @@ -215,7 +220,7 @@ private void startDatabase(String dbname, CallbackContext cbc) {
*
* @param dbName The name of the database file
*/
private SQLiteDatabase openDatabase(String dbname, CallbackContext cbc) throws Exception {
private SQLiteDatabase openDatabase(String dbname, boolean createFromAssets, CallbackContext cbc) throws Exception {
try {
if (this.getDatabase(dbname) != null) {
// this should not happen - should be blocked at the execute("open") level
Expand All @@ -225,6 +230,8 @@ private SQLiteDatabase openDatabase(String dbname, CallbackContext cbc) throws E

File dbfile = this.cordova.getActivity().getDatabasePath(dbname);

if (!dbfile.exists() && createFromAssets) this.createFromAssets(dbname, dbfile);

if (!dbfile.exists()) {
dbfile.getParentFile().mkdirs();
}
Expand All @@ -242,6 +249,54 @@ private SQLiteDatabase openDatabase(String dbname, CallbackContext cbc) throws E
}
}

/**
* If a prepopulated DB file exists in the assets folder it is copied to the dbPath.
* Only runs the first time the app runs.
*/
private void createFromAssets(String myDBName, File dbfile)
{
InputStream in = null;
OutputStream out = null;

try {
in = this.cordova.getActivity().getAssets().open("www/" + myDBName);
String dbPath = dbfile.getAbsolutePath();
dbPath = dbPath.substring(0, dbPath.lastIndexOf("/") + 1);

File dbPathFile = new File(dbPath);
if (!dbPathFile.exists())
dbPathFile.mkdirs();

File newDbFile = new File(dbPath + myDBName);
out = new FileOutputStream(newDbFile);

// XXX TODO: this is very primitive, other alternatives at:
// http://www.journaldev.com/861/4-ways-to-copy-file-in-java
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0)
out.write(buf, 0, len);

Log.v("info", "Copied prepopulated DB content to: " + newDbFile.getAbsolutePath());
} catch (IOException e) {
Log.v("createFromAssets", "No prepopulated DB found, Error=" + e.getMessage());
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {
}
}

if (out != null) {
try {
out.close();
} catch (IOException ignored) {
}
}
}
}

/**
* Close a database (in another thread).
*
Expand Down Expand Up @@ -763,20 +818,22 @@ private void bindPreHoneycomb(JSONObject row, String key, Cursor cursor, int i)

private class DBRunner implements Runnable {
final String dbname;
final boolean createFromAssets;
final BlockingQueue<DBQuery> q;
final CallbackContext openCbc;

SQLiteDatabase mydb;

DBRunner(final String dbname, CallbackContext cbc) {
DBRunner(final String dbname, boolean createFromAssets, CallbackContext cbc) {
this.dbname = dbname;
this.createFromAssets = createFromAssets;
this.q = new LinkedBlockingQueue<DBQuery>();
this.openCbc = cbc;
}

public void run() {
try {
this.mydb = openDatabase(dbname, this.openCbc);
this.mydb = openDatabase(dbname, this.createFromAssets, this.openCbc);
} catch (Exception e) {
Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error, stopping db thread", e);
dbrmap.remove(dbname);
Expand Down
53 changes: 41 additions & 12 deletions src/ios/SQLitePlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ -(id) getDBPath:(NSString *)dbFile at:(NSString *)atkey {
}

NSString *dbdir = [appDBPaths objectForKey:atkey];
NSString *dbPath = [NSString stringWithFormat:@"%@/%@", dbdir, dbFile];
//NSString *dbPath = [NSString stringWithFormat:@"%@/%@", dbdir, dbFile];
NSString *dbPath = [dbdir stringByAppendingPathComponent: dbFile];
return dbPath;
}

Expand All @@ -242,30 +243,38 @@ -(void)open: (CDVInvokedUrlCommand*)command
if (dbPointer != NULL) {
NSLog(@"Reusing existing database connection for db name %@", dbfilename);
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Database opened"];
}
else {
} else {
const char *name = [dbname UTF8String];
sqlite3 *db;

NSLog(@"open full db path: %@", dbname);

/* Option to create from resource (pre-populated) if db does not exist: */
if (![[NSFileManager defaultManager] fileExistsAtPath:dbname]) {
NSString *createFromResource = [options objectForKey:@"createFromResource"];
if (createFromResource != NULL)
[self createFromResource:dbfilename withDbname:dbname];
}

if (sqlite3_open(name, &db) != SQLITE_OK) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Unable to open DB"];
return;
}
else {
// Extra test for SQLCipher:
// const char *key = [@"your_key_here" UTF8String];
// if(key != NULL) sqlite3_key(db, key, strlen(key));

} else {
sqlite3_create_function(db, "regexp", 2, SQLITE_ANY, NULL, &sqlite_regexp, NULL, NULL);

// Attempt to read the SQLite master table (test for SQLCipher version):

// for SQLCipher version:
// NSString *dbkey = [options objectForKey:@"key"];
// const char *key = NULL;
// if (dbkey != NULL) key = [dbkey UTF8String];
// if (key != NULL) sqlite3_key(db, key, strlen(key));

// Attempt to read the SQLite master table [to support SQLCipher version]:
if(sqlite3_exec(db, (const char*)"SELECT count(*) FROM sqlite_master;", NULL, NULL, NULL) == SQLITE_OK) {
dbPointer = [NSValue valueWithPointer:db];
[openDBs setObject: dbPointer forKey: dbfilename];
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Database opened"];
} else {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Unable to open DB"];
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"Unable to open DB with key"];
// XXX TODO: close the db handle & [perhaps] remove from openDBs!!
}
}
Expand All @@ -284,6 +293,26 @@ -(void)open: (CDVInvokedUrlCommand*)command
// NSLog(@"open cb finished ok");
}


-(void)createFromResource:(NSString *)dbfile withDbname:(NSString *)dbname {
NSString *bundleRoot = [[NSBundle mainBundle] resourcePath];
NSString *www = [bundleRoot stringByAppendingPathComponent:@"www"];
NSString *prepopulatedDb = [www stringByAppendingPathComponent: dbfile];
// NSLog(@"Look for prepopulated DB at: %@", prepopulatedDb);

if ([[NSFileManager defaultManager] fileExistsAtPath:prepopulatedDb]) {
NSLog(@"Found prepopulated DB: %@", prepopulatedDb);
NSError *error;
BOOL success = [[NSFileManager defaultManager] copyItemAtPath:prepopulatedDb toPath:dbname error:&error];

if(success)
NSLog(@"Copied prepopulated DB content to: %@", dbname);
else
NSLog(@"Unable to copy DB file: %@", [error localizedDescription]);
}
}


-(void) close: (CDVInvokedUrlCommand*)command
{
CDVPluginResult* pluginResult = nil;
Expand Down
3 changes: 3 additions & 0 deletions www/SQLitePlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,9 @@
}
dblocation = !!openargs.location ? dblocations[openargs.location] : null;
openargs.dblocation = dblocation || dblocations[0];
if (!!openargs.createFromLocation && openargs.createFromLocation === 1) {
openargs.createFromResource = "1";
}
return new SQLitePlugin(openargs, okcb, errorcb);
}),
deleteDb: function(first, success, error) {
Expand Down

0 comments on commit 070a0d7

Please sign in to comment.