diff --git a/config.xml b/config.xml
index fc3b343..b3d0d62 100644
--- a/config.xml
+++ b/config.xml
@@ -99,4 +99,5 @@
+
diff --git a/package-lock.json b/package-lock.json
index 8fda849..744cdf7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2413,6 +2413,19 @@
"resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.3.tgz",
"integrity": "sha1-tehezbv+Wu3tQKG/TuI3LmfZb7Q="
},
+ "cordova-sqlite-storage": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/cordova-sqlite-storage/-/cordova-sqlite-storage-2.5.1.tgz",
+ "integrity": "sha512-RMZcheSs9ihxcXUEmcAg8inG0UHZ5rQKQZqLY40jFES8rpiH5/sYeqaTmnuATx5w2apGB7fFLQHWMG2qaxAVtw==",
+ "requires": {
+ "cordova-sqlite-storage-dependencies": "1.2.0"
+ }
+ },
+ "cordova-sqlite-storage-dependencies": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/cordova-sqlite-storage-dependencies/-/cordova-sqlite-storage-dependencies-1.2.0.tgz",
+ "integrity": "sha512-lJl5uJFrCWrCYYGhpSEXe6sepjgOOzbeh8fFur3LqaSRvx+xFNYtfMYumE0+xqZwSmPODzex+y6I7ixwcBn73Q=="
+ },
"core-js": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz",
@@ -4601,7 +4614,7 @@
},
"ionic": {
"version": "3.20.0",
- "resolved": "https://registry.npmjs.org/ionic/-/ionic-3.20.0.tgz",
+ "resolved": "http://registry.npmjs.org/ionic/-/ionic-3.20.0.tgz",
"integrity": "sha512-yeLPusYOSyF+VmO+Hf2a5kf2Kx4ST1f3MILM8g+9ckF/MdaoD9UzXif2/sumGem6I6RTrqo9horBmC7QJYcClA==",
"dev": true,
"requires": {
diff --git a/package.json b/package.json
index 934b178..7003882 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"cordova-plugin-splashscreen": "^5.0.2",
"cordova-plugin-telerik-imagepicker": "^2.1.8",
"cordova-plugin-whitelist": "^1.3.3",
+ "cordova-sqlite-storage": "2.5.1",
"ionic-angular": "3.9.2",
"ionicons": "4.2.4",
"moment": "^2.18.1",
@@ -72,7 +73,8 @@
"cordova-android-support-gradle-release": {
"ANDROID_SUPPORT_VERSION": "27.+"
},
- "cordova-plugin-filepath": {}
+ "cordova-plugin-filepath": {},
+ "cordova-sqlite-storage": {}
},
"platforms": [
"android"
diff --git a/platforms/android/android.json b/platforms/android/android.json
index 9c6bd98..be09320 100644
--- a/platforms/android/android.json
+++ b/platforms/android/android.json
@@ -49,6 +49,10 @@
{
"xml": "",
"count": 1
+ },
+ {
+ "xml": "",
+ "count": 1
}
]
}
@@ -135,6 +139,9 @@
},
"cordova-plugin-filepath": {
"PACKAGE_NAME": "com.monkeystew.goober_m"
+ },
+ "cordova-sqlite-storage": {
+ "PACKAGE_NAME": "com.monkeystew.goober_m"
}
},
"dependent_plugins": {},
@@ -384,6 +391,14 @@
"clobbers": [
"window.FilePath"
]
+ },
+ {
+ "id": "cordova-sqlite-storage.SQLitePlugin",
+ "file": "plugins/cordova-sqlite-storage/www/SQLitePlugin.js",
+ "pluginId": "cordova-sqlite-storage",
+ "clobbers": [
+ "SQLitePlugin"
+ ]
}
],
"plugin_metadata": {
@@ -397,6 +412,7 @@
"cordova-plugin-splashscreen": "5.0.2",
"cordova-plugin-device": "2.0.2",
"cordova-android-support-gradle-release": "1.4.4",
- "cordova-plugin-filepath": "1.4.2"
+ "cordova-plugin-filepath": "1.4.2",
+ "cordova-sqlite-storage": "2.5.1"
}
}
\ No newline at end of file
diff --git a/platforms/android/app/libs/sqlite-connector.jar b/platforms/android/app/libs/sqlite-connector.jar
new file mode 100644
index 0000000..73387a1
Binary files /dev/null and b/platforms/android/app/libs/sqlite-connector.jar differ
diff --git a/platforms/android/app/libs/sqlite-native-driver.jar b/platforms/android/app/libs/sqlite-native-driver.jar
new file mode 100644
index 0000000..b1850ad
Binary files /dev/null and b/platforms/android/app/libs/sqlite-native-driver.jar differ
diff --git a/platforms/android/app/src/main/java/io/sqlc/SQLiteAndroidDatabase.java b/platforms/android/app/src/main/java/io/sqlc/SQLiteAndroidDatabase.java
new file mode 100644
index 0000000..6540f86
--- /dev/null
+++ b/platforms/android/app/src/main/java/io/sqlc/SQLiteAndroidDatabase.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright (c) 2012-present Christopher J. Brody (aka Chris Brody)
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
+package io.sqlc;
+
+import android.annotation.SuppressLint;
+
+import android.database.Cursor;
+import android.database.CursorWindow;
+
+import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteCursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteStatement;
+
+import android.util.Log;
+
+import java.io.File;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Number;
+
+import java.util.Locale;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.cordova.CallbackContext;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Android Database helper class
+ */
+class SQLiteAndroidDatabase
+{
+ private static final Pattern FIRST_WORD = Pattern.compile("^[\\s;]*([^\\s;]+)",
+ Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern WHERE_CLAUSE = Pattern.compile("\\s+WHERE\\s+(.+)$",
+ Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern UPDATE_TABLE_NAME = Pattern.compile("^\\s*UPDATE\\s+(\\S+)",
+ Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern DELETE_TABLE_NAME = Pattern.compile("^\\s*DELETE\\s+FROM\\s+(\\S+)",
+ Pattern.CASE_INSENSITIVE);
+
+ private static final boolean isPostHoneycomb = android.os.Build.VERSION.SDK_INT >= 11;
+
+ File dbFile;
+
+ SQLiteDatabase mydb;
+
+ boolean isTransactionActive = false;
+
+ /**
+ * NOTE: Using default constructor, no explicit constructor.
+ */
+
+ /**
+ * Open a database.
+ *
+ * @param dbfile The database File specification
+ */
+ void open(File dbfile) throws Exception {
+ dbFile = dbfile; // for possible bug workaround
+ mydb = SQLiteDatabase.openOrCreateDatabase(dbfile, null);
+ }
+
+ /**
+ * Close a database (in the current thread).
+ */
+ void closeDatabaseNow() {
+ if (mydb != null) {
+ if (isTransactionActive) {
+ mydb.endTransaction();
+ isTransactionActive = false;
+ }
+ mydb.close();
+ mydb = null;
+ }
+ }
+
+ void bugWorkaround() throws Exception {
+ this.closeDatabaseNow();
+ this.open(dbFile);
+ }
+
+ /**
+ * Executes a batch request and sends the results via cbc.
+ *
+ * @param queryarr Array of query strings
+ * @param jsonparamsArr Array of JSON query parameters
+ * @param cbc Callback context from Cordova API
+ */
+ void executeSqlBatch(String[] queryarr, JSONArray[] jsonparamsArr, CallbackContext cbc) {
+
+ if (mydb == null) {
+ // not allowed - can only happen if someone has closed (and possibly deleted) a database and then re-used the database
+ // (internal plugin error)
+ cbc.error("INTERNAL PLUGIN ERROR: database not open");
+ return;
+ }
+
+ int len = queryarr.length;
+ JSONArray batchResults = new JSONArray();
+
+ for (int i = 0; i < len; i++) {
+ executeSqlBatchStatement(queryarr[i], jsonparamsArr[i], batchResults);
+ }
+
+ cbc.success(batchResults);
+ }
+
+ @SuppressLint("NewApi")
+ private void executeSqlBatchStatement(String query, JSONArray json_params, JSONArray batchResults) {
+
+ if (mydb == null) {
+ // Should not happen here
+ return;
+
+ } else {
+
+ int rowsAffectedCompat = 0;
+ boolean needRowsAffectedCompat = false;
+
+ JSONObject queryResult = null;
+
+ String errorMessage = "unknown";
+ int code = 0; // SQLException.UNKNOWN_ERR
+
+ try {
+ boolean needRawQuery = true;
+
+ //Log.v("executeSqlBatch", "get query type");
+ QueryType queryType = getQueryType(query);
+ //Log.v("executeSqlBatch", "query type: " + queryType);
+
+ if (queryType == QueryType.update || queryType == queryType.delete) {
+ if (isPostHoneycomb) {
+ SQLiteStatement myStatement = mydb.compileStatement(query);
+
+ if (json_params != null) {
+ bindArgsToStatement(myStatement, json_params);
+ }
+
+ int rowsAffected = -1; // (assuming invalid)
+
+ // Use try & catch just in case android.os.Build.VERSION.SDK_INT >= 11 is lying:
+ // (Catch SQLiteException here to avoid extra retry)
+ try {
+ rowsAffected = myStatement.executeUpdateDelete();
+ // Indicate valid results:
+ needRawQuery = false;
+ } catch (SQLiteConstraintException ex) {
+ // Indicate problem & stop this query:
+ ex.printStackTrace();
+ errorMessage = "constraint failure: " + ex.getMessage();
+ code = 6; // SQLException.CONSTRAINT_ERR
+ Log.v("executeSqlBatch", "SQLiteStatement.executeUpdateDelete(): Error=" + errorMessage);
+ needRawQuery = false;
+ } catch (SQLiteException ex) {
+ // Indicate problem & stop this query:
+ ex.printStackTrace();
+ errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "SQLiteStatement.executeUpdateDelete(): Error=" + errorMessage);
+ needRawQuery = false;
+ } catch (Exception ex) {
+ // Assuming SDK_INT was lying & method not found:
+ // do nothing here & try again with raw query.
+ ex.printStackTrace();
+ Log.v("executeSqlBatch", "SQLiteStatement.executeUpdateDelete(): runtime error (fallback to old API): " + errorMessage);
+ }
+
+ // "finally" cleanup myStatement
+ myStatement.close();
+
+ if (rowsAffected != -1) {
+ queryResult = new JSONObject();
+ queryResult.put("rowsAffected", rowsAffected);
+ }
+ }
+
+ if (needRawQuery) { // for pre-honeycomb behavior
+ rowsAffectedCompat = countRowsAffectedCompat(queryType, query, json_params, mydb);
+ needRowsAffectedCompat = true;
+ }
+ }
+
+ // INSERT:
+ if (queryType == QueryType.insert && json_params != null) {
+ needRawQuery = false;
+
+ SQLiteStatement myStatement = mydb.compileStatement(query);
+
+ bindArgsToStatement(myStatement, json_params);
+
+ long insertId = -1; // (invalid)
+
+ try {
+ insertId = myStatement.executeInsert();
+
+ // statement has finished with no constraint violation:
+ queryResult = new JSONObject();
+ if (insertId != -1) {
+ queryResult.put("insertId", insertId);
+ queryResult.put("rowsAffected", 1);
+ } else {
+ queryResult.put("rowsAffected", 0);
+ }
+ } catch (SQLiteConstraintException ex) {
+ // report constraint violation error result with the error message
+ ex.printStackTrace();
+ errorMessage = "constraint failure: " + ex.getMessage();
+ code = 6; // SQLException.CONSTRAINT_ERR
+ Log.v("executeSqlBatch", "SQLiteDatabase.executeInsert(): Error=" + errorMessage);
+ } catch (SQLiteException ex) {
+ // report some other error result with the error message
+ ex.printStackTrace();
+ errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "SQLiteDatabase.executeInsert(): Error=" + errorMessage);
+ }
+
+ // "finally" cleanup myStatement
+ myStatement.close();
+ }
+
+ if (queryType == QueryType.begin) {
+ needRawQuery = false;
+ try {
+ mydb.beginTransaction();
+ isTransactionActive = true;
+
+ queryResult = new JSONObject();
+ queryResult.put("rowsAffected", 0);
+ } catch (SQLiteException ex) {
+ ex.printStackTrace();
+ errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "SQLiteDatabase.beginTransaction(): Error=" + errorMessage);
+ }
+ }
+
+ if (queryType == QueryType.commit) {
+ needRawQuery = false;
+ try {
+ mydb.setTransactionSuccessful();
+ mydb.endTransaction();
+ isTransactionActive = false;
+
+ queryResult = new JSONObject();
+ queryResult.put("rowsAffected", 0);
+ } catch (SQLiteException ex) {
+ ex.printStackTrace();
+ errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "SQLiteDatabase.setTransactionSuccessful/endTransaction(): Error=" + errorMessage);
+ }
+ }
+
+ if (queryType == QueryType.rollback) {
+ needRawQuery = false;
+ try {
+ mydb.endTransaction();
+ isTransactionActive = false;
+
+ queryResult = new JSONObject();
+ queryResult.put("rowsAffected", 0);
+ } catch (SQLiteException ex) {
+ ex.printStackTrace();
+ errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "SQLiteDatabase.endTransaction(): Error=" + errorMessage);
+ }
+ }
+
+ // raw query for other statements:
+ if (needRawQuery) {
+ try {
+ queryResult = this.executeSqlStatementQuery(mydb, query, json_params);
+
+ } catch (SQLiteConstraintException ex) {
+ // report constraint violation error result with the error message
+ ex.printStackTrace();
+ errorMessage = "constraint failure: " + ex.getMessage();
+ code = 6; // SQLException.CONSTRAINT_ERR
+ Log.v("executeSqlBatch", "Raw query error=" + errorMessage);
+ } catch (SQLiteException ex) {
+ // report some other error result with the error message
+ ex.printStackTrace();
+ errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "Raw query error=" + errorMessage);
+ }
+
+ if (needRowsAffectedCompat) {
+ queryResult.put("rowsAffected", rowsAffectedCompat);
+ }
+ }
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "SQLiteAndroidDatabase.executeSql[Batch](): Error=" + errorMessage);
+ }
+
+ try {
+ if (queryResult != null) {
+ JSONObject r = new JSONObject();
+
+ r.put("type", "success");
+ r.put("result", queryResult);
+
+ batchResults.put(r);
+ } else {
+ JSONObject r = new JSONObject();
+ r.put("type", "error");
+
+ JSONObject er = new JSONObject();
+ er.put("message", errorMessage);
+ er.put("code", code);
+ r.put("result", er);
+
+ batchResults.put(r);
+ }
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ Log.v("executeSqlBatch", "SQLiteAndroidDatabase.executeSql[Batch](): Error=" + ex.getMessage());
+ // TODO what to do?
+ }
+ }
+ }
+
+ private final int countRowsAffectedCompat(QueryType queryType, String query, JSONArray json_params,
+ SQLiteDatabase mydb) throws JSONException {
+ // quick and dirty way to calculate the rowsAffected in pre-Honeycomb. just do a SELECT
+ // beforehand using the same WHERE clause. might not be perfect, but it's better than nothing
+ Matcher whereMatcher = WHERE_CLAUSE.matcher(query);
+
+ String where = "";
+
+ int pos = 0;
+ while (whereMatcher.find(pos)) {
+ where = " WHERE " + whereMatcher.group(1);
+ pos = whereMatcher.start(1);
+ }
+ // WHERE clause may be omitted, and also be sure to find the last one,
+ // e.g. for cases where there's a subquery
+
+ // bindings may be in the update clause, so only take the last n
+ int numQuestionMarks = 0;
+ for (int j = 0; j < where.length(); j++) {
+ if (where.charAt(j) == '?') {
+ numQuestionMarks++;
+ }
+ }
+
+ JSONArray subParams = null;
+
+ if (json_params != null) {
+ // only take the last n of every array of sqlArgs
+ JSONArray origArray = json_params;
+ subParams = new JSONArray();
+ int startPos = origArray.length() - numQuestionMarks;
+ for (int j = startPos; j < origArray.length(); j++) {
+ subParams.put(j - startPos, origArray.get(j));
+ }
+ }
+
+ if (queryType == QueryType.update) {
+ Matcher tableMatcher = UPDATE_TABLE_NAME.matcher(query);
+ if (tableMatcher.find()) {
+ String table = tableMatcher.group(1);
+ try {
+ SQLiteStatement statement = mydb.compileStatement(
+ "SELECT count(*) FROM " + table + where);
+
+ if (subParams != null) {
+ bindArgsToStatement(statement, subParams);
+ }
+
+ return (int)statement.simpleQueryForLong();
+ } catch (Exception e) {
+ // assume we couldn't count for whatever reason, keep going
+ Log.e(SQLiteAndroidDatabase.class.getSimpleName(), "uncaught", e);
+ }
+ }
+ } else { // delete
+ Matcher tableMatcher = DELETE_TABLE_NAME.matcher(query);
+ if (tableMatcher.find()) {
+ String table = tableMatcher.group(1);
+ try {
+ SQLiteStatement statement = mydb.compileStatement(
+ "SELECT count(*) FROM " + table + where);
+ bindArgsToStatement(statement, subParams);
+
+ return (int)statement.simpleQueryForLong();
+ } catch (Exception e) {
+ // assume we couldn't count for whatever reason, keep going
+ Log.e(SQLiteAndroidDatabase.class.getSimpleName(), "uncaught", e);
+
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ private void bindArgsToStatement(SQLiteStatement myStatement, JSONArray sqlArgs) throws JSONException {
+ for (int i = 0; i < sqlArgs.length(); i++) {
+ if (sqlArgs.get(i) instanceof Float || sqlArgs.get(i) instanceof Double) {
+ myStatement.bindDouble(i + 1, sqlArgs.getDouble(i));
+ } else if (sqlArgs.get(i) instanceof Number) {
+ myStatement.bindLong(i + 1, sqlArgs.getLong(i));
+ } else if (sqlArgs.isNull(i)) {
+ myStatement.bindNull(i + 1);
+ } else {
+ myStatement.bindString(i + 1, sqlArgs.getString(i));
+ }
+ }
+ }
+
+ /**
+ * Get rows results from query cursor.
+ *
+ * @param cur Cursor into query results
+ * @return results in string form
+ */
+ private JSONObject executeSqlStatementQuery(SQLiteDatabase mydb, String query,
+ JSONArray paramsAsJson) throws Exception {
+ JSONObject rowsResult = new JSONObject();
+
+ Cursor cur = null;
+ try {
+ String[] params = null;
+
+ params = new String[paramsAsJson.length()];
+
+ for (int j = 0; j < paramsAsJson.length(); j++) {
+ if (paramsAsJson.isNull(j))
+ params[j] = "";
+ else
+ params[j] = paramsAsJson.getString(j);
+ }
+
+ cur = mydb.rawQuery(query, params);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ String errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "SQLiteAndroidDatabase.executeSql[Batch](): Error=" + errorMessage);
+ throw ex;
+ }
+
+ // If query result has rows
+ if (cur != null && cur.moveToFirst()) {
+ JSONArray rowsArrayResult = new JSONArray();
+ String key = "";
+ int colCount = cur.getColumnCount();
+
+ // Build up JSON result object for each row
+ do {
+ JSONObject row = new JSONObject();
+ try {
+ for (int i = 0; i < colCount; ++i) {
+ key = cur.getColumnName(i);
+
+ if (isPostHoneycomb) {
+
+ // Use try & catch just in case android.os.Build.VERSION.SDK_INT >= 11 is lying:
+ try {
+ bindPostHoneycomb(row, key, cur, i);
+ } catch (Exception ex) {
+ bindPreHoneycomb(row, key, cur, i);
+ }
+ } else {
+ bindPreHoneycomb(row, key, cur, i);
+ }
+ }
+
+ rowsArrayResult.put(row);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ } while (cur.moveToNext());
+
+ try {
+ rowsResult.put("rows", rowsArrayResult);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (cur != null) {
+ cur.close();
+ }
+
+ return rowsResult;
+ }
+
+ @SuppressLint("NewApi")
+ private void bindPostHoneycomb(JSONObject row, String key, Cursor cur, int i) throws JSONException {
+ int curType = cur.getType(i);
+
+ switch (curType) {
+ case Cursor.FIELD_TYPE_NULL:
+ row.put(key, JSONObject.NULL);
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ row.put(key, cur.getLong(i));
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ row.put(key, cur.getDouble(i));
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ default: /* (BLOB) */
+ row.put(key, cur.getString(i));
+ break;
+ }
+ }
+
+ private void bindPreHoneycomb(JSONObject row, String key, Cursor cursor, int i) throws JSONException {
+ // Since cursor.getType() is not available pre-honeycomb, this is
+ // a workaround so we don't have to bind everything as a string
+ // Details here: http://stackoverflow.com/q/11658239
+ SQLiteCursor sqLiteCursor = (SQLiteCursor) cursor;
+ CursorWindow cursorWindow = sqLiteCursor.getWindow();
+ int pos = cursor.getPosition();
+ if (cursorWindow.isNull(pos, i)) {
+ row.put(key, JSONObject.NULL);
+ } else if (cursorWindow.isLong(pos, i)) {
+ row.put(key, cursor.getLong(i));
+ } else if (cursorWindow.isFloat(pos, i)) {
+ row.put(key, cursor.getDouble(i));
+ } else {
+ // STRING or BLOB:
+ row.put(key, cursor.getString(i));
+ }
+ }
+
+ static QueryType getQueryType(String query) {
+ Matcher matcher = FIRST_WORD.matcher(query);
+
+ // FIND & return query type, or throw:
+ if (matcher.find()) {
+ try {
+ String first = matcher.group(1);
+
+ // explictly reject if blank
+ // (needed for SQLCipher version)
+ if (first.length() == 0) throw new RuntimeException("query not found");
+
+ return QueryType.valueOf(first.toLowerCase(Locale.ENGLISH));
+ } catch (IllegalArgumentException ignore) {
+ // unknown verb (NOT blank)
+ return QueryType.other;
+ }
+ } else {
+ // explictly reject if blank
+ // (needed for SQLCipher version)
+ throw new RuntimeException("query not found");
+ }
+ }
+
+ static enum QueryType {
+ update,
+ insert,
+ delete,
+ select,
+ begin,
+ commit,
+ rollback,
+ other
+ }
+} /* vim: set expandtab : */
diff --git a/platforms/android/app/src/main/java/io/sqlc/SQLiteConnectorDatabase.java b/platforms/android/app/src/main/java/io/sqlc/SQLiteConnectorDatabase.java
new file mode 100644
index 0000000..e16e822
--- /dev/null
+++ b/platforms/android/app/src/main/java/io/sqlc/SQLiteConnectorDatabase.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (c) 2012-present Christopher J. Brody (aka Chris Brody)
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
+package io.sqlc;
+
+import android.annotation.SuppressLint;
+
+import android.util.Log;
+
+import java.io.File;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Number;
+
+import java.sql.SQLException;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.cordova.CallbackContext;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import io.liteglue.SQLCode;
+import io.liteglue.SQLColumnType;
+import io.liteglue.SQLiteConnector;
+import io.liteglue.SQLiteConnection;
+import io.liteglue.SQLiteOpenFlags;
+import io.liteglue.SQLiteStatement;
+
+/**
+ * Android SQLite-Connector Database helper class
+ */
+class SQLiteConnectorDatabase extends SQLiteAndroidDatabase
+{
+ static SQLiteConnector connector = new SQLiteConnector();
+
+ SQLiteConnection mydb;
+
+ /**
+ * NOTE: Using default constructor, no explicit constructor.
+ */
+
+
+ /**
+ * Open a database.
+ *
+ * @param dbFile The database File specification
+ */
+ @Override
+ void open(File dbFile) throws Exception {
+ mydb = connector.newSQLiteConnection(dbFile.getAbsolutePath(),
+ SQLiteOpenFlags.READWRITE | SQLiteOpenFlags.CREATE);
+ }
+
+ /**
+ * Close a database (in the current thread).
+ */
+ @Override
+ void closeDatabaseNow() {
+ try {
+ if (mydb != null)
+ mydb.dispose();
+ } catch (Exception e) {
+ Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database, ignoring", e);
+ }
+ }
+
+ /**
+ * Ignore Android bug workaround for NDK version
+ */
+ @Override
+ void bugWorkaround() { }
+
+ /**
+ * Executes a batch request and sends the results via cbc.
+ *
+ * @param dbname The name of the database.
+ * @param queryarr Array of query strings
+ * @param jsonparams Array of JSON query parameters
+ * @param cbc Callback context from Cordova API
+ */
+ @Override
+ void executeSqlBatch( String[] queryarr, JSONArray[] jsonparams, CallbackContext cbc) {
+
+ if (mydb == null) {
+ // not allowed - can only happen if someone has closed (and possibly deleted) a database and then re-used the database
+ cbc.error("database has been closed");
+ return;
+ }
+
+ int len = queryarr.length;
+ JSONArray batchResults = new JSONArray();
+
+ for (int i = 0; i < len; i++) {
+ int rowsAffectedCompat = 0;
+ boolean needRowsAffectedCompat = false;
+
+ JSONObject queryResult = null;
+
+ String errorMessage = "unknown";
+ int sqliteErrorCode = -1;
+ int code = 0; // SQLException.UNKNOWN_ERR
+
+ try {
+ String query = queryarr[i];
+
+ long lastTotal = mydb.getTotalChanges();
+ queryResult = this.executeSQLiteStatement(query, jsonparams[i], cbc);
+ long newTotal = mydb.getTotalChanges();
+ long rowsAffected = newTotal - lastTotal;
+
+ queryResult.put("rowsAffected", rowsAffected);
+ if (rowsAffected > 0) {
+ long insertId = mydb.getLastInsertRowid();
+ if (insertId > 0) {
+ queryResult.put("insertId", insertId);
+ }
+ }
+ } catch (SQLException ex) {
+ ex.printStackTrace();
+ sqliteErrorCode = ex.getErrorCode();
+ errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): SQL Error code = " + sqliteErrorCode + " message = " + errorMessage);
+
+ switch(sqliteErrorCode) {
+ case SQLCode.ERROR:
+ code = 5; // SQLException.SYNTAX_ERR
+ break;
+ case 13: // SQLITE_FULL
+ code = 4; // SQLException.QUOTA_ERR
+ break;
+ case SQLCode.CONSTRAINT:
+ code = 6; // SQLException.CONSTRAINT_ERR
+ break;
+ default:
+ /* do nothing */
+ }
+ } catch (JSONException ex) {
+ // NOT expected:
+ ex.printStackTrace();
+ errorMessage = ex.getMessage();
+ code = 0; // SQLException.UNKNOWN_ERR
+ Log.e("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): UNEXPECTED JSON Error=" + errorMessage);
+ }
+
+ try {
+ if (queryResult != null) {
+ JSONObject r = new JSONObject();
+
+ r.put("type", "success");
+ r.put("result", queryResult);
+
+ batchResults.put(r);
+ } else {
+ JSONObject r = new JSONObject();
+ r.put("type", "error");
+
+ JSONObject er = new JSONObject();
+ er.put("message", errorMessage);
+ er.put("code", code);
+ r.put("result", er);
+
+ batchResults.put(r);
+ }
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ Log.e("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): Error=" + ex.getMessage());
+ // TODO what to do?
+ }
+ }
+
+ cbc.success(batchResults);
+ }
+
+ /**
+ * Get rows results from query cursor.
+ *
+ * @param cur Cursor into query results
+ * @return results in string form
+ */
+ private JSONObject executeSQLiteStatement(String query, JSONArray paramsAsJson,
+ CallbackContext cbc) throws JSONException, SQLException {
+ JSONObject rowsResult = new JSONObject();
+
+ boolean hasRows = false;
+
+ SQLiteStatement myStatement = mydb.prepareStatement(query);
+
+ try {
+ String[] params = null;
+
+ params = new String[paramsAsJson.length()];
+
+ for (int i = 0; i < paramsAsJson.length(); ++i) {
+ if (paramsAsJson.isNull(i)) {
+ myStatement.bindNull(i + 1);
+ } else {
+ Object p = paramsAsJson.get(i);
+ if (p instanceof Float || p instanceof Double)
+ myStatement.bindDouble(i + 1, paramsAsJson.getDouble(i));
+ else if (p instanceof Number)
+ myStatement.bindLong(i + 1, paramsAsJson.getLong(i));
+ else
+ myStatement.bindTextNativeString(i + 1, paramsAsJson.getString(i));
+ }
+ }
+
+ hasRows = myStatement.step();
+ } catch (SQLException ex) {
+ ex.printStackTrace();
+ String errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): Error=" + errorMessage);
+
+ // cleanup statement and throw the exception:
+ myStatement.dispose();
+ throw ex;
+ } catch (JSONException ex) {
+ ex.printStackTrace();
+ String errorMessage = ex.getMessage();
+ Log.v("executeSqlBatch", "SQLitePlugin.executeSql[Batch](): Error=" + errorMessage);
+
+ // cleanup statement and throw the exception:
+ myStatement.dispose();
+ throw ex;
+ }
+
+ // If query result has rows
+ if (hasRows) {
+ JSONArray rowsArrayResult = new JSONArray();
+ String key = "";
+ int colCount = myStatement.getColumnCount();
+
+ // Build up JSON result object for each row
+ do {
+ JSONObject row = new JSONObject();
+ try {
+ for (int i = 0; i < colCount; ++i) {
+ key = myStatement.getColumnName(i);
+
+ switch (myStatement.getColumnType(i)) {
+ case SQLColumnType.NULL:
+ row.put(key, JSONObject.NULL);
+ break;
+
+ case SQLColumnType.REAL:
+ row.put(key, myStatement.getColumnDouble(i));
+ break;
+
+ case SQLColumnType.INTEGER:
+ row.put(key, myStatement.getColumnLong(i));
+ break;
+
+ case SQLColumnType.BLOB:
+ case SQLColumnType.TEXT:
+ default: // (just in case)
+ row.put(key, myStatement.getColumnTextNativeString(i));
+ }
+
+ }
+
+ rowsArrayResult.put(row);
+
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ } while (myStatement.step());
+
+ try {
+ rowsResult.put("rows", rowsArrayResult);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ myStatement.dispose();
+
+ return rowsResult;
+ }
+
+} /* vim: set expandtab : */
diff --git a/platforms/android/app/src/main/java/io/sqlc/SQLitePlugin.java b/platforms/android/app/src/main/java/io/sqlc/SQLitePlugin.java
new file mode 100755
index 0000000..fdf92d0
--- /dev/null
+++ b/platforms/android/app/src/main/java/io/sqlc/SQLitePlugin.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (c) 2012-present Christopher J. Brody (aka Chris Brody)
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
+package io.sqlc;
+
+import android.annotation.SuppressLint;
+
+import android.util.Log;
+
+import java.io.File;
+
+import java.lang.IllegalArgumentException;
+import java.lang.Number;
+
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaPlugin;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class SQLitePlugin extends CordovaPlugin {
+
+ /**
+ * Multiple database runner map (static).
+ *
+ * NOTE: no public static accessor to db (runner) map since it is not
+ * expected to work properly with db threading.
+ *
+ * FUTURE TBD put DBRunner into a public class that can provide external accessor.
+ *
+ * ADDITIONAL NOTE: Storing as Map to avoid portabiity issue
+ * between Java 6/7/8 as discussed in:
+ * https://gist.github.com/AlainODea/1375759b8720a3f9f094
+ *
+ * THANKS to @NeoLSN (Jason Yang/楊朝傑) for giving the pointer in:
+ * https://github.com/litehelpers/Cordova-sqlite-storage/issues/727
+ */
+ static Map dbrmap = new ConcurrentHashMap();
+
+ /**
+ * NOTE: Using default constructor, no explicit constructor.
+ */
+
+ /**
+ * Executes the request and returns PluginResult.
+ *
+ * @param actionAsString The action to execute.
+ * @param args JSONArry of arguments for the plugin.
+ * @param cbc Callback context from Cordova API
+ * @return Whether the action was valid.
+ */
+ @Override
+ public boolean execute(String actionAsString, JSONArray args, CallbackContext cbc) {
+
+ Action action;
+ try {
+ action = Action.valueOf(actionAsString);
+ } catch (IllegalArgumentException e) {
+ // shouldn't ever happen
+ Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error", e);
+ return false;
+ }
+
+ try {
+ return executeAndPossiblyThrow(action, args, cbc);
+ } catch (JSONException e) {
+ // TODO: signal JSON problem to JS
+ Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error", e);
+ return false;
+ }
+ }
+
+ private boolean executeAndPossiblyThrow(Action action, JSONArray args, CallbackContext cbc)
+ throws JSONException {
+
+ boolean status = true;
+ JSONObject o;
+ String echo_value;
+ String dbname;
+
+ switch (action) {
+ case echoStringValue:
+ o = args.getJSONObject(0);
+ echo_value = o.getString("value");
+ cbc.success(echo_value);
+ break;
+
+ case open:
+ o = args.getJSONObject(0);
+ dbname = o.getString("name");
+ // open database and start reading its queue
+ this.startDatabase(dbname, o, cbc);
+ break;
+
+ case close:
+ o = args.getJSONObject(0);
+ dbname = o.getString("path");
+ // put request in the q to close the db
+ this.closeDatabase(dbname, cbc);
+ break;
+
+ case delete:
+ o = args.getJSONObject(0);
+ dbname = o.getString("path");
+
+ deleteDatabase(dbname, cbc);
+
+ break;
+
+ case executeSqlBatch:
+ case backgroundExecuteSqlBatch:
+ JSONObject allargs = args.getJSONObject(0);
+ JSONObject dbargs = allargs.getJSONObject("dbargs");
+ dbname = dbargs.getString("dbname");
+ JSONArray txargs = allargs.getJSONArray("executes");
+
+ if (txargs.isNull(0)) {
+ cbc.error("INTERNAL PLUGIN ERROR: missing executes list");
+ } else {
+ int len = txargs.length();
+ String[] queries = new String[len];
+ JSONArray[] jsonparams = new JSONArray[len];
+
+ for (int i = 0; i < len; i++) {
+ JSONObject a = txargs.getJSONObject(i);
+ queries[i] = a.getString("sql");
+ jsonparams[i] = a.getJSONArray("params");
+ }
+
+ // put db query in the queue to be executed in the db thread:
+ DBQuery q = new DBQuery(queries, jsonparams, cbc);
+ DBRunner r = dbrmap.get(dbname);
+ if (r != null) {
+ try {
+ r.q.put(q);
+ } catch(Exception e) {
+ Log.e(SQLitePlugin.class.getSimpleName(), "couldn't add to queue", e);
+ cbc.error("INTERNAL PLUGIN ERROR: couldn't add to queue");
+ }
+ } else {
+ cbc.error("INTERNAL PLUGIN ERROR: database not open");
+ }
+ }
+ break;
+ }
+
+ return status;
+ }
+
+ /**
+ * Clean up and close all open databases.
+ */
+ @Override
+ public void onDestroy() {
+ while (!dbrmap.isEmpty()) {
+ String dbname = dbrmap.keySet().iterator().next();
+
+ this.closeDatabaseNow(dbname);
+
+ DBRunner r = dbrmap.get(dbname);
+ try {
+ // stop the db runner thread:
+ r.q.put(new DBQuery());
+ } catch(Exception e) {
+ Log.e(SQLitePlugin.class.getSimpleName(), "INTERNAL PLUGIN CLEANUP ERROR: could not stop db thread due to exception", e);
+ }
+ dbrmap.remove(dbname);
+ }
+ }
+
+ // --------------------------------------------------------------------------
+ // LOCAL METHODS
+ // --------------------------------------------------------------------------
+
+ private void startDatabase(String dbname, JSONObject options, CallbackContext cbc) {
+ DBRunner r = dbrmap.get(dbname);
+
+ if (r != null) {
+ // NO LONGER EXPECTED due to BUG 666 workaround solution:
+ cbc.error("INTERNAL ERROR: database already open for db name: " + dbname);
+ } else {
+ r = new DBRunner(dbname, options, cbc);
+ dbrmap.put(dbname, r);
+ this.cordova.getThreadPool().execute(r);
+ }
+ }
+ /**
+ * Open a database.
+ *
+ * @param dbName The name of the database file
+ */
+ private SQLiteAndroidDatabase openDatabase(String dbname, CallbackContext cbc, boolean old_impl) throws Exception {
+ try {
+ // ASSUMPTION: no db (connection/handle) is already stored in the map
+ // [should be true according to the code in DBRunner.run()]
+
+ File dbfile = this.cordova.getActivity().getDatabasePath(dbname);
+
+ if (!dbfile.exists()) {
+ dbfile.getParentFile().mkdirs();
+ }
+
+ Log.v("info", "Open sqlite db: " + dbfile.getAbsolutePath());
+
+ SQLiteAndroidDatabase mydb = old_impl ? new SQLiteAndroidDatabase() : new SQLiteConnectorDatabase();
+ mydb.open(dbfile);
+
+ if (cbc != null) // XXX Android locking/closing BUG workaround
+ cbc.success();
+
+ return mydb;
+ } catch (Exception e) {
+ if (cbc != null) // XXX Android locking/closing BUG workaround
+ cbc.error("can't open database " + e);
+ throw e;
+ }
+ }
+
+ /**
+ * Close a database (in another thread).
+ *
+ * @param dbName The name of the database file
+ */
+ private void closeDatabase(String dbname, CallbackContext cbc) {
+ DBRunner r = dbrmap.get(dbname);
+ if (r != null) {
+ try {
+ r.q.put(new DBQuery(false, cbc));
+ } catch(Exception e) {
+ if (cbc != null) {
+ cbc.error("couldn't close database" + e);
+ }
+ Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e);
+ }
+ } else {
+ if (cbc != null) {
+ cbc.success();
+ }
+ }
+ }
+
+ /**
+ * Close a database (in the current thread).
+ *
+ * @param dbname The name of the database file
+ */
+ private void closeDatabaseNow(String dbname) {
+ DBRunner r = dbrmap.get(dbname);
+
+ if (r != null) {
+ SQLiteAndroidDatabase mydb = r.mydb;
+
+ if (mydb != null)
+ mydb.closeDatabaseNow();
+ }
+ }
+
+ private void deleteDatabase(String dbname, CallbackContext cbc) {
+ DBRunner r = dbrmap.get(dbname);
+ if (r != null) {
+ try {
+ r.q.put(new DBQuery(true, cbc));
+ } catch(Exception e) {
+ if (cbc != null) {
+ cbc.error("couldn't close database" + e);
+ }
+ Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e);
+ }
+ } else {
+ boolean deleteResult = this.deleteDatabaseNow(dbname);
+ if (deleteResult) {
+ cbc.success();
+ } else {
+ cbc.error("couldn't delete database");
+ }
+ }
+ }
+
+ /**
+ * Delete a database.
+ *
+ * @param dbName The name of the database file
+ *
+ * @return true if successful or false if an exception was encountered
+ */
+ private boolean deleteDatabaseNow(String dbname) {
+ File dbfile = this.cordova.getActivity().getDatabasePath(dbname);
+
+ try {
+ return cordova.getActivity().deleteDatabase(dbfile.getAbsolutePath());
+ } catch (Exception e) {
+ Log.e(SQLitePlugin.class.getSimpleName(), "couldn't delete database", e);
+ return false;
+ }
+ }
+
+ private class DBRunner implements Runnable {
+ final String dbname;
+ private boolean oldImpl;
+ private boolean bugWorkaround;
+
+ final BlockingQueue q;
+ final CallbackContext openCbc;
+
+ SQLiteAndroidDatabase mydb;
+
+ DBRunner(final String dbname, JSONObject options, CallbackContext cbc) {
+ this.dbname = dbname;
+ this.oldImpl = options.has("androidOldDatabaseImplementation");
+ Log.v(SQLitePlugin.class.getSimpleName(), "Android db implementation: built-in android.database.sqlite package");
+ this.bugWorkaround = this.oldImpl && options.has("androidBugWorkaround");
+ if (this.bugWorkaround)
+ Log.v(SQLitePlugin.class.getSimpleName(), "Android db closing/locking workaround applied");
+
+ this.q = new LinkedBlockingQueue();
+ this.openCbc = cbc;
+ }
+
+ public void run() {
+ try {
+ this.mydb = openDatabase(dbname, this.openCbc, this.oldImpl);
+ } catch (Exception e) {
+ Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error, stopping db thread", e);
+ dbrmap.remove(dbname);
+ return;
+ }
+
+ DBQuery dbq = null;
+
+ try {
+ dbq = q.take();
+
+ while (!dbq.stop) {
+ mydb.executeSqlBatch(dbq.queries, dbq.jsonparams, dbq.cbc);
+
+ if (this.bugWorkaround && dbq.queries.length == 1 && dbq.queries[0] == "COMMIT")
+ mydb.bugWorkaround();
+
+ dbq = q.take();
+ }
+ } catch (Exception e) {
+ Log.e(SQLitePlugin.class.getSimpleName(), "unexpected error", e);
+ }
+
+ if (dbq != null && dbq.close) {
+ try {
+ closeDatabaseNow(dbname);
+
+ dbrmap.remove(dbname); // (should) remove ourself
+
+ if (!dbq.delete) {
+ dbq.cbc.success();
+ } else {
+ try {
+ boolean deleteResult = deleteDatabaseNow(dbname);
+ if (deleteResult) {
+ dbq.cbc.success();
+ } else {
+ dbq.cbc.error("couldn't delete database");
+ }
+ } catch (Exception e) {
+ Log.e(SQLitePlugin.class.getSimpleName(), "couldn't delete database", e);
+ dbq.cbc.error("couldn't delete database: " + e);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(SQLitePlugin.class.getSimpleName(), "couldn't close database", e);
+ if (dbq.cbc != null) {
+ dbq.cbc.error("couldn't close database: " + e);
+ }
+ }
+ }
+ }
+ }
+
+ private final class DBQuery {
+ // XXX TODO replace with DBRunner action enum:
+ final boolean stop;
+ final boolean close;
+ final boolean delete;
+ final String[] queries;
+ final JSONArray[] jsonparams;
+ final CallbackContext cbc;
+
+ DBQuery(String[] myqueries, JSONArray[] params, CallbackContext c) {
+ this.stop = false;
+ this.close = false;
+ this.delete = false;
+ this.queries = myqueries;
+ this.jsonparams = params;
+ this.cbc = c;
+ }
+
+ DBQuery(boolean delete, CallbackContext cbc) {
+ this.stop = true;
+ this.close = true;
+ this.delete = delete;
+ this.queries = null;
+ this.jsonparams = null;
+ this.cbc = cbc;
+ }
+
+ // signal the DBRunner thread to stop:
+ DBQuery() {
+ this.stop = true;
+ this.close = false;
+ this.delete = false;
+ this.queries = null;
+ this.jsonparams = null;
+ this.cbc = null;
+ }
+ }
+
+ private static enum Action {
+ echoStringValue,
+ open,
+ close,
+ delete,
+ executeSqlBatch,
+ backgroundExecuteSqlBatch,
+ }
+}
+
+/* vim: set expandtab : */
diff --git a/platforms/android/app/src/main/res/xml/config.xml b/platforms/android/app/src/main/res/xml/config.xml
index 58c1605..89634ef 100644
--- a/platforms/android/app/src/main/res/xml/config.xml
+++ b/platforms/android/app/src/main/res/xml/config.xml
@@ -33,13 +33,16 @@
+
+
+
Goober
Goober, a mobile app for pnut.io
Morgan McMillian
-
+
@@ -49,6 +52,7 @@
+
diff --git a/platforms/android/platform_www/cordova_plugins.js b/platforms/android/platform_www/cordova_plugins.js
index a9e93ae..d6e5719 100644
--- a/platforms/android/platform_www/cordova_plugins.js
+++ b/platforms/android/platform_www/cordova_plugins.js
@@ -245,6 +245,14 @@ module.exports = [
"clobbers": [
"window.FilePath"
]
+ },
+ {
+ "id": "cordova-sqlite-storage.SQLitePlugin",
+ "file": "plugins/cordova-sqlite-storage/www/SQLitePlugin.js",
+ "pluginId": "cordova-sqlite-storage",
+ "clobbers": [
+ "SQLitePlugin"
+ ]
}
];
module.exports.metadata =
@@ -260,7 +268,8 @@ module.exports.metadata =
"cordova-plugin-splashscreen": "5.0.2",
"cordova-plugin-device": "2.0.2",
"cordova-android-support-gradle-release": "1.4.4",
- "cordova-plugin-filepath": "1.4.2"
+ "cordova-plugin-filepath": "1.4.2",
+ "cordova-sqlite-storage": "2.5.1"
};
// BOTTOM OF METADATA
});
\ No newline at end of file