diff --git a/framework/assets/js/phonegap.js.base b/framework/assets/js/phonegap.js.base index 59401348..acef0497 100755 --- a/framework/assets/js/phonegap.js.base +++ b/framework/assets/js/phonegap.js.base @@ -316,7 +316,9 @@ PhoneGap.stringify = function(args) { s = s + '}'; } else { - s = s + '"' + args[i] + '"'; + var a = args[i].replace(/\\/g, '\\\\'); + a = a.replace(/"/g, '\\"'); + s = s + '"' + a + '"'; } } s = s + "]"; diff --git a/framework/assets/js/storage.js b/framework/assets/js/storage.js old mode 100644 new mode 100755 index ba234035..42499161 --- a/framework/assets/js/storage.js +++ b/framework/assets/js/storage.js @@ -5,71 +5,305 @@ * most manufacturers ship with Android 1.5 and do not do OTA Updates, this is required */ +/** + * Storage object that is called by native code when performing queries. + * PRIVATE METHOD + */ var DroidDB = function() { - this.txQueue = []; + this.queryQueue = {}; }; -DroidDB.prototype.addResult = function(rawdata, tx_id) { - eval("var data = " + rawdata); - var tx = this.txQueue[tx_id]; - tx.resultSet.push(data); +/** + * Callback from native code when result from a query is available. + * PRIVATE METHOD + * + * @param rawdata JSON string of the row data + * @param id Query id + */ +DroidDB.prototype.addResult = function(rawdata, id) { + try { + eval("var data = " + rawdata + ";"); + var query = this.queryQueue[id]; + query.resultSet.push(data); + } catch (e) { + console.log("DroidDB.addResult(): Error="+e); + } }; -DroidDB.prototype.completeQuery = function(tx_id) { - var tx = this.txQueue[tx_id]; - var r = new result(); - r.rows.resultSet = tx.resultSet; - r.rows.length = tx.resultSet.length; - tx.win(r); +/** + * Callback from native code when query is complete. + * PRIVATE METHOD + * + * @param id Query id + */ +DroidDB.prototype.completeQuery = function(id) { + var query = this.queryQueue[id]; + if (query) { + try { + delete this.queryQueue[id]; + + // Get transaction + var tx = query.tx; + + // If transaction hasn't failed + // Note: We ignore all query results if previous query + // in the same transaction failed. + if (tx && tx.queryList[id]) { + + // Save query results + var r = new DroidDB_Result(); + r.rows.resultSet = query.resultSet; + r.rows.length = query.resultSet.length; + try { + if (typeof query.successCallback == 'function') { + query.successCallback(query.tx, r); + } + } catch (ex) { + console.log("executeSql error calling user success callback: "+ex); + } + + tx.queryComplete(id); + } + } catch (e) { + console.log("executeSql error: "+e); + } + } }; -DroidDB.prototype.fail = function(reason, tx_id) { - var tx = this.txQueue[tx_id]; - tx.fail(reason); +/** + * Callback from native code when query fails + * PRIVATE METHOD + * + * @param reason Error message + * @param id Query id + */ +DroidDB.prototype.fail = function(reason, id) { + var query = this.queryQueue[id]; + if (query) { + try { + delete this.queryQueue[id]; + + // Get transaction + var tx = query.tx; + + // If transaction hasn't failed + // Note: We ignore all query results if previous query + // in the same transaction failed. + if (tx && tx.queryList[id]) { + tx.queryList = {}; + + try { + if (typeof query.errorCallback == 'function') { + query.errorCallback(query.tx, reason); + } + } catch (ex) { + console.log("executeSql error calling user error callback: "+ex); + } + + tx.queryFailed(id, reason); + } + + } catch (e) { + console.log("executeSql error: "+e); + } + } }; var DatabaseShell = function() { }; -DatabaseShell.prototype.transaction = function(process) { - tx = new Tx(); - process(tx); +/** + * Start a transaction. + * Does not support rollback in event of failure. + * + * @param process {Function} The transaction function + * @param successCallback {Function} + * @param errorCallback {Function} + */ +DatabaseShell.prototype.transaction = function(process, successCallback, errorCallback) { + var tx = new DroidDB_Tx(); + tx.successCallback = successCallback; + tx.errorCallback = errorCallback; + try { + process(tx); + } catch (e) { + console.log("Transaction error: "+e); + if (tx.errorCallback) { + try { + tx.errorCallback(e); + } catch (ex) { + console.log("Transaction error calling user error callback: "+e); + } + } + } }; -var Tx = function() { - droiddb.txQueue.push(this); - this.id = droiddb.txQueue.length - 1; +/** + * Transaction object + * PRIVATE METHOD + */ +var DroidDB_Tx = function() { + + // Set the id of the transaction + this.id = PhoneGap.createUUID(); + + // Callbacks + this.successCallback = null; + this.errorCallback = null; + + // Query list + this.queryList = {}; +}; + +/** + * Mark query in transaction as complete. + * If all queries are complete, call the user's transaction success callback. + * + * @param id Query id + */ +DroidDB_Tx.prototype.queryComplete = function(id) { + delete this.queryList[id]; + + // If no more outstanding queries, then fire transaction success + if (this.successCallback) { + var count = 0; + for (var i in this.queryList) { + count++; + } + if (count == 0) { + try { + this.successCallback(); + } catch(e) { + console.log("Transaction error calling user success callback: " + e); + } + } + } +}; + +/** + * Mark query in transaction as failed. + * + * @param id Query id + * @param reason Error message + */ +DroidDB_Tx.prototype.queryFailed = function(id, reason) { + + // The sql queries in this transaction have already been run, since + // we really don't have a real transaction implemented in native code. + // However, the user callbacks for the remaining sql queries in transaction + // will not be called. + this.queryList = {}; + + if (this.errorCallback) { + try { + this.errorCallback(reason); + } catch(e) { + console.log("Transaction error calling user error callback: " + e); + } + } +}; + +/** + * SQL query object + * PRIVATE METHOD + * + * @param tx The transaction object that this query belongs to + */ +var DroidDB_Query = function(tx) { + + // Set the id of the query + this.id = PhoneGap.createUUID(); + + // Add this query to the queue + droiddb.queryQueue[this.id] = this; + + // Init result this.resultSet = []; + + // Set transaction that this query belongs to + this.tx = tx; + + // Add this query to transaction list + this.tx.queryList[this.id] = this; + + // Callbacks + this.successCallback = null; + this.errorCallback = null; + +} + +/** + * Execute SQL statement + * + * @param sql SQL statement to execute + * @param params Statement parameters + * @param successCallback Success callback + * @param errorCallback Error callback + */ +DroidDB_Tx.prototype.executeSql = function(sql, params, successCallback, errorCallback) { + + // Init params array + if (typeof params == 'undefined') { + params = []; + } + + // Create query and add to queue + var query = new DroidDB_Query(this); + droiddb.queryQueue[query.id] = query; + + // Save callbacks + query.successCallback = successCallback; + query.errorCallback = errorCallback; + + // Call native code + PhoneGap.execAsync(null, null, "Storage", "executeSql", [sql, params, query.id]); }; -Tx.prototype.executeSql = function(query, params, win, fail) { - PhoneGap.execAsync(null, null, "Storage", "executeSql", [query, params, this.id]); - tx.win = win; - tx.fail = fail; +/** + * SQL result set that is returned to user. + * PRIVATE METHOD + */ +DroidDB_Result = function() { + this.rows = new DroidDB_Rows(); }; -var result = function() { - this.rows = new Rows(); +/** + * SQL result set object + * PRIVATE METHOD + */ +DroidDB_Rows = function() { + this.resultSet = []; // results array + this.length = 0; // number of rows }; -var Rows = function() { - this.resultSet = []; - this.length = 0; +/** + * Get item from SQL result set + * + * @param row The row number to return + * @return The row object + */ +DroidDB_Rows.prototype.item = function(row) { + return this.resultSet[row]; }; -Rows.prototype.item = function(row_id) { - return this.resultSet[id]; -}; - -var dbSetup = function(name, version, display_name, size) { +/** + * Open database + * + * @param name Database name + * @param version Database version + * @param display_name Database display name + * @param size Database size in bytes + * @return Database object + */ +DroidDB_openDatabase = function(name, version, display_name, size) { PhoneGap.execAsync(null, null, "Storage", "openDatabase", [name, version, display_name, size]); - db_object = new DatabaseShell(); - return db_object; + var db = new DatabaseShell(); + return db; }; PhoneGap.addConstructor(function() { if (typeof window.openDatabase == "undefined") { - navigator.openDatabase = window.openDatabase = dbSetup; + navigator.openDatabase = window.openDatabase = DroidDB_openDatabase; window.droiddb = new DroidDB(); } }); diff --git a/framework/src/com/phonegap/AudioPlayer.java b/framework/src/com/phonegap/AudioPlayer.java index 48a58004..44c71776 100755 --- a/framework/src/com/phonegap/AudioPlayer.java +++ b/framework/src/com/phonegap/AudioPlayer.java @@ -182,6 +182,8 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On if (this.isStreaming(file)) { this.mPlayer.setDataSource(file); this.mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + this.setState(MEDIA_STARTING); + this.mPlayer.setOnPreparedListener(this); this.mPlayer.prepareAsync(); } @@ -195,13 +197,13 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On else { this.mPlayer.setDataSource("/sdcard/" + file); } + this.setState(MEDIA_STARTING); + this.mPlayer.setOnPreparedListener(this); this.mPlayer.prepare(); // Get duration this.duration = this.mPlayer.getDuration(); } - this.mPlayer.setOnPreparedListener(this); - this.setState(MEDIA_STARTING); } catch (Exception e) { e.printStackTrace(); diff --git a/framework/src/com/phonegap/ContactAccessorSdk3_4.java b/framework/src/com/phonegap/ContactAccessorSdk3_4.java index ae64b109..879902c3 100644 --- a/framework/src/com/phonegap/ContactAccessorSdk3_4.java +++ b/framework/src/com/phonegap/ContactAccessorSdk3_4.java @@ -31,6 +31,7 @@ import android.app.Activity; import android.content.ContentResolver; import android.database.Cursor; import android.net.Uri; +import android.provider.Contacts; import android.provider.Contacts.ContactMethods; import android.provider.Contacts.ContactMethodsColumns; import android.provider.Contacts.Organizations; @@ -117,9 +118,8 @@ public class ContactAccessorSdk3_4 extends ContactAccessor { contact.put("id", contactId); // Do query for name and note - // Right now we are just querying the displayName Cursor cur = cr.query(People.CONTENT_URI, - null, + new String[] {People.DISPLAY_NAME, People.NOTES}, "people._id = ?", new String[] {contactId}, null); diff --git a/framework/src/com/phonegap/DroidGap.java b/framework/src/com/phonegap/DroidGap.java index 46697229..e091cb10 100755 --- a/framework/src/com/phonegap/DroidGap.java +++ b/framework/src/com/phonegap/DroidGap.java @@ -291,15 +291,6 @@ public class DroidGap extends Activity { this.addService("Storage", "com.phonegap.Storage"); this.addService("Temperature", "com.phonegap.TempListener"); - // Add in support for storage for Android 1.X devices - if (android.os.Build.VERSION.RELEASE.startsWith("1.")) { - System.out.println("Android 1.X device"); - - Package pack = this.getClass().getPackage(); - String appPackage = pack.getName(); - this.pluginManager.exec("Storage", "setStorage", null, "["+appPackage+"]", false); - } - } /** diff --git a/framework/src/com/phonegap/Storage.java b/framework/src/com/phonegap/Storage.java index a3339306..0d764f9c 100755 --- a/framework/src/com/phonegap/Storage.java +++ b/framework/src/com/phonegap/Storage.java @@ -2,21 +2,21 @@ package com.phonegap; import org.json.JSONArray; import org.json.JSONException; - +import org.json.JSONObject; import com.phonegap.api.Plugin; import com.phonegap.api.PluginResult; - import android.database.Cursor; import android.database.sqlite.*; -import android.util.Log; +/** + * This class implements the HTML5 database support for Android 1.X devices. + * It is not used for Android 2.X, since HTML5 database is built in to the browser. + */ public class Storage extends Plugin { - private static final String LOG_TAG = "SQLite Storage:"; - - SQLiteDatabase myDb; - String path; - String txid = ""; + SQLiteDatabase myDb = null; // Database object + String path = null; // Database path + String dbName = null; // Database name /** * Constructor. @@ -37,6 +37,7 @@ public class Storage extends Plugin { String result = ""; try { + // TODO: Do we want to allow a user to do this, since they could get to other app databases? if (action.equals("setStorage")) { this.setStorage(args.getString(0)); } @@ -67,57 +68,120 @@ public class Storage extends Plugin { public boolean isSynch(String action) { return false; } + + /** + * Clean up and close database. + */ + @Override + public void onDestroy() { + if (this.myDb != null) { + this.myDb.close(); + this.myDb = null; + } + } //-------------------------------------------------------------------------- // LOCAL METHODS //-------------------------------------------------------------------------- + /** + * Set the application package for the database. Each application saves its + * database files in a directory with the application package as part of the file name. + * + * For example, application "com.phonegap.demo.Demo" would save its database + * files in "/data/data/com.phonegap.demo/databases/" directory. + * + * @param appPackage The application package. + */ public void setStorage(String appPackage) { - path = "/data/data/" + appPackage + "/databases/"; + this.path = "/data/data/" + appPackage + "/databases/"; } + /** + * Open database. + * + * @param db The name of the database + * @param version The version + * @param display_name The display name + * @param size The size in bytes + */ public void openDatabase(String db, String version, String display_name, long size) { - if (path != null) { - path += db + ".db"; - myDb = SQLiteDatabase.openOrCreateDatabase(path, null); + + // If database is open, then close it + if (this.myDb != null) { + this.myDb.close(); } + + // If no database path, generate from application package + if (this.path == null) { + Package pack = this.ctx.getClass().getPackage(); + String appPackage = pack.getName(); + this.setStorage(appPackage); + } + + this.dbName = this.path + db + ".db"; + this.myDb = SQLiteDatabase.openOrCreateDatabase(this.dbName, null); } + /** + * Execute SQL statement. + * + * @param query The SQL query + * @param params Parameters for the query + * @param tx_id Transaction id + */ public void executeSql(String query, String[] params, String tx_id) { try { - txid = tx_id; - Cursor myCursor = myDb.rawQuery(query, params); - processResults(myCursor); - } catch (SQLiteException ex) { - Log.d(LOG_TAG, ex.getMessage()); - txid = ""; - this.sendJavascript("droiddb.fail(" + ex.getMessage() + "," + txid + ");"); + Cursor myCursor = this.myDb.rawQuery(query, params); + this.processResults(myCursor, tx_id); + myCursor.close(); + } + catch (SQLiteException ex) { + ex.printStackTrace(); + System.out.println("Storage.executeSql(): Error=" + ex.getMessage()); + + // Send error message back to JavaScript + this.sendJavascript("droiddb.fail('" + ex.getMessage() + "','" + tx_id + "');"); } } - public void processResults(Cursor cur) { - String key = ""; - String value = ""; - String resultString = ""; + /** + * Process query results. + * + * @param cur Cursor into query results + * @param tx_id Transaction id + */ + public void processResults(Cursor cur, String tx_id) { + + // If query result has rows if (cur.moveToFirst()) { + String key = ""; + String value = ""; int colCount = cur.getColumnCount(); + + // Build up JSON result object for each row do { - resultString = "{"; - for (int i = 0; i < colCount; ++i) { - key = cur.getColumnName(i); - value = cur.getString(i); - resultString += " \"" + key + "\" : \"" + value + "\""; - if (i != (colCount - 1)) { - resultString += ","; + JSONObject result = new JSONObject(); + try { + for (int i = 0; i < colCount; ++i) { + key = cur.getColumnName(i); + value = cur.getString(i).replace("\"", "\\\""); // must escape " with \" for JavaScript + result.put(key, value); } + + // Send row back to JavaScript + this.sendJavascript("droiddb.addResult('" + result.toString() + "','" + tx_id + "');"); + + } catch (JSONException e) { + e.printStackTrace(); } - resultString += "}"; - this.sendJavascript("droiddb.addResult('" + resultString + "', " + txid + ");"); - } while (cur.moveToNext()); - this.sendJavascript("droiddb.completeQuery(" + txid + ");"); - txid = ""; - myDb.close(); - } + + } while (cur.moveToNext()); + + } + // Let JavaScript know that there are no more rows + this.sendJavascript("droiddb.completeQuery('" + tx_id + "');"); + } } \ No newline at end of file diff --git a/framework/src/com/phonegap/api/PluginManager.java b/framework/src/com/phonegap/api/PluginManager.java index 2addf3af..8ad13abe 100755 --- a/framework/src/com/phonegap/api/PluginManager.java +++ b/framework/src/com/phonegap/api/PluginManager.java @@ -140,15 +140,7 @@ public final class PluginManager { @SuppressWarnings("unchecked") private boolean isPhoneGapPlugin(Class c) { if (c != null) { - if (c.getSuperclass().getName().equals("com.phonegap.api.Plugin")) { - return true; - } - Class[] interfaces = c.getInterfaces(); - for (int j=0; j