diff --git a/README.md b/README.md
index 4ccdc6b5..6acc90ea 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ Commands:
help ...... See this message. Type help [command name] to see specific help topics.
- gen ....... Generate an example PhoneGap application to current directory.
+ gen ....... Generate the example PhoneGap application to current directory (or optionally provide an output directory as parameter).
create .... Creates an Android compatible project from a WWW folder.
classic ... Backwards support for droidgap script. Run "droidgap help classic" for more info.
update .... Copy a fresh phonegap.jar and phonegap.js into a valid PhoneGap/Android project.
@@ -41,8 +41,8 @@ Commands:
Quickstart:
- $ droidgap gen example
- $ cd example
+ $ droidgap gen exampleapp
+ $ cd exampleapp
$ ant debug install && adb logcat
diff --git a/VERSION b/VERSION
new file mode 100644
index 00000000..2bd77c74
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.9.4
\ No newline at end of file
diff --git a/bin/droidgap b/bin/droidgap
index dc75bbdd..bf93568e 100755
--- a/bin/droidgap
+++ b/bin/droidgap
@@ -1,5 +1,5 @@
#!/usr/bin/env ruby
-ROOT = File.expand_path(File.dirname(__FILE__).gsub('bin',''))
+ROOT = File.expand_path(File.dirname(__FILE__).gsub(/bin$/,''))
require 'fileutils'
require File.join(ROOT, "lib", "generate.rb")
require File.join(ROOT, "lib", "classic.rb")
@@ -60,7 +60,7 @@ if ARGV.first.nil? || ARGV.first == 'help'
Commands:
help ...... See this message. Type help [command name] to see specific help topics.
- gen ....... Generate an example PhoneGap application to current directory.
+ gen ....... Generate the example PhoneGap application to current directory (or optionally provide an output directory as parameter).
create .... Creates an Android compatible project from a WWW folder.
classic ... Backwards support for droidgap script. Run "droidgap help classic" for more info.
update .... Copy a fresh phonegap.jar and phonegap.js into a valid PhoneGap/Android project.
@@ -68,8 +68,8 @@ if ARGV.first.nil? || ARGV.first == 'help'
Quickstart:
- $ droidgap gen example
- $ cd example
+ $ droidgap gen exampleapp
+ $ cd exampleapp
$ ant debug install && adb logcat
EOF
@@ -79,11 +79,13 @@ if ARGV.first.nil? || ARGV.first == 'help'
DroidGap Generate
-----------------
- Generate an example PhoneGap application to path supplied or current working directory if none is supplied.
+ Generate the example PhoneGap application to path supplied or current working directory if none is supplied.
Usage:
droidgap gen [path]
+
+ NOTE: Do *not* run "droidgap gen example" - you will end up with a recursive directory problem.
EOF
diff --git a/example/index.html b/example/index.html
index ecf05ee7..89e302ef 100644
--- a/example/index.html
+++ b/example/index.html
@@ -5,141 +5,32 @@
PhoneGap
-
-
+
- function show_pic()
- {
- var viewport = document.getElementById('viewport');
- viewport.style.display = "";
- navigator.camera.getPicture(dump_pic, fail, { quality: 50 });
- }
-
- function dump_pic(data)
- {
- var viewport = document.getElementById('viewport');
- console.log(data);
- viewport.style.display = "";
- viewport.style.position = "absolute";
- viewport.style.top = "10px";
- viewport.style.left = "10px";
- document.getElementById("test_img").src = "data:image/jpeg;base64," + data;
- }
-
- function close()
- {
- var viewport = document.getElementById('viewport');
- viewport.style.position = "relative";
- viewport.style.display = "none";
- }
-
- function fail(fail)
- {
- alert(fail);
- }
-
- // This is just to do this.
- function readFile()
- {
- navigator.file.read('/sdcard/phonegap.txt', fail , fail);
- }
-
- function writeFile()
- {
- navigator.file.write('foo.txt', "This is a test of writing to a file", fail, fail);
- }
-
- function get_contacts()
- {
- var obj = new ContactFindOptions();
- obj.filter="";
- obj.multiple=true;
- obj.limit=5;
- navigator.service.contacts.find(["displayName", "phoneNumbers", "emails"], count_contacts, fail, obj);
- }
-
- function count_contacts(contacts)
- {
- alert(contacts.length);
- }
-
- function init(){
- document.addEventListener("touchmove", preventBehavior, false);
- document.addEventListener("deviceready", deviceInfo, true);
- }
-
-
Welcome to PhoneGap!
- this file is located at assets/index.html
+ this file is located at assets/www/index.html
-
Platform:
- Version:
- UUID:
-
+ Platform: , Version:
+ UUID: , Name:
+ Width: , Height:
+ , Color Depth:
+
X:
Y:
Z:
- Watch Accelerometer
+ Toggle Accelerometer
Get Location
Call 411
Beep
Vibrate
Get a Picture
- Get phone's contacts
+ Get Phone's Contacts
+ Check Network
diff --git a/example/main.js b/example/main.js
new file mode 100644
index 00000000..ae447aa9
--- /dev/null
+++ b/example/main.js
@@ -0,0 +1,140 @@
+var deviceInfo = function() {
+ document.getElementById("platform").innerHTML = device.platform;
+ document.getElementById("version").innerHTML = device.version;
+ document.getElementById("uuid").innerHTML = device.uuid;
+ document.getElementById("name").innerHTML = device.name;
+ document.getElementById("width").innerHTML = screen.width;
+ document.getElementById("height").innerHTML = screen.height;
+ document.getElementById("colorDepth").innerHTML = screen.colorDepth;
+};
+
+var getLocation = function() {
+ var suc = function(p) {
+ alert(p.coords.latitude + " " + p.coords.longitude);
+ };
+ var locFail = function() {
+ };
+ navigator.geolocation.getCurrentPosition(suc, locFail);
+};
+
+var beep = function() {
+ navigator.notification.beep(2);
+};
+
+var vibrate = function() {
+ navigator.notification.vibrate(0);
+};
+
+function roundNumber(num) {
+ var dec = 3;
+ var result = Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
+ return result;
+}
+
+var accelerationWatch = null;
+
+function updateAcceleration(a) {
+ document.getElementById('x').innerHTML = roundNumber(a.x);
+ document.getElementById('y').innerHTML = roundNumber(a.y);
+ document.getElementById('z').innerHTML = roundNumber(a.z);
+}
+
+var toggleAccel = function() {
+ if (accelerationWatch !== null) {
+ navigator.accelerometer.clearWatch(accelerationWatch);
+ updateAcceleration({
+ x : "",
+ y : "",
+ z : ""
+ });
+ accelerationWatch = null;
+ } else {
+ var options = {};
+ options.frequency = 1000;
+ accelerationWatch = navigator.accelerometer.watchAcceleration(
+ updateAcceleration, function(ex) {
+ alert("accel fail (" + ex.name + ": " + ex.message + ")");
+ }, options);
+ }
+};
+
+var preventBehavior = function(e) {
+ e.preventDefault();
+};
+
+function dump_pic(data) {
+ var viewport = document.getElementById('viewport');
+ console.log(data);
+ viewport.style.display = "";
+ viewport.style.position = "absolute";
+ viewport.style.top = "10px";
+ viewport.style.left = "10px";
+ document.getElementById("test_img").src = "data:image/jpeg;base64," + data;
+}
+
+function fail(msg) {
+ alert(msg);
+}
+
+function show_pic() {
+ navigator.camera.getPicture(dump_pic, fail, {
+ quality : 50
+ });
+}
+
+function close() {
+ var viewport = document.getElementById('viewport');
+ viewport.style.position = "relative";
+ viewport.style.display = "none";
+}
+
+// This is just to do this.
+function readFile() {
+ navigator.file.read('/sdcard/phonegap.txt', fail, fail);
+}
+
+function writeFile() {
+ navigator.file.write('foo.txt', "This is a test of writing to a file",
+ fail, fail);
+}
+
+function contacts_success(contacts) {
+ alert(contacts.length
+ + ' contacts returned.'
+ + (contacts[2] && contacts[2].name ? (' Third contact is ' + contacts[2].name.formatted)
+ : ''));
+}
+
+function get_contacts() {
+ var obj = new ContactFindOptions();
+ obj.filter = "";
+ obj.multiple = true;
+ obj.limit = 5;
+ navigator.service.contacts.find(
+ [ "displayName", "name" ], contacts_success,
+ fail, obj);
+}
+
+var networkReachableCallback = function(reachability) {
+ // There is no consistency on the format of reachability
+ var networkState = reachability.code || reachability;
+
+ var currentState = {};
+ currentState[NetworkStatus.NOT_REACHABLE] = 'No network connection';
+ currentState[NetworkStatus.REACHABLE_VIA_CARRIER_DATA_NETWORK] = 'Carrier data connection';
+ currentState[NetworkStatus.REACHABLE_VIA_WIFI_NETWORK] = 'WiFi connection';
+
+ confirm("Connection type:\n" + currentState[networkState]);
+};
+
+function check_network() {
+ navigator.network.isReachable("www.mobiledevelopersolutions.com",
+ networkReachableCallback, {});
+}
+
+function init() {
+ // the next line makes it impossible to see Contacts on the HTC Evo since it
+ // doesn't have a scroll button
+ // document.addEventListener("touchmove", preventBehavior, false);
+ document.addEventListener("deviceready", deviceInfo, true);
+}
diff --git a/framework/assets/js/accelerometer.js b/framework/assets/js/accelerometer.js
index 5c091710..2368b596 100755
--- a/framework/assets/js/accelerometer.js
+++ b/framework/assets/js/accelerometer.js
@@ -11,7 +11,7 @@ function Acceleration(x, y, z) {
this.y = y;
this.z = z;
this.timestamp = new Date().getTime();
-};
+}
/**
* This class provides access to device accelerometer data.
@@ -28,7 +28,7 @@ function Accelerometer() {
* List of accelerometer watch timers
*/
this.timers = {};
-};
+}
Accelerometer.ERROR_MSG = ["Not running", "Starting", "", "Failed to start"];
@@ -40,16 +40,15 @@ Accelerometer.ERROR_MSG = ["Not running", "Starting", "", "Failed to start"];
* @param {AccelerationOptions} options The options for getting the accelerometer data such as timeout. (OPTIONAL)
*/
Accelerometer.prototype.getCurrentAcceleration = function(successCallback, errorCallback, options) {
- console.log("Accelerometer.getCurrentAcceleration()");
// successCallback required
- if (typeof successCallback != "function") {
+ if (typeof successCallback !== "function") {
console.log("Accelerometer Error: successCallback is not a function");
return;
}
// errorCallback optional
- if (errorCallback && (typeof errorCallback != "function")) {
+ if (errorCallback && (typeof errorCallback !== "function")) {
console.log("Accelerometer Error: errorCallback is not a function");
return;
}
@@ -69,16 +68,16 @@ Accelerometer.prototype.getCurrentAcceleration = function(successCallback, error
Accelerometer.prototype.watchAcceleration = function(successCallback, errorCallback, options) {
// Default interval (10 sec)
- var frequency = (options != undefined)? options.frequency : 10000;
+ var frequency = (options !== undefined)? options.frequency : 10000;
// successCallback required
- if (typeof successCallback != "function") {
+ if (typeof successCallback !== "function") {
console.log("Accelerometer Error: successCallback is not a function");
return;
}
// errorCallback optional
- if (errorCallback && (typeof errorCallback != "function")) {
+ if (errorCallback && (typeof errorCallback !== "function")) {
console.log("Accelerometer Error: errorCallback is not a function");
return;
}
@@ -109,12 +108,14 @@ Accelerometer.prototype.watchAcceleration = function(successCallback, errorCallb
Accelerometer.prototype.clearWatch = function(id) {
// Stop javascript timer & remove from timer list
- if (id && navigator.accelerometer.timers[id] != undefined) {
+ if (id && navigator.accelerometer.timers[id] !== undefined) {
clearInterval(navigator.accelerometer.timers[id]);
delete navigator.accelerometer.timers[id];
}
};
PhoneGap.addConstructor(function() {
- if (typeof navigator.accelerometer == "undefined") navigator.accelerometer = new Accelerometer();
+ if (typeof navigator.accelerometer === "undefined") {
+ navigator.accelerometer = new Accelerometer();
+ }
});
diff --git a/framework/assets/js/app.js b/framework/assets/js/app.js
new file mode 100755
index 00000000..5a45cdda
--- /dev/null
+++ b/framework/assets/js/app.js
@@ -0,0 +1,89 @@
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010-2011, IBM Corporation
+ */
+
+/**
+ * Constructor
+ */
+function App() {}
+
+/**
+ * Clear the resource cache.
+ */
+App.prototype.clearCache = function() {
+ PhoneGap.exec(null, null, "App", "clearCache", []);
+};
+
+/**
+ * Load the url into the webview.
+ *
+ * @param url The URL to load
+ * @param props Properties that can be passed in to the activity:
+ * wait: int => wait msec before loading URL
+ * loadingDialog: "Title,Message" => display a native loading dialog
+ * hideLoadingDialogOnPage: boolean => hide loadingDialog when page loaded instead of when deviceready event occurs.
+ * loadInWebView: boolean => cause all links on web page to be loaded into existing web view, instead of being loaded into new browser.
+ * loadUrlTimeoutValue: int => time in msec to wait before triggering a timeout error
+ * errorUrl: URL => URL to load if there's an error loading specified URL with loadUrl(). Should be a local URL such as file:///android_asset/www/error.html");
+ * keepRunning: boolean => enable app to keep running in background
+ *
+ * Example:
+ * App app = new App();
+ * app.loadUrl("http://server/myapp/index.html", {wait:2000, loadingDialog:"Wait,Loading App", loadUrlTimeoutValue: 60000});
+ */
+App.prototype.loadUrl = function(url, props) {
+ PhoneGap.exec(null, null, "App", "loadUrl", [url, props]);
+};
+
+/**
+ * Cancel loadUrl that is waiting to be loaded.
+ */
+App.prototype.cancelLoadUrl = function() {
+ PhoneGap.exec(null, null, "App", "cancelLoadUrl", []);
+};
+
+/**
+ * Clear web history in this web view.
+ * Instead of BACK button loading the previous web page, it will exit the app.
+ */
+App.prototype.clearHistory = function() {
+ PhoneGap.exec(null, null, "App", "clearHistory", []);
+};
+
+/**
+ * Add a class that implements a service.
+ *
+ * @param serviceType
+ * @param className
+ */
+App.prototype.addService = function(serviceType, className) {
+ PhoneGap.exec(null, null, "App", "addService", [serviceType, className]);
+};
+
+/**
+ * Override the default behavior of the Android back button.
+ * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired.
+ *
+ * Note: The user should not have to call this method. Instead, when the user
+ * registers for the "backbutton" event, this is automatically done.
+ *
+ * @param override T=override, F=cancel override
+ */
+App.prototype.overrideBackbutton = function(override) {
+ PhoneGap.exec(null, null, "App", "overrideBackbutton", [override]);
+};
+
+/**
+ * Exit and terminate the application.
+ */
+App.prototype.exitApp = function() {
+ return PhoneGap.exec(null, null, "App", "exitApp", []);
+};
+
+PhoneGap.addConstructor(function() {
+ navigator.app = window.app = new App();
+});
diff --git a/framework/assets/js/camera.js b/framework/assets/js/camera.js
index e49777aa..1e4a75a6 100755
--- a/framework/assets/js/camera.js
+++ b/framework/assets/js/camera.js
@@ -59,19 +59,17 @@ Camera.prototype.PictureSourceType = Camera.PictureSourceType;
Camera.prototype.getPicture = function(successCallback, errorCallback, options) {
// successCallback required
- if (typeof successCallback != "function") {
+ if (typeof successCallback !== "function") {
console.log("Camera Error: successCallback is not a function");
return;
}
// errorCallback optional
- if (errorCallback && (typeof errorCallback != "function")) {
+ if (errorCallback && (typeof errorCallback !== "function")) {
console.log("Camera Error: errorCallback is not a function");
return;
}
- this.successCallback = successCallback;
- this.errorCallback = errorCallback;
this.options = options;
var quality = 80;
if (options.quality) {
@@ -82,45 +80,14 @@ Camera.prototype.getPicture = function(successCallback, errorCallback, options)
destinationType = this.options.destinationType;
}
var sourceType = Camera.PictureSourceType.CAMERA;
- if (typeof this.options.sourceType == "number") {
+ if (typeof this.options.sourceType === "number") {
sourceType = this.options.sourceType;
}
- PhoneGap.exec(null, null, "Camera", "takePicture", [quality, destinationType, sourceType]);
-};
-
-/**
- * Callback function from native code that is called when image has been captured.
- *
- * @param picture The base64 encoded string of the image
- */
-Camera.prototype.success = function(picture) {
- if (this.successCallback) {
- try {
- this.successCallback(picture);
- }
- catch (e) {
- console.log("Camera error calling user's success callback: " + e);
- }
- }
-};
-
-/**
- * Callback function from native code that is called when there is an error
- * capturing an image, or the capture is cancelled.
- *
- * @param err The error message
- */
-Camera.prototype.error = function(err) {
- if (this.errorCallback) {
- try {
- this.errorCallback(err);
- }
- catch (e) {
- console.log("Camera error calling user's error callback: " + e);
- }
- }
+ PhoneGap.exec(successCallback, errorCallback, "Camera", "takePicture", [quality, destinationType, sourceType]);
};
PhoneGap.addConstructor(function() {
- if (typeof navigator.camera == "undefined") navigator.camera = new Camera();
+ if (typeof navigator.camera === "undefined") {
+ navigator.camera = new Camera();
+ }
});
diff --git a/framework/assets/js/compass.js b/framework/assets/js/compass.js
index eefff79f..ffb16463 100755
--- a/framework/assets/js/compass.js
+++ b/framework/assets/js/compass.js
@@ -20,7 +20,7 @@ function Compass() {
* List of compass watch timers
*/
this.timers = {};
-};
+}
Compass.ERROR_MSG = ["Not running", "Starting", "", "Failed to start"];
@@ -34,13 +34,13 @@ Compass.ERROR_MSG = ["Not running", "Starting", "", "Failed to start"];
Compass.prototype.getCurrentHeading = function(successCallback, errorCallback, options) {
// successCallback required
- if (typeof successCallback != "function") {
+ if (typeof successCallback !== "function") {
console.log("Compass Error: successCallback is not a function");
return;
}
// errorCallback optional
- if (errorCallback && (typeof errorCallback != "function")) {
+ if (errorCallback && (typeof errorCallback !== "function")) {
console.log("Compass Error: errorCallback is not a function");
return;
}
@@ -60,16 +60,16 @@ Compass.prototype.getCurrentHeading = function(successCallback, errorCallback, o
Compass.prototype.watchHeading= function(successCallback, errorCallback, options) {
// Default interval (100 msec)
- var frequency = (options != undefined) ? options.frequency : 100;
+ var frequency = (options !== undefined) ? options.frequency : 100;
// successCallback required
- if (typeof successCallback != "function") {
+ if (typeof successCallback !== "function") {
console.log("Compass Error: successCallback is not a function");
return;
}
// errorCallback optional
- if (errorCallback && (typeof errorCallback != "function")) {
+ if (errorCallback && (typeof errorCallback !== "function")) {
console.log("Compass Error: errorCallback is not a function");
return;
}
@@ -109,5 +109,7 @@ Compass.prototype.clearWatch = function(id) {
};
PhoneGap.addConstructor(function() {
- if (typeof navigator.compass == "undefined") navigator.compass = new Compass();
+ if (typeof navigator.compass === "undefined") {
+ navigator.compass = new Compass();
+ }
});
diff --git a/framework/assets/js/contact.js b/framework/assets/js/contact.js
index 974b9f65..67559c60 100755
--- a/framework/assets/js/contact.js
+++ b/framework/assets/js/contact.js
@@ -17,25 +17,19 @@
* @param {ContactAddress[]} addresses array of addresses
* @param {ContactField[]} ims instant messaging user ids
* @param {ContactOrganization[]} organizations
-* @param {DOMString} published date contact was first created
-* @param {DOMString} updated date contact was last updated
+* @param {DOMString} revision date contact was last updated
* @param {DOMString} birthday contact's birthday
-* @param (DOMString} anniversary contact's anniversary
* @param {DOMString} gender contact's gender
* @param {DOMString} note user notes about contact
-* @param {DOMString} preferredUsername
* @param {ContactField[]} photos
-* @param {ContactField[]} tags
-* @param {ContactField[]} relationships
+* @param {ContactField[]} categories
* @param {ContactField[]} urls contact's web sites
-* @param {ContactAccounts[]} accounts contact's online accounts
-* @param {DOMString} utcOffset UTC time zone offset
-* @param {DOMString} connected
+* @param {DOMString} timezone the contacts time zone
*/
-var Contact = function(id, displayName, name, nickname, phoneNumbers, emails, addresses,
- ims, organizations, published, updated, birthday, anniversary, gender, note,
- preferredUsername, photos, tags, relationships, urls, accounts, utcOffset, connected) {
+var Contact = function (id, displayName, name, nickname, phoneNumbers, emails, addresses,
+ ims, organizations, revision, birthday, gender, note, photos, categories, urls, timezone) {
this.id = id || null;
+ this.rawId = null;
this.displayName = displayName || null;
this.name = name || null; // ContactName
this.nickname = nickname || null;
@@ -44,184 +38,14 @@ var Contact = function(id, displayName, name, nickname, phoneNumbers, emails, ad
this.addresses = addresses || null; // ContactAddress[]
this.ims = ims || null; // ContactField[]
this.organizations = organizations || null; // ContactOrganization[]
- this.published = published || null;
- this.updated = updated || null;
+ this.revision = revision || null;
this.birthday = birthday || null;
- this.anniversary = anniversary || null;
this.gender = gender || null;
this.note = note || null;
- this.preferredUsername = preferredUsername || null;
this.photos = photos || null; // ContactField[]
- this.tags = tags || null; // ContactField[]
- this.relationships = relationships || null; // ContactField[]
+ this.categories = categories || null; // ContactField[]
this.urls = urls || null; // ContactField[]
- this.accounts = accounts || null; // ContactAccount[]
- this.utcOffset = utcOffset || null;
- this.connected = connected || null;
-};
-
-/**
-* Removes contact from device storage.
-* @param successCB success callback
-* @param errorCB error callback
-*/
-Contact.prototype.remove = function(successCB, errorCB) {
- if (this.id == null) {
- var errorObj = new ContactError();
- errorObj.code = ContactError.NOT_FOUND_ERROR;
- errorCB(errorObj);
- }
-
- PhoneGap.exec(successCB, errorCB, "Contacts", "remove", [this.id]);
-};
-
-/**
-* Creates a deep copy of this Contact.
-* With the contact ID set to null.
-* @return copy of this Contact
-*/
-Contact.prototype.clone = function() {
- var clonedContact = PhoneGap.clone(this);
- clonedContact.id = null;
- return clonedContact;
-};
-
-/**
-* Persists contact to device storage.
-* @param successCB success callback
-* @param errorCB error callback
-*/
-Contact.prototype.save = function(successCB, errorCB) {
-};
-
-/**
-* Contact name.
-* @param formatted
-* @param familyName
-* @param givenName
-* @param middle
-* @param prefix
-* @param suffix
-*/
-var ContactName = function(formatted, familyName, givenName, middle, prefix, suffix) {
- this.formatted = formatted || null;
- this.familyName = familyName || null;
- this.givenName = givenName || null;
- this.middleName = middle || null;
- this.honorificPrefix = prefix || null;
- this.honorificSuffix = suffix || null;
-};
-
-/**
-* Generic contact field.
-* @param type
-* @param value
-* @param primary
-*/
-var ContactField = function(type, value, primary) {
- this.type = type || null;
- this.value = value || null;
- this.primary = primary || null;
-};
-
-/**
-* Contact address.
-* @param formatted
-* @param streetAddress
-* @param locality
-* @param region
-* @param postalCode
-* @param country
-*/
-var ContactAddress = function(formatted, streetAddress, locality, region, postalCode, country) {
- this.formatted = formatted || null;
- this.streetAddress = streetAddress || null;
- this.locality = locality || null;
- this.region = region || null;
- this.postalCode = postalCode || null;
- this.country = country || null;
-};
-
-/**
-* Contact organization.
-* @param name
-* @param dept
-* @param title
-* @param startDate
-* @param endDate
-* @param location
-* @param desc
-*/
-var ContactOrganization = function(name, dept, title, startDate, endDate, location, desc) {
- this.name = name || null;
- this.department = dept || null;
- this.title = title || null;
- this.startDate = startDate || null;
- this.endDate = endDate || null;
- this.location = location || null;
- this.description = desc || null;
-};
-
-/**
-* Contact account.
-* @param domain
-* @param username
-* @param userid
-*/
-var ContactAccount = function(domain, username, userid) {
- this.domain = domain || null;
- this.username = username || null;
- this.userid = userid || null;
-}
-
-/**
-* Represents a group of Contacts.
-*/
-var Contacts = function() {
- this.inProgress = false;
- this.records = new Array();
-}
-/**
-* Returns an array of Contacts matching the search criteria.
-* @param fields that should be searched
-* @param successCB success callback
-* @param errorCB error callback
-* @param {ContactFindOptions} options that can be applied to contact searching
-* @return array of Contacts matching search criteria
-*/
-Contacts.prototype.find = function(fields, successCB, errorCB, options) {
- PhoneGap.exec(successCB, errorCB, "Contacts", "search", [fields, options]);
-};
-
-/**
-* This function creates a new contact, but it does not persist the contact
-* to device storage. To persist the contact to device storage, invoke
-* contact.save().
-* @param properties an object who's properties will be examined to create a new Contact
-* @returns new Contact object
-*/
-Contacts.prototype.create = function(properties) {
- var contact = new Contact();
- for (i in properties) {
- if (contact[i]!='undefined') {
- contact[i]=properties[i];
- }
- }
- return contact;
-};
-
-/**
- * ContactFindOptions.
- * @param filter used to match contacts against
- * @param multiple boolean used to determine if more than one contact should be returned
- * @param limit maximum number of results to return from the contacts search
- * @param updatedSince return only contact records that have been updated on or after the given time
- */
-var ContactFindOptions = function(filter, multiple, limit, updatedSince) {
- this.filter = filter || '';
- this.multiple = multiple || false;
- this.limit = limit || 1;
- this.updatedSince = updatedSince || '';
+ this.timezone = timezone || null;
};
/**
@@ -244,10 +68,230 @@ ContactError.IO_ERROR = 5;
ContactError.NOT_SUPPORTED_ERROR = 6;
ContactError.PERMISSION_DENIED_ERROR = 20;
+/**
+* Removes contact from device storage.
+* @param successCB success callback
+* @param errorCB error callback
+*/
+Contact.prototype.remove = function(successCB, errorCB) {
+ if (this.id === null) {
+ var errorObj = new ContactError();
+ errorObj.code = ContactError.NOT_FOUND_ERROR;
+ errorCB(errorObj);
+ }
+ else {
+ PhoneGap.exec(successCB, errorCB, "Contacts", "remove", [this.id]);
+ }
+};
+
+/**
+* Creates a deep copy of this Contact.
+* With the contact ID set to null.
+* @return copy of this Contact
+*/
+Contact.prototype.clone = function() {
+ var clonedContact = PhoneGap.clone(this);
+ var i;
+ clonedContact.id = null;
+ clonedContact.rawId = null;
+ // Loop through and clear out any id's in phones, emails, etc.
+ if (clonedContact.phoneNumbers) {
+ for (i = 0; i < clonedContact.phoneNumbers.length; i++) {
+ clonedContact.phoneNumbers[i].id = null;
+ }
+ }
+ if (clonedContact.emails) {
+ for (i = 0; i < clonedContact.emails.length; i++) {
+ clonedContact.emails[i].id = null;
+ }
+ }
+ if (clonedContact.addresses) {
+ for (i = 0; i < clonedContact.addresses.length; i++) {
+ clonedContact.addresses[i].id = null;
+ }
+ }
+ if (clonedContact.ims) {
+ for (i = 0; i < clonedContact.ims.length; i++) {
+ clonedContact.ims[i].id = null;
+ }
+ }
+ if (clonedContact.organizations) {
+ for (i = 0; i < clonedContact.organizations.length; i++) {
+ clonedContact.organizations[i].id = null;
+ }
+ }
+ if (clonedContact.tags) {
+ for (i = 0; i < clonedContact.tags.length; i++) {
+ clonedContact.tags[i].id = null;
+ }
+ }
+ if (clonedContact.photos) {
+ for (i = 0; i < clonedContact.photos.length; i++) {
+ clonedContact.photos[i].id = null;
+ }
+ }
+ if (clonedContact.urls) {
+ for (i = 0; i < clonedContact.urls.length; i++) {
+ clonedContact.urls[i].id = null;
+ }
+ }
+ return clonedContact;
+};
+
+/**
+* Persists contact to device storage.
+* @param successCB success callback
+* @param errorCB error callback
+*/
+Contact.prototype.save = function(successCB, errorCB) {
+ PhoneGap.exec(successCB, errorCB, "Contacts", "save", [this]);
+};
+
+/**
+* Contact name.
+* @param formatted
+* @param familyName
+* @param givenName
+* @param middle
+* @param prefix
+* @param suffix
+*/
+var ContactName = function(formatted, familyName, givenName, middle, prefix, suffix) {
+ this.formatted = formatted || null;
+ this.familyName = familyName || null;
+ this.givenName = givenName || null;
+ this.middleName = middle || null;
+ this.honorificPrefix = prefix || null;
+ this.honorificSuffix = suffix || null;
+};
+
+/**
+* Generic contact field.
+* @param {DOMString} id unique identifier, should only be set by native code
+* @param type
+* @param value
+* @param pref
+*/
+var ContactField = function(type, value, pref) {
+ this.id = null;
+ this.type = type || null;
+ this.value = value || null;
+ this.pref = pref || null;
+};
+
+/**
+* Contact address.
+* @param {DOMString} id unique identifier, should only be set by native code
+* @param formatted
+* @param streetAddress
+* @param locality
+* @param region
+* @param postalCode
+* @param country
+*/
+var ContactAddress = function(formatted, streetAddress, locality, region, postalCode, country) {
+ this.id = null;
+ this.formatted = formatted || null;
+ this.streetAddress = streetAddress || null;
+ this.locality = locality || null;
+ this.region = region || null;
+ this.postalCode = postalCode || null;
+ this.country = country || null;
+};
+
+/**
+* Contact organization.
+* @param {DOMString} id unique identifier, should only be set by native code
+* @param name
+* @param dept
+* @param title
+* @param startDate
+* @param endDate
+* @param location
+* @param desc
+*/
+var ContactOrganization = function(name, dept, title) {
+ this.id = null;
+ this.name = name || null;
+ this.department = dept || null;
+ this.title = title || null;
+};
+
+/**
+* Represents a group of Contacts.
+*/
+var Contacts = function() {
+ this.inProgress = false;
+ this.records = [];
+};
+/**
+* Returns an array of Contacts matching the search criteria.
+* @param fields that should be searched
+* @param successCB success callback
+* @param errorCB error callback
+* @param {ContactFindOptions} options that can be applied to contact searching
+* @return array of Contacts matching search criteria
+*/
+Contacts.prototype.find = function(fields, successCB, errorCB, options) {
+ PhoneGap.exec(successCB, errorCB, "Contacts", "search", [fields, options]);
+};
+
+/**
+* This function creates a new contact, but it does not persist the contact
+* to device storage. To persist the contact to device storage, invoke
+* contact.save().
+* @param properties an object who's properties will be examined to create a new Contact
+* @returns new Contact object
+*/
+Contacts.prototype.create = function(properties) {
+ var i;
+ var contact = new Contact();
+ for (i in properties) {
+ if (contact[i] !== 'undefined') {
+ contact[i] = properties[i];
+ }
+ }
+ return contact;
+};
+
+/**
+* This function returns and array of contacts. It is required as we need to convert raw
+* JSON objects into concrete Contact objects. Currently this method is called after
+* navigator.service.contacts.find but before the find methods success call back.
+*
+* @param jsonArray an array of JSON Objects that need to be converted to Contact objects.
+* @returns an array of Contact objects
+*/
+Contacts.prototype.cast = function(pluginResult) {
+ var contacts = [];
+ var i;
+ for (i=0; i][;base64],
*
- * @param file The name of the file
+ * @param file {File} File object containing file properties
*/
FileReader.prototype.readAsDataURL = function(file) {
- this.fileName = file;
+ this.fileName = "";
+ if (typeof file.fullPath === "undefined") {
+ this.fileName = file;
+ } else {
+ this.fileName = file.fullPath;
+ }
// LOADING state
this.readyState = FileReader.LOADING;
// If loadstart callback
- if (typeof this.onloadstart == "function") {
- var evt = File._createEvent("loadstart", this);
- this.onloadstart(evt);
+ if (typeof this.onloadstart === "function") {
+ this.onloadstart({"type":"loadstart", "target":this});
}
var me = this;
// Read file
- navigator.fileMgr.readAsDataURL(file,
+ navigator.fileMgr.readAsDataURL(this.fileName,
// Success callback
function(r) {
+ var evt;
// If DONE (cancelled), then don't do anything
- if (me.readyState == FileReader.DONE) {
+ if (me.readyState === FileReader.DONE) {
return;
}
// Save result
me.result = r;
+ // If onload callback
+ if (typeof me.onload === "function") {
+ me.onload({"type":"load", "target":me});
+ }
+
// DONE state
me.readyState = FileReader.DONE;
- // If onload callback
- if (typeof me.onload == "function") {
- var evt = File._createEvent("load", me);
- me.onload(evt);
- }
-
// If onloadend callback
- if (typeof me.onloadend == "function") {
- var evt = File._createEvent("loadend", me);
- me.onloadend(evt);
+ if (typeof me.onloadend === "function") {
+ me.onloadend({"type":"loadend", "target":me});
}
},
// Error callback
function(e) {
-
+ var evt;
// If DONE (cancelled), then don't do anything
- if (me.readyState == FileReader.DONE) {
+ if (me.readyState === FileReader.DONE) {
return;
}
// Save error
me.error = e;
+ // If onerror callback
+ if (typeof me.onerror === "function") {
+ me.onerror({"type":"error", "target":me});
+ }
+
// DONE state
me.readyState = FileReader.DONE;
- // If onerror callback
- if (typeof me.onerror == "function") {
- var evt = File._createEvent("error", me);
- me.onerror(evt);
- }
-
// If onloadend callback
- if (typeof me.onloadend == "function") {
- var evt = File._createEvent("loadend", me);
- me.onloadend(evt);
+ if (typeof me.onloadend === "function") {
+ me.onloadend({"type":"loadend", "target":me});
}
}
);
@@ -330,13 +348,23 @@ FileReader.prototype.readAsDataURL = function(file) {
/**
* Read file and return data as a binary data.
*
- * @param file The name of the file
+ * @param file {File} File object containing file properties
*/
FileReader.prototype.readAsBinaryString = function(file) {
// TODO - Can't return binary data to browser.
this.fileName = file;
};
+/**
+ * Read file and return data as a binary data.
+ *
+ * @param file {File} File object containing file properties
+ */
+FileReader.prototype.readAsArrayBuffer = function(file) {
+ // TODO - Can't return binary data to browser.
+ this.fileName = file;
+};
+
//-----------------------------------------------------------------------------
// File Writer
//-----------------------------------------------------------------------------
@@ -347,78 +375,676 @@ FileReader.prototype.readAsBinaryString = function(file) {
* For Android:
* The root directory is the root of the file system.
* To write to the SD card, the file name is "sdcard/my_file.txt"
+ *
+ * @param file {File} File object containing file properties
+ * @param append if true write to the end of the file, otherwise overwrite the file
*/
-function FileWriter() {
+function FileWriter(file) {
this.fileName = "";
- this.result = null;
+ this.length = 0;
+ if (file) {
+ this.fileName = file.fullPath || file;
+ this.length = file.size || 0;
+ }
+ // default is to write at the beginning of the file
+ this.position = 0;
+
this.readyState = 0; // EMPTY
+
this.result = null;
- this.onerror = null;
- this.oncomplete = null;
-};
+
+ // Error
+ this.error = null;
+
+ // Event handlers
+ this.onwritestart = null; // When writing starts
+ this.onprogress = null; // While writing the file, and reporting partial file data
+ this.onwrite = null; // When the write has successfully completed.
+ this.onwriteend = null; // When the request has completed (either in success or failure).
+ this.onabort = null; // When the write has been aborted. For instance, by invoking the abort() method.
+ this.onerror = null; // When the write has failed (see errors).
+}
// States
-FileWriter.EMPTY = 0;
-FileWriter.LOADING = 1;
+FileWriter.INIT = 0;
+FileWriter.WRITING = 1;
FileWriter.DONE = 2;
-FileWriter.prototype.writeAsText = function(file, text, bAppend) {
- if (bAppend != true) {
- bAppend = false; // for null values
+/**
+ * Abort writing file.
+ */
+FileWriter.prototype.abort = function() {
+ // check for invalid state
+ if (this.readyState === FileWriter.DONE || this.readyState === FileWriter.INIT) {
+ throw FileError.INVALID_STATE_ERR;
+ }
+
+ // set error
+ var error = new FileError(), evt;
+ error.code = error.ABORT_ERR;
+ this.error = error;
+
+ // If error callback
+ if (typeof this.onerror === "function") {
+ this.onerror({"type":"error", "target":this});
}
+ // If abort callback
+ if (typeof this.onabort === "function") {
+ this.oneabort({"type":"abort", "target":this});
+ }
+
+ this.readyState = FileWriter.DONE;
- this.fileName = file;
+ // If write end callback
+ if (typeof this.onwriteend == "function") {
+ this.onwriteend({"type":"writeend", "target":this});
+ }
+};
- // LOADING state
- this.readyState = FileWriter.LOADING;
+/**
+ * Writes data to the file
+ *
+ * @param text to be written
+ */
+FileWriter.prototype.write = function(text) {
+ // Throw an exception if we are already writing a file
+ if (this.readyState === FileWriter.WRITING) {
+ throw FileError.INVALID_STATE_ERR;
+ }
+
+ // WRITING state
+ this.readyState = FileWriter.WRITING;
var me = this;
- // Read file
- navigator.fileMgr.writeAsText(file, text, bAppend,
+ // If onwritestart callback
+ if (typeof me.onwritestart === "function") {
+ me.onwritestart({"type":"writestart", "target":me});
+ }
+
+ // Write file
+ navigator.fileMgr.write(this.fileName, text, this.position,
// Success callback
function(r) {
-
+ var evt;
// If DONE (cancelled), then don't do anything
- if (me.readyState == FileWriter.DONE) {
+ if (me.readyState === FileWriter.DONE) {
return;
}
- // Save result
- me.result = r;
+ // So if the user wants to keep appending to the file
+ me.length = Math.max(me.length, me.position + r);
+ // position always increases by bytes written because file would be extended
+ me.position += r;
+
+ // If onwrite callback
+ if (typeof me.onwrite === "function") {
+ me.onwrite({"type":"write", "target":me});
+ }
// DONE state
me.readyState = FileWriter.DONE;
- // If oncomplete callback
- if (typeof me.oncomplete == "function") {
- var evt = File._createEvent("complete", me);
- me.oncomplete(evt);
+ // If onwriteend callback
+ if (typeof me.onwriteend === "function") {
+ me.onwriteend({"type":"writeend", "target":me});
}
},
// Error callback
function(e) {
+ var evt;
// If DONE (cancelled), then don't do anything
- if (me.readyState == FileWriter.DONE) {
+ if (me.readyState === FileWriter.DONE) {
return;
}
// Save error
me.error = e;
+ // If onerror callback
+ if (typeof me.onerror === "function") {
+ me.onerror({"type":"error", "target":me});
+ }
+
// DONE state
me.readyState = FileWriter.DONE;
- // If onerror callback
- if (typeof me.onerror == "function") {
- var evt = File._createEvent("error", me);
- me.onerror(evt);
+ // If onwriteend callback
+ if (typeof me.onwriteend === "function") {
+ me.onwriteend({"type":"writeend", "target":me});
}
}
);
};
+/**
+ * Moves the file pointer to the location specified.
+ *
+ * If the offset is a negative number the position of the file
+ * pointer is rewound. If the offset is greater than the file
+ * size the position is set to the end of the file.
+ *
+ * @param offset is the location to move the file pointer to.
+ */
+FileWriter.prototype.seek = function(offset) {
+ // Throw an exception if we are already writing a file
+ if (this.readyState === FileWriter.WRITING) {
+ throw FileError.INVALID_STATE_ERR;
+ }
+
+ if (!offset) {
+ return;
+ }
+
+ // See back from end of file.
+ if (offset < 0) {
+ this.position = Math.max(offset + this.length, 0);
+ }
+ // Offset is bigger then file size so set position
+ // to the end of the file.
+ else if (offset > this.length) {
+ this.position = this.length;
+ }
+ // Offset is between 0 and file size so set the position
+ // to start writing.
+ else {
+ this.position = offset;
+ }
+};
+
+/**
+ * Truncates the file to the size specified.
+ *
+ * @param size to chop the file at.
+ */
+FileWriter.prototype.truncate = function(size) {
+ // Throw an exception if we are already writing a file
+ if (this.readyState === FileWriter.WRITING) {
+ throw FileError.INVALID_STATE_ERR;
+ }
+
+ // WRITING state
+ this.readyState = FileWriter.WRITING;
+
+ var me = this;
+
+ // If onwritestart callback
+ if (typeof me.onwritestart === "function") {
+ me.onwritestart({"type":"writestart", "target":this});
+ }
+
+ // Write file
+ navigator.fileMgr.truncate(this.fileName, size,
+
+ // Success callback
+ function(r) {
+ var evt;
+ // If DONE (cancelled), then don't do anything
+ if (me.readyState === FileWriter.DONE) {
+ return;
+ }
+
+ // Update the length of the file
+ me.length = r;
+ me.position = Math.min(me.position, r);
+
+ // If onwrite callback
+ if (typeof me.onwrite === "function") {
+ me.onwrite({"type":"write", "target":me});
+ }
+
+ // DONE state
+ me.readyState = FileWriter.DONE;
+
+ // If onwriteend callback
+ if (typeof me.onwriteend === "function") {
+ me.onwriteend({"type":"writeend", "target":me});
+ }
+ },
+
+ // Error callback
+ function(e) {
+ var evt;
+ // If DONE (cancelled), then don't do anything
+ if (me.readyState === FileWriter.DONE) {
+ return;
+ }
+
+ // Save error
+ me.error = e;
+
+ // If onerror callback
+ if (typeof me.onerror === "function") {
+ me.onerror({"type":"error", "target":me});
+ }
+
+ // DONE state
+ me.readyState = FileWriter.DONE;
+
+ // If onwriteend callback
+ if (typeof me.onwriteend === "function") {
+ me.onwriteend({"type":"writeend", "target":me});
+ }
+ }
+ );
+};
+
+function LocalFileSystem() {
+};
+
+// File error codes
+LocalFileSystem.TEMPORARY = 0;
+LocalFileSystem.PERSISTENT = 1;
+LocalFileSystem.RESOURCE = 2;
+LocalFileSystem.APPLICATION = 3;
+
+/**
+ * Requests a filesystem in which to store application data.
+ *
+ * @param {int} type of file system being requested
+ * @param {Function} successCallback is called with the new FileSystem
+ * @param {Function} errorCallback is called with a FileError
+ */
+LocalFileSystem.prototype.requestFileSystem = function(type, size, successCallback, errorCallback) {
+ if (type < 0 || type > 3) {
+ if (typeof errorCallback == "function") {
+ errorCallback({
+ "code": FileError.SYNTAX_ERR
+ });
+ }
+ }
+ else {
+ PhoneGap.exec(successCallback, errorCallback, "File", "requestFileSystem", [type, size]);
+ }
+};
+
+/**
+ *
+ * @param {DOMString} uri referring to a local file in a filesystem
+ * @param {Function} successCallback is called with the new entry
+ * @param {Function} errorCallback is called with a FileError
+ */
+LocalFileSystem.prototype.resolveLocalFileSystemURI = function(uri, successCallback, errorCallback) {
+ PhoneGap.exec(successCallback, errorCallback, "File", "resolveLocalFileSystemURI", [uri]);
+};
+
+/**
+* This function returns and array of contacts. It is required as we need to convert raw
+* JSON objects into concrete Contact objects. Currently this method is called after
+* navigator.service.contacts.find but before the find methods success call back.
+*
+* @param a JSON Objects that need to be converted to DirectoryEntry or FileEntry objects.
+* @returns an entry
+*/
+LocalFileSystem.prototype._castFS = function(pluginResult) {
+ var entry = null;
+ entry = new DirectoryEntry();
+ entry.isDirectory = pluginResult.message.root.isDirectory;
+ entry.isFile = pluginResult.message.root.isFile;
+ entry.name = pluginResult.message.root.name;
+ entry.fullPath = pluginResult.message.root.fullPath;
+ pluginResult.message.root = entry;
+ return pluginResult;
+}
+
+LocalFileSystem.prototype._castEntry = function(pluginResult) {
+ var entry = null;
+ if (pluginResult.message.isDirectory) {
+ console.log("This is a dir");
+ entry = new DirectoryEntry();
+ }
+ else if (pluginResult.message.isFile) {
+ console.log("This is a file");
+ entry = new FileEntry();
+ }
+ entry.isDirectory = pluginResult.message.isDirectory;
+ entry.isFile = pluginResult.message.isFile;
+ entry.name = pluginResult.message.name;
+ entry.fullPath = pluginResult.message.fullPath;
+ pluginResult.message = entry;
+ return pluginResult;
+}
+
+LocalFileSystem.prototype._castEntries = function(pluginResult) {
+ var entries = pluginResult.message;
+ var retVal = [];
+ for (i=0; i
+ *
+ * @param name The plugin name
+ * @param obj The plugin object
*/
PhoneGap.addPlugin = function(name, obj) {
- if ( !window.plugins ) {
- window.plugins = {};
- }
-
- if ( !window.plugins[name] ) {
- window.plugins[name] = obj;
- }
-}
+ if (!window.plugins[name]) {
+ window.plugins[name] = obj;
+ }
+ else {
+ console.log("Error: Plugin "+name+" already exists.");
+ }
+};
/**
* onDOMContentLoaded channel is fired when the DOM content
@@ -232,13 +253,59 @@ if (typeof _nativeReady !== 'undefined') { PhoneGap.onNativeReady.fire(); }
PhoneGap.onDeviceReady = new PhoneGap.Channel('onDeviceReady');
+// Array of channels that must fire before "deviceready" is fired
+PhoneGap.deviceReadyChannelsArray = [ PhoneGap.onPhoneGapReady, PhoneGap.onPhoneGapInfoReady];
+
+// Hashtable of user defined channels that must also fire before "deviceready" is fired
+PhoneGap.deviceReadyChannelsMap = {};
+
+/**
+ * Indicate that a feature needs to be initialized before it is ready to be used.
+ * This holds up PhoneGap's "deviceready" event until the feature has been initialized
+ * and PhoneGap.initComplete(feature) is called.
+ *
+ * @param feature {String} The unique feature name
+ */
+PhoneGap.waitForInitialization = function(feature) {
+ if (feature) {
+ var channel = new PhoneGap.Channel(feature);
+ PhoneGap.deviceReadyChannelsMap[feature] = channel;
+ PhoneGap.deviceReadyChannelsArray.push(channel);
+ }
+};
+
+/**
+ * Indicate that initialization code has completed and the feature is ready to be used.
+ *
+ * @param feature {String} The unique feature name
+ */
+PhoneGap.initializationComplete = function(feature) {
+ var channel = PhoneGap.deviceReadyChannelsMap[feature];
+ if (channel) {
+ channel.fire();
+ }
+};
+
/**
* Create all PhoneGap objects once page has fully loaded and native side is ready.
*/
PhoneGap.Channel.join(function() {
// Start listening for XHR callbacks
- PhoneGap.JSCallback();
+ setTimeout(function() {
+ if (PhoneGap.UsePolling) {
+ PhoneGap.JSCallbackPolling();
+ }
+ else {
+ var polling = prompt("usePolling", "gap_callbackServer:");
+ if (polling == "true") {
+ PhoneGap.JSCallbackPolling();
+ }
+ else {
+ PhoneGap.JSCallback();
+ }
+ }
+ }, 1);
// Run PhoneGap constructors
PhoneGap.onPhoneGapInit.fire();
@@ -246,22 +313,21 @@ PhoneGap.Channel.join(function() {
// Fire event to notify that all objects are created
PhoneGap.onPhoneGapReady.fire();
+ // Fire onDeviceReady event once all constructors have run and PhoneGap info has been
+ // received from native side, and any user defined initialization channels.
+ PhoneGap.Channel.join(function() {
+
+ // Turn off app loading dialog
+ navigator.notification.activityStop();
+
+ PhoneGap.onDeviceReady.fire();
+
+ // Fire the onresume event, since first one happens before JavaScript is loaded
+ PhoneGap.onResume.fire();
+ }, PhoneGap.deviceReadyChannelsArray);
+
}, [ PhoneGap.onDOMContentLoaded, PhoneGap.onNativeReady ]);
-/**
- * Fire onDeviceReady event once all constructors have run and PhoneGap info has been
- * received from native side.
- */
-PhoneGap.Channel.join(function() {
- // Turn off app loading dialog
- navigator.notification.activityStop();
-
- PhoneGap.onDeviceReady.fire();
-
- // Fire the onresume event, since first one happens before JavaScript is loaded
- PhoneGap.onResume.fire();
-}, [ PhoneGap.onPhoneGapReady, PhoneGap.onPhoneGapInfoReady]);
-
// Listen for DOMContentLoaded and notify our channel subscribers
document.addEventListener('DOMContentLoaded', function() {
PhoneGap.onDOMContentLoaded.fire();
@@ -272,17 +338,50 @@ PhoneGap.m_document_addEventListener = document.addEventListener;
document.addEventListener = function(evt, handler, capture) {
var e = evt.toLowerCase();
- if (e == 'deviceready') {
+ if (e === 'deviceready') {
PhoneGap.onDeviceReady.subscribeOnce(handler);
- } else if (e == 'resume') {
+ } else if (e === 'resume') {
PhoneGap.onResume.subscribe(handler);
- } else if (e == 'pause') {
+ if (PhoneGap.onDeviceReady.fired) {
+ PhoneGap.onResume.fire();
+ }
+ } else if (e === 'pause') {
PhoneGap.onPause.subscribe(handler);
- } else {
- PhoneGap.m_document_addEventListener.call(document, evt, handler);
+ }
+ else {
+ // If subscribing to Android backbutton
+ if (e === 'backbutton') {
+ PhoneGap.exec(null, null, "App", "overrideBackbutton", [true]);
+ }
+
+ PhoneGap.m_document_addEventListener.call(document, evt, handler, capture);
}
};
+// Intercept calls to document.removeEventListener and watch for events that
+// are generated by PhoneGap native code
+PhoneGap.m_document_removeEventListener = document.removeEventListener;
+
+document.removeEventListener = function(evt, handler, capture) {
+ var e = evt.toLowerCase();
+
+ // If unsubscribing to Android backbutton
+ if (e === 'backbutton') {
+ PhoneGap.exec(null, null, "App", "overrideBackbutton", [false]);
+ }
+
+ PhoneGap.m_document_removeEventListener.call(document, evt, handler, capture);
+};
+
+/**
+ * Method to fire event from native code
+ */
+PhoneGap.fireEvent = function(type) {
+ var e = document.createEvent('Events');
+ e.initEvent(type);
+ document.dispatchEvent(e);
+};
+
/**
* If JSON not included, use our own stringify. (Android 1.6)
* The restriction on ours is that it must be an array of simple types.
@@ -291,48 +390,53 @@ document.addEventListener = function(evt, handler, capture) {
* @return
*/
PhoneGap.stringify = function(args) {
- if (typeof JSON == "undefined") {
+ if (typeof JSON === "undefined") {
var s = "[";
- for (var i=0; i 0) {
- s = s + ",";
- }
- var type = typeof args[i];
- if ((type == "number") || (type == "boolean")) {
- s = s + args[i];
- }
- else if (args[i] instanceof Array) {
- s = s + "[" + args[i] + "]";
- }
- else if (args[i] instanceof Object) {
- var start = true;
- s = s + '{';
- for (var name in args[i]) {
- if (!start) {
- s = s + ',';
- }
- s = s + '"' + name + '":';
- var nameType = typeof args[i][name];
- if ((nameType == "number") || (nameType == "boolean")) {
- s = s + args[i][name];
- }
- else {
- s = s + '"' + args[i][name] + '"';
- }
- start=false;
- }
- s = s + '}';
- }
- else {
- var a = args[i].replace(/\\/g, '\\\\');
- a = a.replace(/"/g, '\\"');
- s = s + '"' + a + '"';
+ var i, type, start, name, nameType, a;
+ for (i = 0; i < args.length; i++) {
+ if (args[i] != null) {
+ if (i > 0) {
+ s = s + ",";
+ }
+ type = typeof args[i];
+ if ((type === "number") || (type === "boolean")) {
+ s = s + args[i];
+ } else if (args[i] instanceof Array) {
+ s = s + "[" + args[i] + "]";
+ } else if (args[i] instanceof Object) {
+ start = true;
+ s = s + '{';
+ for (name in args[i]) {
+ if (args[i][name] !== null) {
+ if (!start) {
+ s = s + ',';
+ }
+ s = s + '"' + name + '":';
+ nameType = typeof args[i][name];
+ if ((nameType === "number") || (nameType === "boolean")) {
+ s = s + args[i][name];
+ } else if ((typeof args[i][name]) === 'function') {
+ // don't copy the functions
+ s = s + '""';
+ } else if (args[i][name] instanceof Object) {
+ s = s + this.stringify(args[i][name]);
+ } else {
+ s = s + '"' + args[i][name] + '"';
+ }
+ start = false;
+ }
+ }
+ s = s + '}';
+ } else {
+ a = args[i].replace(/\\/g, '\\\\');
+ a = a.replace(/"/g, '\\"');
+ s = s + '"' + a + '"';
+ }
}
}
s = s + "]";
return s;
- }
- else {
+ } else {
return JSON.stringify(args);
}
};
@@ -344,13 +448,14 @@ PhoneGap.stringify = function(args) {
* @return
*/
PhoneGap.clone = function(obj) {
+ var i, retVal;
if(!obj) {
return obj;
}
if(obj instanceof Array){
- var retVal = new Array();
- for(var i = 0; i < obj.length; ++i){
+ retVal = [];
+ for(i = 0; i < obj.length; ++i){
retVal.push(PhoneGap.clone(obj[i]));
}
return retVal;
@@ -363,10 +468,14 @@ PhoneGap.clone = function(obj) {
if(!(obj instanceof Object)){
return obj;
}
-
- retVal = new Object();
+
+ if (obj instanceof Date) {
+ return obj;
+ }
+
+ retVal = {};
for(i in obj){
- if(!(i in retVal) || retVal[i] != obj[i]) {
+ if(!(i in retVal) || retVal[i] !== obj[i]) {
retVal[i] = PhoneGap.clone(obj[i]);
}
}
@@ -375,6 +484,19 @@ PhoneGap.clone = function(obj) {
PhoneGap.callbackId = 0;
PhoneGap.callbacks = {};
+PhoneGap.callbackStatus = {
+ NO_RESULT: 0,
+ OK: 1,
+ CLASS_NOT_FOUND_EXCEPTION: 2,
+ ILLEGAL_ACCESS_EXCEPTION: 3,
+ INSTANTIATION_EXCEPTION: 4,
+ MALFORMED_URL_EXCEPTION: 5,
+ IO_EXCEPTION: 6,
+ INVALID_ACTION: 7,
+ JSON_EXCEPTION: 8,
+ ERROR: 9
+ };
+
/**
* Execute a PhoneGap command. It is up to the native side whether this action is synch or async.
@@ -397,38 +519,64 @@ PhoneGap.exec = function(success, fail, service, action, args) {
PhoneGap.callbacks[callbackId] = {success:success, fail:fail};
}
- // Note: Device returns string, but for some reason emulator returns object - so convert to string.
- var r = ""+PluginManager.exec(service, action, callbackId, this.stringify(args), true);
+ var r = prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true]));
// If a result was returned
if (r.length > 0) {
eval("var v="+r+";");
// If status is OK, then return value back to caller
- if (v.status == 0) {
+ if (v.status === PhoneGap.callbackStatus.OK) {
- // If there is a success callback, then call it now with returned value
+ // If there is a success callback, then call it now with
+ // returned value
if (success) {
- success(v.message);
- delete PhoneGap.callbacks[callbackId];
+ try {
+ success(v.message);
+ } catch (e) {
+ console.log("Error in success callback: " + callbackId + " = " + e);
+ }
+
+ // Clear callback if not expecting any more results
+ if (!v.keepCallback) {
+ delete PhoneGap.callbacks[callbackId];
+ }
}
return v.message;
}
+ // If no result
+ else if (v.status === PhoneGap.callbackStatus.NO_RESULT) {
+
+ // Clear callback if not expecting any more results
+ if (!v.keepCallback) {
+ delete PhoneGap.callbacks[callbackId];
+ }
+ }
+
// If error, then display error
else {
- console.log("Error: Status="+r.status+" Message="+v.message);
+ console.log("Error: Status="+v.status+" Message="+v.message);
// If there is a fail callback, then call it now with returned value
if (fail) {
- fail(v.message);
- delete PhoneGap.callbacks[callbackId];
+ try {
+ fail(v.message);
+ }
+ catch (e1) {
+ console.log("Error in error callback: "+callbackId+" = "+e1);
+ }
+
+ // Clear callback if not expecting any more results
+ if (!v.keepCallback) {
+ delete PhoneGap.callbacks[callbackId];
+ }
}
return null;
}
}
- } catch (e) {
- console.log("Error: "+e);
+ } catch (e2) {
+ console.log("Error: "+e2);
}
};
@@ -440,15 +588,23 @@ PhoneGap.exec = function(success, fail, service, action, args) {
*/
PhoneGap.callbackSuccess = function(callbackId, args) {
if (PhoneGap.callbacks[callbackId]) {
- try {
- if (PhoneGap.callbacks[callbackId].success) {
- PhoneGap.callbacks[callbackId].success(args.message);
+
+ // If result is to be sent to callback
+ if (args.status === PhoneGap.callbackStatus.OK) {
+ try {
+ if (PhoneGap.callbacks[callbackId].success) {
+ PhoneGap.callbacks[callbackId].success(args.message);
+ }
+ }
+ catch (e) {
+ console.log("Error in success callback: "+callbackId+" = "+e);
}
}
- catch (e) {
- console.log("Error in success callback: "+callbackId+" = "+e);
+
+ // Clear callback if not expecting any more results
+ if (!args.keepCallback) {
+ delete PhoneGap.callbacks[callbackId];
}
- delete PhoneGap.callbacks[callbackId];
}
};
@@ -468,7 +624,11 @@ PhoneGap.callbackError = function(callbackId, args) {
catch (e) {
console.log("Error in error callback: "+callbackId+" = "+e);
}
- delete PhoneGap.callbacks[callbackId];
+
+ // Clear callback if not expecting any more results
+ if (!args.keepCallback) {
+ delete PhoneGap.callbacks[callbackId];
+ }
}
};
@@ -482,43 +642,51 @@ PhoneGap.callbackError = function(callbackId, args) {
*/
// TODO: Is this used?
PhoneGap.run_command = function() {
- if (!PhoneGap.available || !PhoneGap.queue.ready)
+ if (!PhoneGap.available || !PhoneGap.queue.ready) {
return;
-
+ }
PhoneGap.queue.ready = false;
var args = PhoneGap.queue.commands.shift();
- if (PhoneGap.queue.commands.length == 0) {
+ if (PhoneGap.queue.commands.length === 0) {
clearInterval(PhoneGap.queue.timer);
PhoneGap.queue.timer = null;
}
var uri = [];
var dict = null;
- for (var i = 1; i < args.length; i++) {
+ var i;
+ for (i = 1; i < args.length; i++) {
var arg = args[i];
- if (arg == undefined || arg == null)
+ if (arg === undefined || arg === null) {
arg = '';
- if (typeof(arg) == 'object')
+ }
+ if (typeof(arg) === 'object') {
dict = arg;
- else
+ } else {
uri.push(encodeURIComponent(arg));
+ }
}
var url = "gap://" + args[0] + "/" + uri.join("/");
- if (dict != null) {
+ if (dict !== null) {
+ var name;
var query_args = [];
- for (var name in dict) {
- if (typeof(name) != 'string')
- continue;
- query_args.push(encodeURIComponent(name) + "=" + encodeURIComponent(dict[name]));
+ for (name in dict) {
+ if (dict.hasOwnProperty(name) && (typeof (name) === 'string')) {
+ query_args.push(encodeURIComponent(name) + "=" + encodeURIComponent(dict[name]));
+ }
}
- if (query_args.length > 0)
+ if (query_args.length > 0) {
url += "?" + query_args.join("&");
+ }
}
document.location = url;
};
+PhoneGap.JSCallbackPort = null;
+PhoneGap.JSCallbackToken = null;
+
/**
* This is only for Android.
*
@@ -527,14 +695,21 @@ PhoneGap.run_command = function() {
* Java to JavaScript.
*/
PhoneGap.JSCallback = function() {
+
+ // If polling flag was changed, start using polling from now on
+ if (PhoneGap.UsePolling) {
+ PhoneGap.JSCallbackPolling();
+ return;
+ }
+
var xmlhttp = new XMLHttpRequest();
// Callback function when XMLHttpRequest is ready
xmlhttp.onreadystatechange=function(){
- if(xmlhttp.readyState == 4){
+ if(xmlhttp.readyState === 4){
// If callback has JavaScript statement to execute
- if (xmlhttp.status == 200) {
+ if (xmlhttp.status === 200) {
var msg = xmlhttp.responseText;
setTimeout(function() {
@@ -542,6 +717,8 @@ PhoneGap.JSCallback = function() {
var t = eval(msg);
}
catch (e) {
+ // If we're getting an error here, seeing the message will help in debugging
+ console.log("JSCallback: Message from Server: " + msg);
console.log("JSCallback Error: "+e);
}
}, 1);
@@ -549,21 +726,88 @@ PhoneGap.JSCallback = function() {
}
// If callback ping (used to keep XHR request from timing out)
- else if (xmlhttp.status == 404) {
+ else if (xmlhttp.status === 404) {
setTimeout(PhoneGap.JSCallback, 10);
}
+ // If security error
+ else if (xmlhttp.status === 403) {
+ console.log("JSCallback Error: Invalid token. Stopping callbacks.");
+ }
+
+ // If server is stopping
+ else if (xmlhttp.status === 503) {
+ console.log("JSCallback Error: Service unavailable. Stopping callbacks.");
+ }
+
+ // If request wasn't GET
+ else if (xmlhttp.status === 400) {
+ console.log("JSCallback Error: Bad request. Stopping callbacks.");
+ }
+
// If error, restart callback server
else {
console.log("JSCallback Error: Request failed.");
- CallbackServer.restartServer();
+ prompt("restartServer", "gap_callbackServer:");
+ PhoneGap.JSCallbackPort = null;
+ PhoneGap.JSCallbackToken = null;
setTimeout(PhoneGap.JSCallback, 100);
}
}
+ };
+
+ if (PhoneGap.JSCallbackPort === null) {
+ PhoneGap.JSCallbackPort = prompt("getPort", "gap_callbackServer:");
+ }
+ if (PhoneGap.JSCallbackToken === null) {
+ PhoneGap.JSCallbackToken = prompt("getToken", "gap_callbackServer:");
+ }
+ xmlhttp.open("GET", "http://127.0.0.1:"+PhoneGap.JSCallbackPort+"/"+PhoneGap.JSCallbackToken , true);
+ xmlhttp.send();
+};
+
+/**
+ * The polling period to use with JSCallbackPolling.
+ * This can be changed by the application. The default is 50ms.
+ */
+PhoneGap.JSCallbackPollingPeriod = 50;
+
+/**
+ * Flag that can be set by the user to force polling to be used or force XHR to be used.
+ */
+PhoneGap.UsePolling = false; // T=use polling, F=use XHR
+
+/**
+ * This is only for Android.
+ *
+ * Internal function that uses polling to call into PhoneGap Java code and retrieve
+ * any JavaScript code that needs to be run. This is used for callbacks from
+ * Java to JavaScript.
+ */
+PhoneGap.JSCallbackPolling = function() {
+
+ // If polling flag was changed, stop using polling from now on
+ if (!PhoneGap.UsePolling) {
+ PhoneGap.JSCallback();
+ return;
}
- xmlhttp.open("GET", "http://127.0.0.1:"+CallbackServer.getPort()+"/" , true);
- xmlhttp.send();
+ var msg = prompt("", "gap_poll:");
+ if (msg) {
+ setTimeout(function() {
+ try {
+ var t = eval(""+msg);
+ }
+ catch (e) {
+ console.log("JSCallbackPolling: Message from Server: " + msg);
+ console.log("JSCallbackPolling Error: "+e);
+ }
+ }, 1);
+ setTimeout(PhoneGap.JSCallbackPolling, 1);
+ }
+ else {
+ setTimeout(PhoneGap.JSCallbackPolling, PhoneGap.JSCallbackPollingPeriod);
+ }
};
/**
@@ -581,9 +825,10 @@ PhoneGap.createUUID = function() {
PhoneGap.UUIDcreatePart = function(length) {
var uuidpart = "";
- for (var i=0; i
+
+
+
+
+
+
+
+
diff --git a/framework/assets/www/phonegap.js b/framework/assets/www/phonegap.js
index 718612ee..69cf16f7 100644
--- a/framework/assets/www/phonegap.js
+++ b/framework/assets/www/phonegap.js
@@ -1,3 +1,11 @@
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
/**
* The order of events during page load and PhoneGap startup is as follows:
@@ -230,7 +238,14 @@ PhoneGap.onDeviceReady = new PhoneGap.Channel('onDeviceReady');
PhoneGap.Channel.join(function() {
// Start listening for XHR callbacks
- PhoneGap.JSCallback();
+ setTimeout(function() {
+ if (CallbackServer.usePolling()) {
+ PhoneGap.JSCallbackPolling();
+ }
+ else {
+ PhoneGap.JSCallback();
+ }
+ }, 1);
// Run PhoneGap constructors
PhoneGap.onPhoneGapInit.fire();
@@ -245,6 +260,9 @@ PhoneGap.Channel.join(function() {
* received from native side.
*/
PhoneGap.Channel.join(function() {
+ // Turn off app loading dialog
+ navigator.notification.activityStop();
+
PhoneGap.onDeviceReady.fire();
// Fire the onresume event, since first one happens before JavaScript is loaded
@@ -313,7 +331,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 + "]";
@@ -362,35 +382,19 @@ PhoneGap.clone = function(obj) {
PhoneGap.callbackId = 0;
PhoneGap.callbacks = {};
+PhoneGap.callbackStatus = {
+ NO_RESULT: 0,
+ OK: 1,
+ CLASS_NOT_FOUND_EXCEPTION: 2,
+ ILLEGAL_ACCESS_EXCEPTION: 3,
+ INSTANTIATION_EXCEPTION: 4,
+ MALFORMED_URL_EXCEPTION: 5,
+ IO_EXCEPTION: 6,
+ INVALID_ACTION: 7,
+ JSON_EXCEPTION: 8,
+ ERROR: 9
+ };
-/**
- * Execute a PhoneGap command in a queued fashion, to ensure commands do not
- * execute with any race conditions, and only run when PhoneGap is ready to
- * recieve them.
- * @param {String} command Command to be run in PhoneGap, e.g. "ClassName.method"
- * @param {String[]} [args] Zero or more arguments to pass to the method
- */
-// TODO: Not used anymore, should be removed.
-PhoneGap.exec = function(clazz, action, args) {
- try {
- var callbackId = 0;
- var r = PluginManager.exec(clazz, action, callbackId, this.stringify(args), false);
- eval("var v="+r+";");
-
- // If status is OK, then return value back to caller
- if (v.status == 0) {
- return v.message;
- }
-
- // If error, then display error
- else {
- console.log("Error: Status="+r.status+" Message="+v.message);
- return null;
- }
- } catch (e) {
- console.log("Error: "+e);
- }
-};
/**
* Execute a PhoneGap command. It is up to the native side whether this action is synch or async.
@@ -406,7 +410,7 @@ PhoneGap.exec = function(clazz, action, args) {
* @param {String} action Action to be run in PhoneGap
* @param {String[]} [args] Zero or more arguments to pass to the method
*/
-PhoneGap.execAsync = function(success, fail, service, action, args) {
+PhoneGap.exec = function(success, fail, service, action, args) {
try {
var callbackId = service + PhoneGap.callbackId++;
if (success || fail) {
@@ -421,24 +425,51 @@ PhoneGap.execAsync = function(success, fail, service, action, args) {
eval("var v="+r+";");
// If status is OK, then return value back to caller
- if (v.status == 0) {
+ if (v.status == PhoneGap.callbackStatus.OK) {
// If there is a success callback, then call it now with returned value
if (success) {
- success(v.message);
- delete PhoneGap.callbacks[callbackId];
+ try {
+ success(v.message);
+ }
+ catch (e) {
+ console.log("Error in success callback: "+callbackId+" = "+e);
+ }
+
+ // Clear callback if not expecting any more results
+ if (!v.keepCallback) {
+ delete PhoneGap.callbacks[callbackId];
+ }
}
return v.message;
}
+ // If no result
+ else if (v.status == PhoneGap.callbackStatus.NO_RESULT) {
+
+ // Clear callback if not expecting any more results
+ if (!v.keepCallback) {
+ delete PhoneGap.callbacks[callbackId];
+ }
+ }
+
// If error, then display error
else {
console.log("Error: Status="+r.status+" Message="+v.message);
// If there is a fail callback, then call it now with returned value
if (fail) {
- fail(v.message);
- delete PhoneGap.callbacks[callbackId];
+ try {
+ fail(v.message);
+ }
+ catch (e) {
+ console.log("Error in error callback: "+callbackId+" = "+e);
+ }
+
+ // Clear callback if not expecting any more results
+ if (!v.keepCallback) {
+ delete PhoneGap.callbacks[callbackId];
+ }
}
return null;
}
@@ -456,15 +487,23 @@ PhoneGap.execAsync = function(success, fail, service, action, args) {
*/
PhoneGap.callbackSuccess = function(callbackId, args) {
if (PhoneGap.callbacks[callbackId]) {
- try {
- if (PhoneGap.callbacks[callbackId].success) {
- PhoneGap.callbacks[callbackId].success(args.message);
+
+ // If result is to be sent to callback
+ if (args.status == PhoneGap.callbackStatus.OK) {
+ try {
+ if (PhoneGap.callbacks[callbackId].success) {
+ PhoneGap.callbacks[callbackId].success(args.message);
+ }
+ }
+ catch (e) {
+ console.log("Error in success callback: "+callbackId+" = "+e);
}
}
- catch (e) {
- console.log("Error in success callback: "+callbackId+" = "+e);
+
+ // Clear callback if not expecting any more results
+ if (!args.keepCallback) {
+ delete PhoneGap.callbacks[callbackId];
}
- delete PhoneGap.callbacks[callbackId];
}
};
@@ -484,7 +523,11 @@ PhoneGap.callbackError = function(callbackId, args) {
catch (e) {
console.log("Error in error callback: "+callbackId+" = "+e);
}
- delete PhoneGap.callbacks[callbackId];
+
+ // Clear callback if not expecting any more results
+ if (!args.keepCallback) {
+ delete PhoneGap.callbacks[callbackId];
+ }
}
};
@@ -582,6 +625,37 @@ PhoneGap.JSCallback = function() {
xmlhttp.send();
};
+/**
+ * The polling period to use with JSCallbackPolling.
+ * This can be changed by the application. The default is 50ms.
+ */
+PhoneGap.JSCallbackPollingPeriod = 50;
+
+/**
+ * This is only for Android.
+ *
+ * Internal function that uses polling to call into PhoneGap Java code and retrieve
+ * any JavaScript code that needs to be run. This is used for callbacks from
+ * Java to JavaScript.
+ */
+PhoneGap.JSCallbackPolling = function() {
+ var msg = CallbackServer.getJavascript();
+ if (msg) {
+ setTimeout(function() {
+ try {
+ var t = eval(""+msg);
+ }
+ catch (e) {
+ console.log("JSCallbackPolling Error: "+e);
+ }
+ }, 1);
+ setTimeout(PhoneGap.JSCallbackPolling, 1);
+ }
+ else {
+ setTimeout(PhoneGap.JSCallbackPolling, PhoneGap.JSCallbackPollingPeriod);
+ }
+};
+
/**
* Create a UUID
*
@@ -619,6 +693,13 @@ PhoneGap.close = function(context, func, params) {
}
};
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
function Acceleration(x, y, z) {
this.x = x;
@@ -669,7 +750,7 @@ Accelerometer.prototype.getCurrentAcceleration = function(successCallback, error
}
// Get acceleration
- PhoneGap.execAsync(successCallback, errorCallback, "Accelerometer", "getAcceleration", []);
+ PhoneGap.exec(successCallback, errorCallback, "Accelerometer", "getAcceleration", []);
};
/**
@@ -698,10 +779,10 @@ Accelerometer.prototype.watchAcceleration = function(successCallback, errorCallb
}
// Make sure accelerometer timeout > frequency + 10 sec
- PhoneGap.execAsync(
+ PhoneGap.exec(
function(timeout) {
if (timeout < (frequency + 10000)) {
- PhoneGap.execAsync(null, null, "Accelerometer", "setTimeout", [frequency + 10000]);
+ PhoneGap.exec(null, null, "Accelerometer", "setTimeout", [frequency + 10000]);
}
},
function(e) { }, "Accelerometer", "getTimeout", []);
@@ -709,7 +790,7 @@ Accelerometer.prototype.watchAcceleration = function(successCallback, errorCallb
// Start watch timer
var id = PhoneGap.createUUID();
navigator.accelerometer.timers[id] = setInterval(function() {
- PhoneGap.execAsync(successCallback, errorCallback, "Accelerometer", "getAcceleration", []);
+ PhoneGap.exec(successCallback, errorCallback, "Accelerometer", "getAcceleration", []);
}, (frequency ? frequency : 1));
return id;
@@ -732,6 +813,13 @@ Accelerometer.prototype.clearWatch = function(id) {
PhoneGap.addConstructor(function() {
if (typeof navigator.accelerometer == "undefined") navigator.accelerometer = new Accelerometer();
});
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
/**
* This class provides access to the device camera.
@@ -797,8 +885,6 @@ Camera.prototype.getPicture = function(successCallback, errorCallback, options)
return;
}
- this.successCallback = successCallback;
- this.errorCallback = errorCallback;
this.options = options;
var quality = 80;
if (options.quality) {
@@ -812,35 +898,19 @@ Camera.prototype.getPicture = function(successCallback, errorCallback, options)
if (typeof this.options.sourceType == "number") {
sourceType = this.options.sourceType;
}
- PhoneGap.execAsync(null, null, "Camera", "takePicture", [quality, destinationType, sourceType]);
-};
-
-/**
- * Callback function from native code that is called when image has been captured.
- *
- * @param picture The base64 encoded string of the image
- */
-Camera.prototype.success = function(picture) {
- if (this.successCallback) {
- this.successCallback(picture);
- }
-};
-
-/**
- * Callback function from native code that is called when there is an error
- * capturing an image, or the capture is cancelled.
- *
- * @param err The error message
- */
-Camera.prototype.error = function(err) {
- if (this.errorCallback) {
- this.errorCallback(err);
- }
+ PhoneGap.exec(successCallback, errorCallback, "Camera", "takePicture", [quality, destinationType, sourceType]);
};
PhoneGap.addConstructor(function() {
if (typeof navigator.camera == "undefined") navigator.camera = new Camera();
});
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
/**
* This class provides access to device Compass data.
@@ -882,7 +952,7 @@ Compass.prototype.getCurrentHeading = function(successCallback, errorCallback, o
}
// Get heading
- PhoneGap.execAsync(successCallback, errorCallback, "Compass", "getHeading", []);
+ PhoneGap.exec(successCallback, errorCallback, "Compass", "getHeading", []);
};
/**
@@ -911,10 +981,10 @@ Compass.prototype.watchHeading= function(successCallback, errorCallback, options
}
// Make sure compass timeout > frequency + 10 sec
- PhoneGap.execAsync(
+ PhoneGap.exec(
function(timeout) {
if (timeout < (frequency + 10000)) {
- PhoneGap.execAsync(null, null, "Compass", "setTimeout", [frequency + 10000]);
+ PhoneGap.exec(null, null, "Compass", "setTimeout", [frequency + 10000]);
}
},
function(e) { }, "Compass", "getTimeout", []);
@@ -923,7 +993,7 @@ Compass.prototype.watchHeading= function(successCallback, errorCallback, options
var id = PhoneGap.createUUID();
navigator.compass.timers[id] = setInterval(
function() {
- PhoneGap.execAsync(successCallback, errorCallback, "Compass", "getHeading", []);
+ PhoneGap.exec(successCallback, errorCallback, "Compass", "getHeading", []);
}, (frequency ? frequency : 1));
return id;
@@ -947,7 +1017,40 @@ Compass.prototype.clearWatch = function(id) {
PhoneGap.addConstructor(function() {
if (typeof navigator.compass == "undefined") navigator.compass = new Compass();
});
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+/**
+* Contains information about a single contact.
+* @param {DOMString} id unique identifier
+* @param {DOMString} displayName
+* @param {ContactName} name
+* @param {DOMString} nickname
+* @param {ContactField[]} phoneNumbers array of phone numbers
+* @param {ContactField[]} emails array of email addresses
+* @param {ContactAddress[]} addresses array of addresses
+* @param {ContactField[]} ims instant messaging user ids
+* @param {ContactOrganization[]} organizations
+* @param {DOMString} published date contact was first created
+* @param {DOMString} updated date contact was last updated
+* @param {DOMString} birthday contact's birthday
+* @param (DOMString} anniversary contact's anniversary
+* @param {DOMString} gender contact's gender
+* @param {DOMString} note user notes about contact
+* @param {DOMString} preferredUsername
+* @param {ContactField[]} photos
+* @param {ContactField[]} tags
+* @param {ContactField[]} relationships
+* @param {ContactField[]} urls contact's web sites
+* @param {ContactAccounts[]} accounts contact's online accounts
+* @param {DOMString} utcOffset UTC time zone offset
+* @param {DOMString} connected
+*/
var Contact = function(id, displayName, name, nickname, phoneNumbers, emails, addresses,
ims, organizations, published, updated, birthday, anniversary, gender, note,
preferredUsername, photos, tags, relationships, urls, accounts, utcOffset, connected) {
@@ -976,27 +1079,49 @@ var Contact = function(id, displayName, name, nickname, phoneNumbers, emails, ad
this.connected = connected || null;
};
-
+/**
+* Removes contact from device storage.
+* @param successCB success callback
+* @param errorCB error callback
+*/
Contact.prototype.remove = function(successCB, errorCB) {
- if (this.id == null) {
- var errorObj = new ContactError();
- errorObj.code = ContactError.NOT_FOUND_ERROR;
- errorCB(errorObj);
- }
-
- PhoneGap.execAsync(successCB, errorCB, "Contacts", "remove", [this.id]);
+ if (this.id == null) {
+ var errorObj = new ContactError();
+ errorObj.code = ContactError.NOT_FOUND_ERROR;
+ errorCB(errorObj);
+ }
+
+ PhoneGap.exec(successCB, errorCB, "Contacts", "remove", [this.id]);
};
+/**
+* Creates a deep copy of this Contact.
+* With the contact ID set to null.
+* @return copy of this Contact
+*/
Contact.prototype.clone = function() {
- var clonedContact = PhoneGap.clone(this);
- clonedContact.id = null;
+ var clonedContact = PhoneGap.clone(this);
+ clonedContact.id = null;
return clonedContact;
};
-Contact.prototype.save = function(win, fail) {
+/**
+* Persists contact to device storage.
+* @param successCB success callback
+* @param errorCB error callback
+*/
+Contact.prototype.save = function(successCB, errorCB) {
};
-
+/**
+* Contact name.
+* @param formatted
+* @param familyName
+* @param givenName
+* @param middle
+* @param prefix
+* @param suffix
+*/
var ContactName = function(formatted, familyName, givenName, middle, prefix, suffix) {
this.formatted = formatted || null;
this.familyName = familyName || null;
@@ -1006,12 +1131,27 @@ var ContactName = function(formatted, familyName, givenName, middle, prefix, suf
this.honorificSuffix = suffix || null;
};
+/**
+* Generic contact field.
+* @param type
+* @param value
+* @param primary
+*/
var ContactField = function(type, value, primary) {
this.type = type || null;
this.value = value || null;
this.primary = primary || null;
};
+/**
+* Contact address.
+* @param formatted
+* @param streetAddress
+* @param locality
+* @param region
+* @param postalCode
+* @param country
+*/
var ContactAddress = function(formatted, streetAddress, locality, region, postalCode, country) {
this.formatted = formatted || null;
this.streetAddress = streetAddress || null;
@@ -1021,6 +1161,16 @@ var ContactAddress = function(formatted, streetAddress, locality, region, postal
this.country = country || null;
};
+/**
+* Contact organization.
+* @param name
+* @param dept
+* @param title
+* @param startDate
+* @param endDate
+* @param location
+* @param desc
+*/
var ContactOrganization = function(name, dept, title, startDate, endDate, location, desc) {
this.name = name || null;
this.department = dept || null;
@@ -1031,53 +1181,79 @@ var ContactOrganization = function(name, dept, title, startDate, endDate, locati
this.description = desc || null;
};
+/**
+* Contact account.
+* @param domain
+* @param username
+* @param userid
+*/
var ContactAccount = function(domain, username, userid) {
this.domain = domain || null;
this.username = username || null;
this.userid = userid || null;
}
+/**
+* Represents a group of Contacts.
+*/
var Contacts = function() {
this.inProgress = false;
this.records = new Array();
}
-
-Contacts.prototype.find = function(fields, win, fail, options) {
- PhoneGap.execAsync(win, fail, "Contacts", "search", [fields, options]);
+/**
+* Returns an array of Contacts matching the search criteria.
+* @param fields that should be searched
+* @param successCB success callback
+* @param errorCB error callback
+* @param {ContactFindOptions} options that can be applied to contact searching
+* @return array of Contacts matching search criteria
+*/
+Contacts.prototype.find = function(fields, successCB, errorCB, options) {
+ PhoneGap.exec(successCB, errorCB, "Contacts", "search", [fields, options]);
};
-//This function does not create a new contact in the db.
-//Must call contact.save() for it to be persisted in the db.
+/**
+* This function creates a new contact, but it does not persist the contact
+* to device storage. To persist the contact to device storage, invoke
+* contact.save().
+* @param properties an object who's properties will be examined to create a new Contact
+* @returns new Contact object
+*/
Contacts.prototype.create = function(properties) {
- var contact = new Contact();
- for (i in properties) {
- if (contact[i]!='undefined') {
- contact[i]=properties[i];
- }
- }
- return contact;
-};
-
-Contacts.prototype.droidDone = function(contacts) {
- this.win(eval('(' + contacts + ')'));
-};
-
-Contacts.prototype.m_foundContacts = function(win, contacts) {
- this.inProgress = false;
- win(contacts);
+ var contact = new Contact();
+ for (i in properties) {
+ if (contact[i]!='undefined') {
+ contact[i]=properties[i];
+ }
+ }
+ return contact;
};
+/**
+ * ContactFindOptions.
+ * @param filter used to match contacts against
+ * @param multiple boolean used to determine if more than one contact should be returned
+ * @param limit maximum number of results to return from the contacts search
+ * @param updatedSince return only contact records that have been updated on or after the given time
+ */
var ContactFindOptions = function(filter, multiple, limit, updatedSince) {
this.filter = filter || '';
- this.multiple = multiple || true;
- this.limit = limit || Number.MAX_VALUE;
+ this.multiple = multiple || false;
+ this.limit = limit || 1;
this.updatedSince = updatedSince || '';
};
+/**
+ * ContactError.
+ * An error code assigned by an implementation when an error has occurred
+ */
var ContactError = function() {
this.code=null;
};
+/**
+ * Error codes
+ */
ContactError.UNKNOWN_ERROR = 0;
ContactError.INVALID_ARGUMENT_ERROR = 1;
ContactError.NOT_FOUND_ERROR = 2;
@@ -1087,22 +1263,34 @@ ContactError.IO_ERROR = 5;
ContactError.NOT_SUPPORTED_ERROR = 6;
ContactError.PERMISSION_DENIED_ERROR = 20;
+/**
+ * Add the contact interface into the browser.
+ */
PhoneGap.addConstructor(function() {
if(typeof navigator.service == "undefined") navigator.service = new Object();
if(typeof navigator.service.contacts == "undefined") navigator.service.contacts = new Contacts();
});
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
+// TODO: Needs to be commented
var Crypto = function() {
};
Crypto.prototype.encrypt = function(seed, string, callback) {
this.encryptWin = callback;
- PhoneGap.execAsync(null, null, "Crypto", "encrypt", [seed, string]);
+ PhoneGap.exec(null, null, "Crypto", "encrypt", [seed, string]);
};
Crypto.prototype.decrypt = function(seed, string, callback) {
this.decryptWin = callback;
- PhoneGap.execAsync(null, null, "Crypto", "decrypt", [seed, string]);
+ PhoneGap.exec(null, null, "Crypto", "decrypt", [seed, string]);
};
Crypto.prototype.gotCryptedString = function(string) {
@@ -1117,6 +1305,14 @@ PhoneGap.addConstructor(function() {
if (typeof navigator.Crypto == "undefined") navigator.Crypto = new Crypto();
});
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
/**
* This represents the mobile device, and provides properties for inspecting the model, version, UUID of the
* phone, etc.
@@ -1168,7 +1364,7 @@ Device.prototype.getInfo = function(successCallback, errorCallback) {
}
// Get info
- PhoneGap.execAsync(successCallback, errorCallback, "Device", "getDeviceInfo", []);
+ PhoneGap.exec(successCallback, errorCallback, "Device", "getDeviceInfo", []);
};
/*
@@ -1201,6 +1397,14 @@ Device.prototype.exitApp = function() {
PhoneGap.addConstructor(function() {
navigator.device = window.device = new Device();
});
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
/**
* This class provides generic read and write access to the mobile device file system.
* They are not used to read files from a server.
@@ -1263,43 +1467,43 @@ FileMgr.prototype.getFileBasePaths = function() {
};
FileMgr.prototype.testSaveLocationExists = function(successCallback, errorCallback) {
- PhoneGap.execAsync(successCallback, errorCallback, "File", "testSaveLocationExists", []);
+ PhoneGap.exec(successCallback, errorCallback, "File", "testSaveLocationExists", []);
};
FileMgr.prototype.testFileExists = function(fileName, successCallback, errorCallback) {
- PhoneGap.execAsync(successCallback, errorCallback, "File", "testFileExists", [fileName]);
+ PhoneGap.exec(successCallback, errorCallback, "File", "testFileExists", [fileName]);
};
FileMgr.prototype.testDirectoryExists = function(dirName, successCallback, errorCallback) {
- PhoneGap.execAsync(successCallback, errorCallback, "File", "testDirectoryExists", [dirName]);
+ PhoneGap.exec(successCallback, errorCallback, "File", "testDirectoryExists", [dirName]);
};
FileMgr.prototype.createDirectory = function(dirName, successCallback, errorCallback) {
- PhoneGap.execAsync(successCallback, errorCallback, "File", "createDirectory", [dirName]);
+ PhoneGap.exec(successCallback, errorCallback, "File", "createDirectory", [dirName]);
};
FileMgr.prototype.deleteDirectory = function(dirName, successCallback, errorCallback) {
- PhoneGap.execAsync(successCallback, errorCallback, "File", "deleteDirectory", [dirName]);
+ PhoneGap.exec(successCallback, errorCallback, "File", "deleteDirectory", [dirName]);
};
FileMgr.prototype.deleteFile = function(fileName, successCallback, errorCallback) {
- PhoneGap.execAsync(successCallback, errorCallback, "File", "deleteFile", [fileName]);
+ PhoneGap.exec(successCallback, errorCallback, "File", "deleteFile", [fileName]);
};
FileMgr.prototype.getFreeDiskSpace = function(successCallback, errorCallback) {
- PhoneGap.execAsync(successCallback, errorCallback, "File", "getFreeDiskSpace", []);
+ PhoneGap.exec(successCallback, errorCallback, "File", "getFreeDiskSpace", []);
};
FileMgr.prototype.writeAsText = function(fileName, data, append, successCallback, errorCallback) {
- PhoneGap.execAsync(successCallback, errorCallback, "File", "writeAsText", [fileName, data, append]);
+ PhoneGap.exec(successCallback, errorCallback, "File", "writeAsText", [fileName, data, append]);
};
FileMgr.prototype.readAsText = function(fileName, encoding, successCallback, errorCallback) {
- PhoneGap.execAsync(successCallback, errorCallback, "File", "readAsText", [fileName, encoding]);
+ PhoneGap.exec(successCallback, errorCallback, "File", "readAsText", [fileName, encoding]);
};
FileMgr.prototype.readAsDataURL = function(fileName, successCallback, errorCallback) {
- PhoneGap.execAsync(successCallback, errorCallback, "File", "readAsDataURL", [fileName]);
+ PhoneGap.exec(successCallback, errorCallback, "File", "readAsDataURL", [fileName]);
};
PhoneGap.addConstructor(function() {
@@ -1545,18 +1749,43 @@ FileReader.prototype.readAsBinaryString = function(file) {
*/
function FileWriter() {
this.fileName = "";
- this.result = null;
+
this.readyState = 0; // EMPTY
+
this.result = null;
- this.onerror = null;
- this.oncomplete = null;
+
+ // Error
+ this.error = null;
+
+ // Event handlers
+ this.onwritestart = null; // When writing starts
+ this.onprogress = null; // While writing the file, and reporting partial file data
+ this.onwrite = null; // When the write has successfully completed.
+ this.onwriteend = null; // When the request has completed (either in success or failure).
+ this.onabort = null; // When the write has been aborted. For instance, by invoking the abort() method.
+ this.onerror = null; // When the write has failed (see errors).
};
// States
-FileWriter.EMPTY = 0;
-FileWriter.LOADING = 1;
+FileWriter.INIT = 0;
+FileWriter.WRITING = 1;
FileWriter.DONE = 2;
+/**
+ * Abort writing file.
+ */
+FileWriter.prototype.abort = function() {
+ this.readyState = FileWriter.DONE;
+
+ // If abort callback
+ if (typeof this.onabort == "function") {
+ var evt = File._createEvent("abort", this);
+ this.onabort(evt);
+ }
+
+ // TODO: Anything else to do? Maybe sent to native?
+};
+
FileWriter.prototype.writeAsText = function(file, text, bAppend) {
if (bAppend != true) {
bAppend = false; // for null values
@@ -1564,12 +1793,12 @@ FileWriter.prototype.writeAsText = function(file, text, bAppend) {
this.fileName = file;
- // LOADING state
- this.readyState = FileWriter.LOADING;
+ // WRITING state
+ this.readyState = FileWriter.WRITING;
var me = this;
- // Read file
+ // Write file
navigator.fileMgr.writeAsText(file, text, bAppend,
// Success callback
@@ -1586,10 +1815,16 @@ FileWriter.prototype.writeAsText = function(file, text, bAppend) {
// DONE state
me.readyState = FileWriter.DONE;
- // If oncomplete callback
- if (typeof me.oncomplete == "function") {
- var evt = File._createEvent("complete", me);
- me.oncomplete(evt);
+ // If onwrite callback
+ if (typeof me.onwrite == "function") {
+ var evt = File._createEvent("write", me);
+ me.onwrite(evt);
+ }
+
+ // If onwriteend callback
+ if (typeof me.onwriteend == "function") {
+ var evt = File._createEvent("writeend", me);
+ me.onwriteend(evt);
}
},
@@ -1612,11 +1847,25 @@ FileWriter.prototype.writeAsText = function(file, text, bAppend) {
var evt = File._createEvent("error", me);
me.onerror(evt);
}
+
+ // If onwriteend callback
+ if (typeof me.onwriteend == "function") {
+ var evt = File._createEvent("writeend", me);
+ me.onwriteend(evt);
+ }
}
);
};
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
/**
* This class provides access to device GPS data.
* @constructor
@@ -1676,7 +1925,7 @@ Geolocation.prototype.getCurrentPosition = function(successCallback, errorCallba
}
}
navigator._geo.listeners["global"] = {"success" : successCallback, "fail" : errorCallback };
- PhoneGap.execAsync(null, null, "Geolocation", "getCurrentLocation", [enableHighAccuracy, timeout, maximumAge]);
+ PhoneGap.exec(null, null, "Geolocation", "getCurrentLocation", [enableHighAccuracy, timeout, maximumAge]);
}
/**
@@ -1708,7 +1957,7 @@ Geolocation.prototype.watchPosition = function(successCallback, errorCallback, o
}
var id = PhoneGap.createUUID();
navigator._geo.listeners[id] = {"success" : successCallback, "fail" : errorCallback };
- PhoneGap.execAsync(null, null, "Geolocation", "start", [id, enableHighAccuracy, timeout, maximumAge]);
+ PhoneGap.exec(null, null, "Geolocation", "start", [id, enableHighAccuracy, timeout, maximumAge]);
return id;
};
@@ -1769,7 +2018,7 @@ Geolocation.prototype.fail = function(id, code, msg) {
* @param {String} id The ID of the watch returned from #watchPosition
*/
Geolocation.prototype.clearWatch = function(id) {
- PhoneGap.execAsync(null, null, "Geolocation", "stop", [id]);
+ PhoneGap.exec(null, null, "Geolocation", "stop", [id]);
delete navigator._geo.listeners[id];
};
@@ -1803,6 +2052,14 @@ PhoneGap.addConstructor(function() {
}
});
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
function KeyEvent()
{
}
@@ -1818,6 +2075,13 @@ if (document.keyEvent == null || typeof document.keyEvent == 'undefined')
{
window.keyEvent = document.keyEvent = new KeyEvent();
}
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
/**
* List of media objects.
@@ -1955,21 +2219,21 @@ MediaError.MEDIA_ERR_NONE_SUPPORTED = 4;
* Start or resume playing audio file.
*/
Media.prototype.play = function() {
- PhoneGap.execAsync(null, null, "Media", "startPlayingAudio", [this.id, this.src]);
+ PhoneGap.exec(null, null, "Media", "startPlayingAudio", [this.id, this.src]);
};
/**
* Stop playing audio file.
*/
Media.prototype.stop = function() {
- return PhoneGap.execAsync(null, null, "Media", "stopPlayingAudio", [this.id]);
+ return PhoneGap.exec(null, null, "Media", "stopPlayingAudio", [this.id]);
};
/**
* Pause playing audio file.
*/
Media.prototype.pause = function() {
- PhoneGap.execAsync(null, null, "Media", "pausePlayingAudio", [this.id]);
+ PhoneGap.exec(null, null, "Media", "pausePlayingAudio", [this.id]);
};
/**
@@ -1988,23 +2252,30 @@ Media.prototype.getDuration = function() {
* @return
*/
Media.prototype.getCurrentPosition = function(success, fail) {
- PhoneGap.execAsync(success, fail, "Media", "getCurrentPositionAudio", [this.id]);
+ PhoneGap.exec(success, fail, "Media", "getCurrentPositionAudio", [this.id]);
};
/**
* Start recording audio file.
*/
Media.prototype.startRecord = function() {
- PhoneGap.execAsync(null, null, "Media", "startRecordingAudio", [this.id, this.src]);
+ PhoneGap.exec(null, null, "Media", "startRecordingAudio", [this.id, this.src]);
};
/**
* Stop recording audio file.
*/
Media.prototype.stopRecord = function() {
- PhoneGap.execAsync(null, null, "Media", "stopRecordingAudio", [this.id]);
+ PhoneGap.exec(null, null, "Media", "stopRecordingAudio", [this.id]);
};
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
/**
* This class contains information about any NetworkStatus.
@@ -2026,10 +2297,10 @@ NetworkStatus.REACHABLE_VIA_WIFI_NETWORK = 2;
function Network() {
/**
* The last known Network status.
- * { hostName: string, ipAddress: string,
- remoteHostStatus: int(0/1/2), internetConnectionStatus: int(0/1/2), localWiFiConnectionStatus: int (0/2) }
+ * { hostName: string, ipAddress: string,
+ remoteHostStatus: int(0/1/2), internetConnectionStatus: int(0/1/2), localWiFiConnectionStatus: int (0/2) }
*/
- this.lastReachability = null;
+ this.lastReachability = null;
};
/**
@@ -2053,13 +2324,21 @@ Network.prototype.isReachable = function(uri, callback, options) {
if (options && options.isIpAddress) {
isIpAddress = options.isIpAddress;
}
- PhoneGap.execAsync(callback, null, "Network Status", "isReachable", [uri, isIpAddress]);
+ PhoneGap.exec(callback, null, "Network Status", "isReachable", [uri, isIpAddress]);
};
PhoneGap.addConstructor(function() {
if (typeof navigator.network == "undefined") navigator.network = new Network();
});
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
/**
* This class provides access to notifications on the device.
*/
@@ -2068,61 +2347,114 @@ function Notification() {
/**
* Open a native alert dialog, with a customizable title and button text.
- * @param {String} message Message to print in the body of the alert
- * @param {String} [title="Alert"] Title of the alert dialog (default: Alert)
- * @param {String} [buttonLabel="OK"] Label of the close button (default: OK)
+ *
+ * @param {String} message Message to print in the body of the alert
+ * @param {Function} completeCallback The callback that is called when user clicks on a button.
+ * @param {String} title Title of the alert dialog (default: Alert)
+ * @param {String} buttonLabel Label of the close button (default: OK)
*/
-Notification.prototype.alert = function(message, title, buttonLabel) {
- var _title = (title || "Alert");
- var _buttonLabel = (buttonLabel || "OK");
- PhoneGap.execAsync(null, null, "Notification", "alert", [message,_title,_buttonLabel]);
+Notification.prototype.alert = function(message, completeCallback, title, buttonLabel) {
+ var _title = (title || "Alert");
+ var _buttonLabel = (buttonLabel || "OK");
+ PhoneGap.exec(completeCallback, null, "Notification", "alert", [message,_title,_buttonLabel]);
+};
+
+/**
+ * Open a native confirm dialog, with a customizable title and button text.
+ * The result that the user selects is returned to the result callback.
+ *
+ * @param {String} message Message to print in the body of the alert
+ * @param {Function} resultCallback The callback that is called when user clicks on a button.
+ * @param {String} title Title of the alert dialog (default: Confirm)
+ * @param {String} buttonLabels Comma separated list of the labels of the buttons (default: 'OK,Cancel')
+ */
+Notification.prototype.confirm = function(message, resultCallback, title, buttonLabels) {
+ var _title = (title || "Confirm");
+ var _buttonLabels = (buttonLabels || "OK,Cancel");
+ PhoneGap.exec(resultCallback, null, "Notification", "confirm", [message,_title,_buttonLabels]);
};
/**
* Start spinning the activity indicator on the statusbar
*/
Notification.prototype.activityStart = function() {
+ PhoneGap.exec(null, null, "Notification", "activityStart", ["Busy","Please wait..."]);
};
/**
* Stop spinning the activity indicator on the statusbar, if it's currently spinning
*/
Notification.prototype.activityStop = function() {
+ PhoneGap.exec(null, null, "Notification", "activityStop", []);
+};
+
+/**
+ * Display a progress dialog with progress bar that goes from 0 to 100.
+ *
+ * @param {String} title Title of the progress dialog.
+ * @param {String} message Message to display in the dialog.
+ */
+Notification.prototype.progressStart = function(title, message) {
+ PhoneGap.exec(null, null, "Notification", "progressStart", [title, message]);
+};
+
+/**
+ * Set the progress dialog value.
+ *
+ * @param {Number} value 0-100
+ */
+Notification.prototype.progressValue = function(value) {
+ PhoneGap.exec(null, null, "Notification", "progressValue", [value]);
+};
+
+/**
+ * Close the progress dialog.
+ */
+Notification.prototype.progressStop = function() {
+ PhoneGap.exec(null, null, "Notification", "progressStop", []);
};
/**
* Causes the device to blink a status LED.
- * @param {Integer} count The number of blinks.
- * @param {String} colour The colour of the light.
+ *
+ * @param {Integer} count The number of blinks.
+ * @param {String} colour The colour of the light.
*/
Notification.prototype.blink = function(count, colour) {
-
+ // NOT IMPLEMENTED
};
/**
* Causes the device to vibrate.
- * @param {Integer} mills The number of milliseconds to vibrate for.
+ *
+ * @param {Integer} mills The number of milliseconds to vibrate for.
*/
Notification.prototype.vibrate = function(mills) {
- PhoneGap.execAsync(null, null, "Notification", "vibrate", [mills]);
+ PhoneGap.exec(null, null, "Notification", "vibrate", [mills]);
};
/**
* Causes the device to beep.
- * On Android, the default notification ringtone is played.
+ * On Android, the default notification ringtone is played "count" times.
*
- * @param {Integer} count The number of beeps.
+ * @param {Integer} count The number of beeps.
*/
Notification.prototype.beep = function(count) {
- PhoneGap.execAsync(null, null, "Notification", "beep", [count]);
+ PhoneGap.exec(null, null, "Notification", "beep", [count]);
};
-// TODO: of course on Blackberry and Android there notifications in the UI as well
-
PhoneGap.addConstructor(function() {
if (typeof navigator.notification == "undefined") navigator.notification = new Notification();
});
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
/**
* This class contains position information.
* @param {Object} lat
@@ -2199,77 +2531,331 @@ PositionError.UNKNOWN_ERROR = 0;
PositionError.PERMISSION_DENIED = 1;
PositionError.POSITION_UNAVAILABLE = 2;
PositionError.TIMEOUT = 3;
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
+PhoneGap.addConstructor(function() {
+ if (typeof navigator.splashScreen == "undefined") {
+ navigator.splashScreen = SplashScreen; // SplashScreen object come from native side through addJavaScriptInterface
+ }
+});/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+
/*
* This is purely for the Android 1.5/1.6 HTML 5 Storage
* I was hoping that Android 2.0 would deprecate this, but given the fact that
* 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.exec(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) {
- PhoneGap.execAsync(null, null, "Storage", "openDatabase", [name, version, display_name, size]);
- db_object = new DatabaseShell();
- return db_object;
+/**
+ * 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.exec(null, null, "Storage", "openDatabase", [name, version, display_name, size]);
+ 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/build.xml b/framework/build.xml
old mode 100644
new mode 100755
index 285c1908..1ad9bb75
--- a/framework/build.xml
+++ b/framework/build.xml
@@ -1,6 +1,13 @@
+
+
+
+
+
+
+
@@ -73,7 +80,7 @@
-
+
@@ -95,16 +102,72 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/framework/src/com/phonegap/AccelListener.java b/framework/src/com/phonegap/AccelListener.java
index f33dde84..bcd100e4 100755
--- a/framework/src/com/phonegap/AccelListener.java
+++ b/framework/src/com/phonegap/AccelListener.java
@@ -13,6 +13,7 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import com.phonegap.api.PhonegapActivity;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
@@ -60,7 +61,7 @@ public class AccelListener extends Plugin implements SensorEventListener {
*
* @param ctx The context of the main Activity.
*/
- public void setContext(DroidGap ctx) {
+ public void setContext(PhonegapActivity ctx) {
super.setContext(ctx);
this.sensorManager = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE);
}
diff --git a/framework/src/com/phonegap/App.java b/framework/src/com/phonegap/App.java
new file mode 100755
index 00000000..cb3342bc
--- /dev/null
+++ b/framework/src/com/phonegap/App.java
@@ -0,0 +1,173 @@
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010-2011, IBM Corporation
+ */
+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;
+
+/**
+ * This class exposes methods in DroidGap that can be called from JavaScript.
+ */
+public class App extends Plugin {
+
+ /**
+ * Executes the request and returns PluginResult.
+ *
+ * @param action The action to execute.
+ * @param args JSONArry of arguments for the plugin.
+ * @param callbackId The callback id used when calling back into JavaScript.
+ * @return A PluginResult object with a status and message.
+ */
+ public PluginResult execute(String action, JSONArray args, String callbackId) {
+ PluginResult.Status status = PluginResult.Status.OK;
+ String result = "";
+
+ try {
+ if (action.equals("clearCache")) {
+ this.clearCache();
+ }
+ else if (action.equals("loadUrl")) {
+ this.loadUrl(args.getString(0), args.optJSONObject(1));
+ }
+ else if (action.equals("cancelLoadUrl")) {
+ this.cancelLoadUrl();
+ }
+ else if (action.equals("clearHistory")) {
+ this.clearHistory();
+ }
+ else if (action.equals("addService")) {
+ this.addService(args.getString(0), args.getString(1));
+ }
+ else if (action.equals("overrideBackbutton")) {
+ this.overrideBackbutton(args.getBoolean(0));
+ }
+ else if (action.equals("isBackbuttonOverridden")) {
+ boolean b = this.isBackbuttonOverridden();
+ return new PluginResult(status, b);
+ }
+ else if (action.equals("exitApp")) {
+ this.exitApp();
+ }
+ return new PluginResult(status, result);
+ } catch (JSONException e) {
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
+ }
+ }
+
+ //--------------------------------------------------------------------------
+ // LOCAL METHODS
+ //--------------------------------------------------------------------------
+
+ /**
+ * Clear the resource cache.
+ */
+ public void clearCache() {
+ ((DroidGap)this.ctx).clearCache();
+ }
+
+ /**
+ * Load the url into the webview.
+ *
+ * @param url
+ * @param props Properties that can be passed in to the DroidGap activity (i.e. loadingDialog, wait, ...)
+ * @throws JSONException
+ */
+ public void loadUrl(String url, JSONObject props) throws JSONException {
+ System.out.println("App.loadUrl("+url+","+props+")");
+ int wait = 0;
+
+ // If there are properties, then set them on the Activity
+ if (props != null) {
+ JSONArray keys = props.names();
+ for (int i=0; i 0) {
+ ((DroidGap)this.ctx).loadUrl(url, wait);
+ }
+ else {
+ ((DroidGap)this.ctx).loadUrl(url);
+ }
+ }
+
+ /**
+ * Cancel loadUrl before it has been loaded.
+ */
+ public void cancelLoadUrl() {
+ ((DroidGap)this.ctx).cancelLoadUrl();
+ }
+
+ /**
+ * Clear web history in this web view.
+ */
+ public void clearHistory() {
+ ((DroidGap)this.ctx).clearHistory();
+ }
+
+ /**
+ * Add a class that implements a service.
+ *
+ * @param serviceType
+ * @param className
+ */
+ public void addService(String serviceType, String className) {
+ this.ctx.addService(serviceType, className);
+ }
+
+ /**
+ * Override the default behavior of the Android back button.
+ * If overridden, when the back button is pressed, the "backKeyDown" JavaScript event will be fired.
+ *
+ * @param override T=override, F=cancel override
+ */
+ public void overrideBackbutton(boolean override) {
+ System.out.println("WARNING: Back Button Default Behaviour will be overridden. The backbutton event will be fired!");
+ ((DroidGap)this.ctx).bound = override;
+ }
+
+ /**
+ * Return whether the Android back button is overridden by the user.
+ *
+ * @return boolean
+ */
+ public boolean isBackbuttonOverridden() {
+ return ((DroidGap)this.ctx).bound;
+ }
+
+ /**
+ * Exit the Android application.
+ */
+ public void exitApp() {
+ ((DroidGap)this.ctx).finish();
+ }
+
+}
diff --git a/framework/src/com/phonegap/AudioHandler.java b/framework/src/com/phonegap/AudioHandler.java
index e8813bf7..33d05805 100755
--- a/framework/src/com/phonegap/AudioHandler.java
+++ b/framework/src/com/phonegap/AudioHandler.java
@@ -20,7 +20,7 @@ import android.content.Context;
import android.media.AudioManager;
/**
- * This class called by DroidGap to play and record audio.
+ * This class called by PhonegapActivity to play and record audio.
* The file can be local or over a network using http.
*
* Audio formats supported (tested):
@@ -77,13 +77,17 @@ public class AudioHandler extends Plugin {
long l = this.getDurationAudio(args.getString(0), args.getString(1));
return new PluginResult(status, l);
}
+ else if (action.equals("release")) {
+ boolean b = this.release(args.getString(0));
+ return new PluginResult(status, b);
+ }
return new PluginResult(status, result);
} catch (JSONException e) {
e.printStackTrace();
return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
}
}
-
+
/**
* Identifies if action to be executed returns a value and should be run synchronously.
*
@@ -117,6 +121,21 @@ public class AudioHandler extends Plugin {
//--------------------------------------------------------------------------
// LOCAL METHODS
//--------------------------------------------------------------------------
+
+ /**
+ * Release the audio player instance to save memory.
+ *
+ * @param id The id of the audio player
+ */
+ private boolean release(String id) {
+ if (!this.players.containsKey(id)) {
+ return false;
+ }
+ AudioPlayer audio = this.players.get(id);
+ this.players.remove(id);
+ audio.destroy();
+ return true;
+ }
/**
* Start recording and save the specified file.
diff --git a/framework/src/com/phonegap/BrowserKey.java b/framework/src/com/phonegap/BrowserKey.java
deleted file mode 100755
index 940f641c..00000000
--- a/framework/src/com/phonegap/BrowserKey.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * PhoneGap is available under *either* the terms of the modified BSD license *or* the
- * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
- *
- * Copyright (c) 2005-2010, Nitobi Software Inc.
- */
-package com.phonegap;
-
-import android.app.Activity;
-import android.util.Log;
-import android.webkit.WebView;
-
-/*
- * This class literally exists to protect DroidGap from Javascript directly.
- *
- *
- */
-
-public class BrowserKey {
-
- DroidGap mAction;
- boolean bound;
- WebView mView;
-
- BrowserKey(WebView view, DroidGap action)
- {
- bound = false;
- mAction = action;
- }
-
- public void override()
- {
- Log.d("PhoneGap", "WARNING: Back Button Default Behaviour will be overridden. The backKeyDown event will be fired!");
- bound = true;
- }
-
- public boolean isBound()
- {
- return bound;
- }
-
- public void reset()
- {
- bound = false;
- }
-
- public void exitApp()
- {
- mAction.finish();
- }
-}
diff --git a/framework/src/com/phonegap/CallbackServer.java b/framework/src/com/phonegap/CallbackServer.java
index 154c7b2f..2fce8a1e 100755
--- a/framework/src/com/phonegap/CallbackServer.java
+++ b/framework/src/com/phonegap/CallbackServer.java
@@ -17,10 +17,10 @@ import java.util.LinkedList;
/**
* This class provides a way for Java to run JavaScript in the web page that has loaded PhoneGap.
- * The CallbackServer class implements an XHR server and a list of JavaScript statements
- * that are to be executed on the web page.
+ * The CallbackServer class implements an XHR server and a polling server with a list of JavaScript
+ * statements that are to be executed on the web page.
*
- * The process flow is:
+ * The process flow for XHR is:
* 1. JavaScript makes an async XHR call.
* 2. The server holds the connection open until data is available.
* 3. The server writes the data to the client and closes the connection.
@@ -30,6 +30,14 @@ import java.util.LinkedList;
*
* The CallbackServer class requires the following permission in Android manifest file
*
+ *
+ * If the device has a proxy set, then XHR cannot be used, so polling must be used instead.
+ * This can be determined by the client by calling CallbackServer.usePolling().
+ *
+ * The process flow for polling is:
+ * 1. The client calls CallbackServer.getJavascript() to retrieve next statement.
+ * 2. If statement available, then client processes it.
+ * 3. The client repeats #1 in loop.
*/
public class CallbackServer implements Runnable {
@@ -58,6 +66,16 @@ public class CallbackServer implements Runnable {
*/
private boolean empty;
+ /**
+ * Indicates that polling should be used instead of XHR.
+ */
+ private boolean usePolling = true;
+
+ /**
+ * Security token to prevent other apps from accessing this callback server via XHR
+ */
+ private String token;
+
/**
* Constructor.
*/
@@ -66,8 +84,42 @@ public class CallbackServer implements Runnable {
this.active = false;
this.empty = true;
this.port = 0;
- this.javascript = new LinkedList();
- this.startServer();
+ this.javascript = new LinkedList();
+ }
+
+ /**
+ * Init callback server and start XHR if running local app.
+ *
+ * If PhoneGap app is loaded from file://, then we can use XHR
+ * otherwise we have to use polling due to cross-domain security restrictions.
+ *
+ * @param url The URL of the PhoneGap app being loaded
+ */
+ public void init(String url) {
+ //System.out.println("CallbackServer.start("+url+")");
+
+ // Determine if XHR or polling is to be used
+ if ((url != null) && !url.startsWith("file://")) {
+ this.usePolling = true;
+ this.stopServer();
+ }
+ else if (android.net.Proxy.getDefaultHost() != null) {
+ this.usePolling = true;
+ this.stopServer();
+ }
+ else {
+ this.usePolling = false;
+ this.startServer();
+ }
+ }
+
+ /**
+ * Return if polling is being used instead of XHR.
+ *
+ * @return
+ */
+ public boolean usePolling() {
+ return this.usePolling;
}
/**
@@ -79,6 +131,15 @@ public class CallbackServer implements Runnable {
return this.port;
}
+ /**
+ * Get the security token that this server requires when calling getJavascript().
+ *
+ * @return
+ */
+ public String getToken() {
+ return this.token;
+ }
+
/**
* Start the server on a new thread.
*/
@@ -115,7 +176,9 @@ public class CallbackServer implements Runnable {
String request;
ServerSocket waitSocket = new ServerSocket(0);
this.port = waitSocket.getLocalPort();
- //System.out.println(" -- using port " +this.port);
+ //System.out.println("CallbackServer -- using port " +this.port);
+ this.token = java.util.UUID.randomUUID().toString();
+ //System.out.println("CallbackServer -- using token "+this.token);
while (this.active) {
//System.out.println("CallbackServer: Waiting for data on socket");
@@ -123,40 +186,62 @@ public class CallbackServer implements Runnable {
BufferedReader xhrReader = new BufferedReader(new InputStreamReader(connection.getInputStream()),40);
DataOutputStream output = new DataOutputStream(connection.getOutputStream());
request = xhrReader.readLine();
- //System.out.println("Request="+request);
- if(request.contains("GET"))
- {
- //System.out.println(" -- Processing GET request");
-
- // Wait until there is some data to send, or send empty data every 30 sec
- // to prevent XHR timeout on the client
- synchronized (this) {
- while (this.empty) {
- try {
- this.wait(30000); // prevent timeout from happening
- //System.out.println(">>> break <<<");
- break;
- }
- catch (Exception e) { }
- }
- }
-
- // If server is still running
- if (this.active) {
-
- // If no data, then send 404 back to client before it times out
- if (this.empty) {
- //System.out.println(" -- sending data 0");
- output.writeBytes("HTTP/1.1 404 NO DATA\r\n\r\n");
+ String response = "";
+ //System.out.println("CallbackServerRequest="+request);
+ if (this.active && (request != null)) {
+ if (request.contains("GET")) {
+
+ // Get requested file
+ String[] requestParts = request.split(" ");
+
+ // Must have security token
+ if ((requestParts.length == 3) && (requestParts[1].substring(1).equals(this.token))) {
+ //System.out.println("CallbackServer -- Processing GET request");
+
+ // Wait until there is some data to send, or send empty data every 10 sec
+ // to prevent XHR timeout on the client
+ synchronized (this) {
+ while (this.empty) {
+ try {
+ this.wait(10000); // prevent timeout from happening
+ //System.out.println("CallbackServer>>> break <<<");
+ break;
+ }
+ catch (Exception e) { }
+ }
+ }
+
+ // If server is still running
+ if (this.active) {
+
+ // If no data, then send 404 back to client before it times out
+ if (this.empty) {
+ //System.out.println("CallbackServer -- sending data 0");
+ response = "HTTP/1.1 404 NO DATA\r\n\r\n "; // need to send content otherwise some Android devices fail, so send space
+ }
+ else {
+ //System.out.println("CallbackServer -- sending item");
+ response = "HTTP/1.1 200 OK\r\n\r\n"+this.getJavascript();
+ }
+ }
+ else {
+ response = "HTTP/1.1 503 Service Unavailable\r\n\r\n ";
+ }
}
else {
- //System.out.println(" -- sending item");
- output.writeBytes("HTTP/1.1 200 OK\r\n\r\n"+this.getJavascript());
+ response = "HTTP/1.1 403 Forbidden\r\n\r\n ";
}
- }
+ }
+ else {
+ response = "HTTP/1.1 400 Bad Request\r\n\r\n ";
+ }
+ //System.out.println("CallbackServer: response="+response);
+ //System.out.println("CallbackServer: closing output");
+ output.writeBytes(response);
+ output.flush();
}
- //System.out.println("CallbackServer: closing output");
- output.close();
+ output.close();
+ xhrReader.close();
}
} catch (IOException e) {
e.printStackTrace();
@@ -171,11 +256,13 @@ public class CallbackServer implements Runnable {
*/
public void stopServer() {
//System.out.println("CallbackServer.stopServer()");
- this.active = false;
+ if (this.active) {
+ this.active = false;
- // Break out of server wait
- synchronized (this) {
- this.notify();
+ // Break out of server wait
+ synchronized (this) {
+ this.notify();
+ }
}
}
diff --git a/framework/src/com/phonegap/CameraLauncher.java b/framework/src/com/phonegap/CameraLauncher.java
index 9c8638a7..24e72d7c 100755
--- a/framework/src/com/phonegap/CameraLauncher.java
+++ b/framework/src/com/phonegap/CameraLauncher.java
@@ -45,6 +45,7 @@ public class CameraLauncher extends Plugin {
private int mQuality; // Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
private Uri imageUri; // Uri of captured image
+ public String callbackId;
/**
* Constructor.
@@ -63,6 +64,7 @@ public class CameraLauncher extends Plugin {
public PluginResult execute(String action, JSONArray args, String callbackId) {
PluginResult.Status status = PluginResult.Status.OK;
String result = "";
+ this.callbackId = callbackId;
try {
if (action.equals("takePicture")) {
@@ -78,8 +80,11 @@ public class CameraLauncher extends Plugin {
this.takePicture(args.getInt(0), destType);
}
else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) {
- this.getImage(srcType, destType);
+ this.getImage(args.getInt(0), srcType, destType);
}
+ PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
+ r.setKeepCallback(true);
+ return r;
}
return new PluginResult(status, result);
} catch (JSONException e) {
@@ -95,7 +100,7 @@ public class CameraLauncher extends Plugin {
/**
* Take a picture with the camera.
* When an image is captured or the camera view is cancelled, the result is returned
- * in DroidGap.onActivityResult, which forwards the result to this.onActivityResult.
+ * in PhonegapActivity.onActivityResult, which forwards the result to this.onActivityResult.
*
* The image can either be returned as a base64 string or a URI that points to the file.
* To display base64 string in an img tag, set the source to:
@@ -124,10 +129,14 @@ public class CameraLauncher extends Plugin {
/**
* Get image from photo library.
*
- * @param returnType
+ * @param quality Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
+ * @param srcType The album to get image from.
+ * @param returnType Set the type of image to return.
*/
// TODO: Images selected from SDCARD don't display correctly, but from CAMERA ALBUM do!
- public void getImage(int srcType, int returnType) {
+ public void getImage(int quality, int srcType, int returnType) {
+ this.mQuality = quality;
+
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
@@ -190,8 +199,11 @@ public class CameraLauncher extends Plugin {
os.close();
// Send Uri back to JavaScript for viewing image
- this.sendJavascript("navigator.camera.success('" + uri.toString() + "');");
+ this.success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
}
+ bitmap.recycle();
+ bitmap = null;
+ System.gc();
} catch (IOException e) {
e.printStackTrace();
this.failPicture("Error capturing image.");
@@ -219,6 +231,9 @@ public class CameraLauncher extends Plugin {
try {
Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri));
this.processPicture(bitmap);
+ bitmap.recycle();
+ bitmap = null;
+ System.gc();
} catch (FileNotFoundException e) {
e.printStackTrace();
this.failPicture("Error retrieving image.");
@@ -227,7 +242,7 @@ public class CameraLauncher extends Plugin {
// If sending filename back
else if (destType == FILE_URI) {
- this.sendJavascript("navigator.camera.success('" + uri + "');");
+ this.success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
}
}
else if (resultCode == Activity.RESULT_CANCELED) {
@@ -251,12 +266,16 @@ public class CameraLauncher extends Plugin {
byte[] code = jpeg_data.toByteArray();
byte[] output = Base64.encodeBase64(code);
String js_out = new String(output);
- this.sendJavascript("navigator.camera.success('" + js_out + "');");
+ this.success(new PluginResult(PluginResult.Status.OK, js_out), this.callbackId);
+ js_out = null;
+ output = null;
+ code = null;
}
}
catch(Exception e) {
this.failPicture("Error compressing image.");
}
+ jpeg_data = null;
}
/**
@@ -265,6 +284,6 @@ public class CameraLauncher extends Plugin {
* @param err
*/
public void failPicture(String err) {
- this.sendJavascript("navigator.camera.error('" + err + "');");
+ this.error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId);
}
}
diff --git a/framework/src/com/phonegap/CompassListener.java b/framework/src/com/phonegap/CompassListener.java
index 1f2a2994..77ba96b5 100755
--- a/framework/src/com/phonegap/CompassListener.java
+++ b/framework/src/com/phonegap/CompassListener.java
@@ -12,6 +12,7 @@ import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
+import com.phonegap.api.PhonegapActivity;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
@@ -55,7 +56,7 @@ public class CompassListener extends Plugin implements SensorEventListener {
*
* @param ctx The context of the main Activity.
*/
- public void setContext(DroidGap ctx) {
+ public void setContext(PhonegapActivity ctx) {
super.setContext(ctx);
this.sensorManager = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE);
}
diff --git a/framework/src/com/phonegap/ContactAccessor.java b/framework/src/com/phonegap/ContactAccessor.java
index 352853ba..1cc49644 100644
--- a/framework/src/com/phonegap/ContactAccessor.java
+++ b/framework/src/com/phonegap/ContactAccessor.java
@@ -155,6 +155,9 @@ public abstract class ContactAccessor {
else if (key.startsWith("urls")) {
map.put("urls", true);
}
+ else if (key.startsWith("photos")) {
+ map.put("photos", true);
+ }
}
}
catch (JSONException e) {
@@ -162,11 +165,36 @@ public abstract class ContactAccessor {
}
return map;
}
+
+ /**
+ * Convenience method to get a string from a JSON object. Saves a
+ * lot of try/catch writing.
+ * If the property is not found in the object null will be returned.
+ *
+ * @param obj contact object to search
+ * @param property to be looked up
+ * @return The value of the property
+ */
+ protected String getJsonString(JSONObject obj, String property) {
+ String value = null;
+ try {
+ value = obj.getString(property);
+ if (value.equals("null")) {
+ Log.d(LOG_TAG, property + " is string called 'null'");
+ value = null;
+ }
+ }
+ catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get = " + e.getMessage());
+ }
+ return value;
+ }
/**
* Handles adding a JSON Contact object into the database.
+ * @return TODO
*/
- public abstract void save(JSONObject contact);
+ public abstract boolean save(JSONObject contact);
/**
* Handles searching through SDK-specific contacts API.
diff --git a/framework/src/com/phonegap/ContactAccessorSdk3_4.java b/framework/src/com/phonegap/ContactAccessorSdk3_4.java
index c6d213ab..446b5587 100644
--- a/framework/src/com/phonegap/ContactAccessorSdk3_4.java
+++ b/framework/src/com/phonegap/ContactAccessorSdk3_4.java
@@ -36,6 +36,7 @@ import org.json.JSONObject;
import android.app.Activity;
import android.content.ContentResolver;
+import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.provider.Contacts;
@@ -62,6 +63,7 @@ import android.webkit.WebView;
*/
@SuppressWarnings("deprecation")
public class ContactAccessorSdk3_4 extends ContactAccessor {
+ private static final String PEOPLE_ID_EQUALS = "people._id = ?";
/**
* A static map that converts the JavaScript property name to Android database column name.
*/
@@ -102,22 +104,28 @@ public class ContactAccessorSdk3_4 extends ContactAccessor {
*/
public JSONArray search(JSONArray fields, JSONObject options) {
String searchTerm = "";
- int limit = 1;
- boolean multiple = false;
- try {
- searchTerm = options.getString("filter");
+ int limit = Integer.MAX_VALUE;
+ boolean multiple = true;
+
+ if (options != null) {
+ searchTerm = options.optString("filter");
if (searchTerm.length()==0) {
searchTerm = "%";
}
else {
searchTerm = "%" + searchTerm + "%";
}
- multiple = options.getBoolean("multiple");
- if (multiple) {
- limit = options.getInt("limit");
+ try {
+ multiple = options.getBoolean("multiple");
+ if (!multiple) {
+ limit = 1;
+ }
+ } catch (JSONException e) {
+ // Multiple was not specified so we assume the default is true.
}
- } catch (JSONException e) {
- Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ else {
+ searchTerm = "%";
}
ContentResolver cr = mApp.getContentResolver();
@@ -140,7 +148,7 @@ public class ContactAccessorSdk3_4 extends ContactAccessor {
// Do query for name and note
Cursor cur = cr.query(People.CONTENT_URI,
new String[] {People.DISPLAY_NAME, People.NOTES},
- "people._id = ?",
+ PEOPLE_ID_EQUALS,
new String[] {contactId},
null);
cur.moveToFirst();
@@ -305,11 +313,13 @@ public class ContactAccessorSdk3_4 extends ContactAccessor {
while (cursor.moveToNext()) {
im = new JSONObject();
try{
- im.put("primary", false);
+ im.put("id", cursor.getString(
+ cursor.getColumnIndex(ContactMethods._ID)));
+ im.put("perf", false);
im.put("value", cursor.getString(
cursor.getColumnIndex(ContactMethodsColumns.DATA)));
- im.put("type", cursor.getString(
- cursor.getColumnIndex(ContactMethodsColumns.TYPE)));
+ im.put("type", getContactType(cursor.getInt(
+ cursor.getColumnIndex(ContactMethodsColumns.TYPE))));
ims.put(im);
} catch (JSONException e) {
Log.e(LOG_TAG, e.getMessage(), e);
@@ -335,13 +345,10 @@ public class ContactAccessorSdk3_4 extends ContactAccessor {
while (cursor.moveToNext()) {
organization = new JSONObject();
try{
+ organization.put("id", cursor.getString(cursor.getColumnIndex(Organizations._ID)));
organization.put("name", cursor.getString(cursor.getColumnIndex(Organizations.COMPANY)));
organization.put("title", cursor.getString(cursor.getColumnIndex(Organizations.TITLE)));
// organization.put("department", cursor.getString(cursor.getColumnIndex(Organizations)));
- // organization.put("description", cursor.getString(cursor.getColumnIndex(Organizations)));
- // organization.put("endDate", cursor.getString(cursor.getColumnIndex(Organizations)));
- // organization.put("location", cursor.getString(cursor.getColumnIndex(Organizations)));
- // organization.put("startDate", cursor.getString(cursor.getColumnIndex(Organizations)));
organizations.put(organization);
} catch (JSONException e) {
Log.e(LOG_TAG, e.getMessage(), e);
@@ -368,6 +375,7 @@ public class ContactAccessorSdk3_4 extends ContactAccessor {
while (cursor.moveToNext()) {
address = new JSONObject();
try{
+ address.put("id", cursor.getString(cursor.getColumnIndex(ContactMethods._ID)));
address.put("formatted", cursor.getString(cursor.getColumnIndex(ContactMethodsColumns.DATA)));
addresses.put(address);
} catch (JSONException e) {
@@ -394,9 +402,10 @@ public class ContactAccessorSdk3_4 extends ContactAccessor {
while (cursor.moveToNext()) {
phone = new JSONObject();
try{
- phone.put("primary", false);
+ phone.put("id", cursor.getString(cursor.getColumnIndex(Phones._ID)));
+ phone.put("perf", false);
phone.put("value", cursor.getString(cursor.getColumnIndex(Phones.NUMBER)));
- phone.put("type", cursor.getString(cursor.getColumnIndex(Phones.TYPE)));
+ phone.put("type", getPhoneType(cursor.getInt(cursor.getColumnIndex(Phones.TYPE))));
phones.put(phone);
} catch (JSONException e) {
Log.e(LOG_TAG, e.getMessage(), e);
@@ -422,7 +431,8 @@ public class ContactAccessorSdk3_4 extends ContactAccessor {
while (cursor.moveToNext()) {
email = new JSONObject();
try{
- email.put("primary", false);
+ email.put("id", cursor.getString(cursor.getColumnIndex(ContactMethods._ID)));
+ email.put("perf", false);
email.put("value", cursor.getString(cursor.getColumnIndex(ContactMethods.DATA)));
// TODO Find out why adding an email type throws and exception
//email.put("type", cursor.getString(cursor.getColumnIndex(ContactMethods.TYPE)));
@@ -434,10 +444,372 @@ public class ContactAccessorSdk3_4 extends ContactAccessor {
return emails;
}
+ /**
+ * This method will save a contact object into the devices contacts database.
+ *
+ * @param contact the contact to be saved.
+ * @returns true if the contact is successfully saved, false otherwise.
+ */
@Override
- public void save(JSONObject contact) {
- // TODO Auto-generated method stub
+ public boolean save(JSONObject contact) {
+ ContentValues personValues = new ContentValues();
+
+ String id = getJsonString(contact, "id");
+
+ String name = getJsonString(contact, "displayName");
+ if (name != null) {
+ personValues.put(Contacts.People.NAME, name);
+ }
+ String note = getJsonString(contact, "note");
+ if (note != null) {
+ personValues.put(Contacts.People.NOTES, note);
+ }
+
+ /* STARRED 0 = Contacts, 1 = Favorites */
+ personValues.put(Contacts.People.STARRED, 0);
+ Uri newPersonUri;
+ // Add new contact
+ if (id == null) {
+ newPersonUri = Contacts.People.createPersonInMyContactsGroup(mApp.getContentResolver(), personValues);
+ }
+ // modify existing contact
+ else {
+ newPersonUri = Uri.withAppendedPath(Contacts.People.CONTENT_URI, id);
+ mApp.getContentResolver().update(newPersonUri, personValues, PEOPLE_ID_EQUALS, new String[]{id});
+ }
+
+ if (newPersonUri != null) {
+ // phoneNumbers
+ savePhoneNumbers(contact, newPersonUri);
+ // emails
+ saveEntries(contact, newPersonUri, "emails", Contacts.KIND_EMAIL);
+ // addresses
+ saveAddresses(contact, newPersonUri);
+ // organizations
+ saveOrganizations(contact, newPersonUri);
+ // ims
+ saveEntries(contact, newPersonUri, "ims", Contacts.KIND_IM);
+
+ // Successfully create a Contact
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Takes a JSON contact object and loops through the available organizations. If the
+ * organization has an id that is not equal to null the organization will be updated in the database.
+ * If the id is null then we treat it as a new organization.
+ *
+ * @param contact the contact to extract the organizations from
+ * @param uri the base URI for this contact.
+ */
+ private void saveOrganizations(JSONObject contact, Uri newPersonUri) {
+ ContentValues values = new ContentValues();
+ Uri orgUri = Uri.withAppendedPath(newPersonUri,
+ Contacts.Organizations.CONTENT_DIRECTORY);
+ String id = null;
+ try {
+ JSONArray orgs = contact.getJSONArray("organizations");
+ if (orgs != null && orgs.length() > 0) {
+ JSONObject org;
+ for (int i=0; i 0) {
+ JSONObject entry;
+ values.put(Contacts.ContactMethods.KIND, Contacts.KIND_POSTAL);
+ for (int i=0; i 0 ) {
+ buffer.append(", ");
+ }
+ buffer.append(getJsonString(entry, "region"));
+ }
+ if (getJsonString(entry, "postalCode") != null ) {
+ if (buffer.length() > 0 ) {
+ buffer.append(", ");
+ }
+ buffer.append(getJsonString(entry, "postalCode"));
+ }
+ if (getJsonString(entry, "country") != null ) {
+ if (buffer.length() > 0 ) {
+ buffer.append(", ");
+ }
+ buffer.append(getJsonString(entry, "country"));
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * Takes a JSON contact object and loops through the available entries (Emails/IM's). If the
+ * entry has an id that is not equal to null the entry will be updated in the database.
+ * If the id is null then we treat it as a new entry.
+ *
+ * @param contact the contact to extract the entries from
+ * @param uri the base URI for this contact.
+ */
+ private void saveEntries(JSONObject contact, Uri uri, String dataType, int contactKind) {
+ ContentValues values = new ContentValues();
+ Uri newUri = Uri.withAppendedPath(uri,
+ Contacts.People.ContactMethods.CONTENT_DIRECTORY);
+ String id = null;
+
+ try {
+ JSONArray entries = contact.getJSONArray(dataType);
+ if (entries != null && entries.length() > 0) {
+ JSONObject entry;
+ values.put(Contacts.ContactMethods.KIND, contactKind);
+ for (int i=0; i 0) {
+ JSONObject phone;
+ for (int i=0; i 0) ? true : false;
diff --git a/framework/src/com/phonegap/ContactAccessorSdk5.java b/framework/src/com/phonegap/ContactAccessorSdk5.java
index 1267a64a..a1e7d428 100644
--- a/framework/src/com/phonegap/ContactAccessorSdk5.java
+++ b/framework/src/com/phonegap/ContactAccessorSdk5.java
@@ -5,6 +5,7 @@
*
* Copyright (c) 2005-2010, Nitobi Software Inc.
* Copyright (c) 2010, IBM Corporation
+ * Copyright (c) 2011, Giant Leap Technologies AS
*/
/*
* Copyright (C) 2009 The Android Open Source Project
@@ -24,16 +25,33 @@
package com.phonegap;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
import java.util.Map;
+import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import android.accounts.Account;
+import android.accounts.AccountManager;
import android.app.Activity;
+import android.content.ContentProviderOperation;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
import android.provider.ContactsContract;
import android.util.Log;
import android.webkit.WebView;
@@ -57,7 +75,14 @@ import android.webkit.WebView;
*
*/
public class ContactAccessorSdk5 extends ContactAccessor {
+
+ /**
+ * Keep the photo size under the 1 MB blog limit.
+ */
+ private static final long MAX_PHOTO_SIZE = 1048576;
+ private static final String EMAIL_REGEXP = ".+@.+\\.+.+"; /* @.*/
+
/**
* A static map that converts the JavaScript property name to Android database column name.
*/
@@ -90,26 +115,14 @@ public class ContactAccessorSdk5 extends ContactAccessor {
dbMap.put("organizations.name", ContactsContract.CommonDataKinds.Organization.COMPANY);
dbMap.put("organizations.department", ContactsContract.CommonDataKinds.Organization.DEPARTMENT);
dbMap.put("organizations.title", ContactsContract.CommonDataKinds.Organization.TITLE);
- dbMap.put("organizations.location", ContactsContract.CommonDataKinds.Organization.OFFICE_LOCATION);
- dbMap.put("organizations.description", ContactsContract.CommonDataKinds.Organization.JOB_DESCRIPTION);
- //dbMap.put("published", null);
- //dbMap.put("updated", null);
+ //dbMap.put("revision", null);
dbMap.put("birthday", ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE);
- dbMap.put("anniversary", ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE);
- //dbMap.put("gender", null);
dbMap.put("note", ContactsContract.CommonDataKinds.Note.NOTE);
- //dbMap.put("preferredUsername", null);
- //dbMap.put("photos.value", null);
- //dbMap.put("tags.value", null);
- dbMap.put("relationships", ContactsContract.CommonDataKinds.Relation.NAME);
- dbMap.put("relationships.value", ContactsContract.CommonDataKinds.Relation.NAME);
+ dbMap.put("photos.value", ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
+ //dbMap.put("categories.value", null);
dbMap.put("urls", ContactsContract.CommonDataKinds.Website.URL);
dbMap.put("urls.value", ContactsContract.CommonDataKinds.Website.URL);
- //dbMap.put("accounts.domain", null);
- //dbMap.put("accounts.username", null);
- //dbMap.put("accounts.userid", null);
- //dbMap.put("utcOffset", null);
- //dbMap.put("connected", null);
+ //dbMap.put("timezone", null);
}
/**
@@ -134,23 +147,33 @@ public class ContactAccessorSdk5 extends ContactAccessor {
// Get the find options
String searchTerm = "";
- int limit = 1;
- boolean multiple = false;
- try {
- searchTerm = options.getString("filter");
+ int limit = Integer.MAX_VALUE;
+ boolean multiple = true;
+
+ if (options != null) {
+ searchTerm = options.optString("filter");
if (searchTerm.length()==0) {
searchTerm = "%";
}
else {
searchTerm = "%" + searchTerm + "%";
}
- multiple = options.getBoolean("multiple");
- if (multiple) {
- limit = options.getInt("limit");
+ try {
+ multiple = options.getBoolean("multiple");
+ if (!multiple) {
+ limit = 1;
+ }
+ } catch (JSONException e) {
+ // Multiple was not specified so we assume the default is true.
}
- } catch (JSONException e) {
- Log.e(LOG_TAG, e.getMessage(), e);
}
+ else {
+ searchTerm = "%";
+ }
+
+ //Log.d(LOG_TAG, "Search Term = " + searchTerm);
+ //Log.d(LOG_TAG, "Field Length = " + fields.length());
+ //Log.d(LOG_TAG, "Fields = " + fields.toString());
// Loop through the fields the user provided to see what data should be returned.
HashMap populate = buildPopulationSet(fields);
@@ -158,14 +181,36 @@ public class ContactAccessorSdk5 extends ContactAccessor {
// Build the ugly where clause and where arguments for one big query.
WhereOptions whereOptions = buildWhereClause(fields, searchTerm);
- // Get all the rows where the search term matches the fields passed in.
- Cursor c = mApp.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
- null,
+ // Get all the id's where the search term matches the fields passed in.
+ Cursor idCursor = mApp.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
+ new String[] { ContactsContract.Data.CONTACT_ID },
whereOptions.getWhere(),
whereOptions.getWhereArgs(),
ContactsContract.Data.CONTACT_ID + " ASC");
+ // Create a set of unique ids
+ //Log.d(LOG_TAG, "ID cursor query returns = " + idCursor.getCount());
+ Set contactIds = new HashSet();
+ while (idCursor.moveToNext()) {
+ contactIds.add(idCursor.getString(idCursor.getColumnIndex(ContactsContract.Data.CONTACT_ID)));
+ }
+ idCursor.close();
+
+ // Build a query that only looks at ids
+ WhereOptions idOptions = buildIdClause(contactIds, searchTerm);
+
+ // Do the id query
+ Cursor c = mApp.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
+ null,
+ idOptions.getWhere(),
+ idOptions.getWhereArgs(),
+ ContactsContract.Data.CONTACT_ID + " ASC");
+
+
+ //Log.d(LOG_TAG, "Cursor length = " + c.getCount());
+
String contactId = "";
+ String rawId = "";
String oldContactId = "";
boolean newContact = true;
String mimetype = "";
@@ -178,119 +223,157 @@ public class ContactAccessorSdk5 extends ContactAccessor {
JSONArray emails = new JSONArray();
JSONArray ims = new JSONArray();
JSONArray websites = new JSONArray();
- JSONArray relationships = new JSONArray();
+ JSONArray photos = new JSONArray();
- while (c.moveToNext() && (contacts.length() < (limit-1))) {
- try {
- contactId = c.getString(c.getColumnIndex(ContactsContract.Data.CONTACT_ID));
-
- // If we are in the first row set the oldContactId
- if (c.getPosition() == 0) {
- oldContactId = contactId;
- }
-
- // When the contact ID changes we need to push the Contact object
- // to the array of contacts and create new objects.
- if (!oldContactId.equals(contactId)) {
- // Populate the Contact object with it's arrays
- // and push the contact into the contacts array
- contacts.put(populateContact(contact, organizations, addresses, phones,
- emails, ims, websites, relationships));
+ if (c.getCount() > 0) {
+ while (c.moveToNext() && (contacts.length() <= (limit-1))) {
+ try {
+ contactId = c.getString(c.getColumnIndex(ContactsContract.Data.CONTACT_ID));
+ rawId = c.getString(c.getColumnIndex(ContactsContract.Data.RAW_CONTACT_ID));
- // Clean up the objects
- contact = new JSONObject();
- organizations = new JSONArray();
- addresses = new JSONArray();
- phones = new JSONArray();
- emails = new JSONArray();
- ims = new JSONArray();
- websites = new JSONArray();
- relationships = new JSONArray();
-
- // Set newContact to true as we are starting to populate a new contact
- newContact = true;
- }
-
- // When we detect a new contact set the ID and display name.
- // These fields are available in every row in the result set returned.
- if (newContact) {
- newContact = false;
- contact.put("id", contactId);
- contact.put("displayName", c.getString(c.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)));
- }
-
- // Grab the mimetype of the current row as it will be used in a lot of comparisons
- mimetype = c.getString(c.getColumnIndex(ContactsContract.Data.MIMETYPE));
-
- if (mimetype.equals(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
- && isRequired("name",populate)) {
- contact.put("name", nameQuery(c));
- }
- else if (mimetype.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
- && isRequired("phoneNumbers",populate)) {
- phones.put(phoneQuery(c));
- }
- else if (mimetype.equals(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
- && isRequired("emails",populate)) {
- emails.put(emailQuery(c));
- }
- else if (mimetype.equals(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)
- && isRequired("addresses",populate)) {
- addresses.put(addressQuery(c));
- }
- else if (mimetype.equals(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
- && isRequired("organizations",populate)) {
- organizations.put(organizationQuery(c));
- }
- else if (mimetype.equals(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE)
- && isRequired("ims",populate)) {
- ims.put(imQuery(c));
- }
- else if (mimetype.equals(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE)
- && isRequired("note",populate)) {
- contact.put("note",c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Note.NOTE)));
- }
- else if (mimetype.equals(ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE)
- && isRequired("nickname",populate)) {
- contact.put("nickname",c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Nickname.NAME)));
- }
- else if (mimetype.equals(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE)
- && isRequired("urls",populate)) {
- websites.put(websiteQuery(c));
- }
- else if (mimetype.equals(ContactsContract.CommonDataKinds.Relation.CONTENT_ITEM_TYPE)
- && isRequired("relationships",populate)) {
- relationships.put(relationshipQuery(c));
- }
- else if (mimetype.equals(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE)) {
- if (ContactsContract.CommonDataKinds.Event.TYPE_ANNIVERSARY == c.getInt(c.getColumnIndex(ContactsContract.CommonDataKinds.Event.TYPE))
- && isRequired("anniversary",populate)) {
- contact.put("anniversary", c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Event.START_DATE)));
+ // If we are in the first row set the oldContactId
+ if (c.getPosition() == 0) {
+ oldContactId = contactId;
}
- else if (ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY == c.getInt(c.getColumnIndex(ContactsContract.CommonDataKinds.Event.TYPE))
- && isRequired("birthday",populate)) {
- contact.put("birthday", c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Event.START_DATE)));
+
+ // When the contact ID changes we need to push the Contact object
+ // to the array of contacts and create new objects.
+ if (!oldContactId.equals(contactId)) {
+ // Populate the Contact object with it's arrays
+ // and push the contact into the contacts array
+ contacts.put(populateContact(contact, organizations, addresses, phones,
+ emails, ims, websites, photos));
+
+ // Clean up the objects
+ contact = new JSONObject();
+ organizations = new JSONArray();
+ addresses = new JSONArray();
+ phones = new JSONArray();
+ emails = new JSONArray();
+ ims = new JSONArray();
+ websites = new JSONArray();
+ photos = new JSONArray();
+
+ // Set newContact to true as we are starting to populate a new contact
+ newContact = true;
+ }
+
+ // When we detect a new contact set the ID and display name.
+ // These fields are available in every row in the result set returned.
+ if (newContact) {
+ newContact = false;
+ contact.put("id", contactId);
+ contact.put("rawId", rawId);
+ contact.put("displayName", c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME)));
+ }
+
+ // Grab the mimetype of the current row as it will be used in a lot of comparisons
+ mimetype = c.getString(c.getColumnIndex(ContactsContract.Data.MIMETYPE));
+
+ if (mimetype.equals(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ && isRequired("name",populate)) {
+ contact.put("name", nameQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ && isRequired("phoneNumbers",populate)) {
+ phones.put(phoneQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+ && isRequired("emails",populate)) {
+ emails.put(emailQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)
+ && isRequired("addresses",populate)) {
+ addresses.put(addressQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
+ && isRequired("organizations",populate)) {
+ organizations.put(organizationQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE)
+ && isRequired("ims",populate)) {
+ ims.put(imQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE)
+ && isRequired("note",populate)) {
+ contact.put("note",c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Note.NOTE)));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE)
+ && isRequired("nickname",populate)) {
+ contact.put("nickname",c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Nickname.NAME)));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE)
+ && isRequired("urls",populate)) {
+ websites.put(websiteQuery(c));
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE)) {
+ if (ContactsContract.CommonDataKinds.Event.TYPE_BIRTHDAY == c.getInt(c.getColumnIndex(ContactsContract.CommonDataKinds.Event.TYPE))
+ && isRequired("birthday",populate)) {
+ contact.put("birthday", c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.Event.START_DATE)));
+ }
+ }
+ else if (mimetype.equals(ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ && isRequired("photos",populate)) {
+ photos.put(photoQuery(c, contactId));
}
}
+ catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(),e);
+ }
+
+ // Set the old contact ID
+ oldContactId = contactId;
}
- catch (JSONException e) {
- Log.e(LOG_TAG, e.getMessage(),e);
+
+ // Push the last contact into the contacts array
+ if (contacts.length() < limit) {
+ contacts.put(populateContact(contact, organizations, addresses, phones,
+ emails, ims, websites, photos));
}
-
- // Set the old contact ID
- oldContactId = contactId;
}
c.close();
- // Push the last contact into the contacts array
- contacts.put(populateContact(contact, organizations, addresses, phones,
- emails, ims, websites, relationships));
totalEnd = System.currentTimeMillis();
Log.d(LOG_TAG,"Total time = " + (totalEnd-totalStart));
return contacts;
}
+ /**
+ * Builds a where clause all all the ids passed into the method
+ * @param contactIds a set of unique contact ids
+ * @param searchTerm what to search for
+ * @return an object containing the selection and selection args
+ */
+ private WhereOptions buildIdClause(Set contactIds, String searchTerm) {
+ WhereOptions options = new WhereOptions();
+
+ // If the user is searching for every contact then short circuit the method
+ // and return a shorter where clause to be searched.
+ if (searchTerm.equals("%")) {
+ options.setWhere("(" + ContactsContract.Data.CONTACT_ID + " LIKE ? )");
+ options.setWhereArgs(new String[] {searchTerm});
+ return options;
+ }
+
+ // This clause means that there are specific ID's to be populated
+ Iterator it = contactIds.iterator();
+ StringBuffer buffer = new StringBuffer("(");
+
+ while (it.hasNext()) {
+ buffer.append("'" + it.next() + "'");
+ if (it.hasNext()) {
+ buffer.append(",");
+ }
+ }
+ buffer.append(")");
+
+ options.setWhere(ContactsContract.Data.CONTACT_ID + " IN " + buffer.toString());
+ options.setWhereArgs(null);
+
+ return options;
+ }
+
/**
* Create a new contact using a JSONObject to hold all the data.
* @param contact
@@ -300,12 +383,12 @@ public class ContactAccessorSdk5 extends ContactAccessor {
* @param emails array of emails
* @param ims array of instant messenger addresses
* @param websites array of websites
- * @param relationships array of relationships
+ * @param photos
* @return
*/
private JSONObject populateContact(JSONObject contact, JSONArray organizations,
JSONArray addresses, JSONArray phones, JSONArray emails,
- JSONArray ims, JSONArray websites, JSONArray relationships) {
+ JSONArray ims, JSONArray websites, JSONArray photos) {
try {
contact.put("organizations", organizations);
contact.put("addresses", addresses);
@@ -313,7 +396,7 @@ public class ContactAccessorSdk5 extends ContactAccessor {
contact.put("emails", emails);
contact.put("ims", ims);
contact.put("websites", websites);
- contact.put("relationships", relationships);
+ contact.put("photos", photos);
}
catch (JSONException e) {
Log.e(LOG_TAG,e.getMessage(),e);
@@ -345,6 +428,7 @@ public class ContactAccessorSdk5 extends ContactAccessor {
String key;
try {
+ //Log.d(LOG_TAG, "How many fields do we have = " + fields.length());
for (int i=0; i 1) {
+ for(Account a : accounts){
+ if(a.type.contains("eas")&& a.name.matches(EMAIL_REGEXP)) /*Exchange ActiveSync*/
+ {
+ account = a;
+ break;
+ }
+ }
+ if(account == null){
+ for(Account a : accounts){
+ if(a.type.contains("com.google") && a.name.matches(EMAIL_REGEXP)) /*Google sync provider*/
+ {
+ account = a;
+ break;
+ }
+ }
+ }
+ if(account == null){
+ for(Account a : accounts){
+ if(a.name.matches(EMAIL_REGEXP)) /*Last resort, just look for an email address...*/
+ {
+ account = a;
+ break;
+ }
+ }
+ }
+ }
+
+ if(account == null)
+ return false;
+
+ String id = getJsonString(contact, "id");
+ // Create new contact
+ if (id == null) {
+ return createNewContact(contact, account);
+ }
+ // Modify existing contact
+ else {
+ return modifyContact(id, contact, account);
+ }
+ }
+
+ /**
+ * Creates a new contact and stores it in the database
+ *
+ * @param id the raw contact id which is required for linking items to the contact
+ * @param contact the contact to be saved
+ * @param account the account to be saved under
+ */
+ private boolean modifyContact(String id, JSONObject contact, Account account) {
+ // Get the RAW_CONTACT_ID which is needed to insert new values in an already existing contact.
+ // But not needed to update existing values.
+ int rawId = (new Integer(getJsonString(contact,"rawId"))).intValue();
+
+ // Create a list of attributes to add to the contact database
+ ArrayList ops = new ArrayList();
+
+ //Add contact type
+ ops.add(ContentProviderOperation.newUpdate(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
+ .build());
+
+ // Modify name
+ JSONObject name;
+ try {
+ String displayName = getJsonString(contact, "displayName");
+ name = contact.getJSONObject("name");
+ if (displayName != null || name != null) {
+ ContentProviderOperation.Builder builder = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(ContactsContract.Data.CONTACT_ID + "=? AND " +
+ ContactsContract.Data.MIMETYPE + "=?",
+ new String[]{id, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE});
+
+ if (displayName != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName);
+ }
+
+ String familyName = getJsonString(name, "familyName");
+ if (familyName != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, familyName);
+ }
+ String middleName = getJsonString(name, "middleName");
+ if (middleName != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, middleName);
+ }
+ String givenName = getJsonString(name, "givenName");
+ if (givenName != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, givenName);
+ }
+ String honorificPrefix = getJsonString(name, "honorificPrefix");
+ if (honorificPrefix != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, honorificPrefix);
+ }
+ String honorificSuffix = getJsonString(name, "honorificSuffix");
+ if (honorificSuffix != null) {
+ builder.withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, honorificSuffix);
+ }
+
+ ops.add(builder.build());
+ }
+ } catch (JSONException e1) {
+ Log.d(LOG_TAG, "Could not get name");
+ }
+
+ // Modify phone numbers
+ JSONArray phones = null;
+ try {
+ phones = contact.getJSONArray("phoneNumbers");
+ if (phones != null) {
+ for (int i=0; i ops,
+ JSONObject website) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Website.DATA, getJsonString(website, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Website.TYPE, getContactType(getJsonString(website, "type")))
+ .build());
+ }
+
+ /**
+ * Add an im to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param im the item to be inserted
+ */
+ private void insertIm(ArrayList ops, JSONObject im) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Im.DATA, getJsonString(im, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Im.TYPE, getContactType(getJsonString(im, "type")))
+ .build());
+ }
+
+ /**
+ * Add an organization to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param org the item to be inserted
+ */
+ private void insertOrganization(ArrayList ops,
+ JSONObject org) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Organization.DEPARTMENT, getJsonString(org, "department"))
+ .withValue(ContactsContract.CommonDataKinds.Organization.COMPANY, getJsonString(org, "name"))
+ .withValue(ContactsContract.CommonDataKinds.Organization.TITLE, getJsonString(org, "title"))
+ .build());
+ }
+
+ /**
+ * Add an address to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param address the item to be inserted
+ */
+ private void insertAddress(ArrayList ops,
+ JSONObject address) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS, getJsonString(address, "formatted"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.STREET, getJsonString(address, "streetAddress"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.CITY, getJsonString(address, "locality"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.REGION, getJsonString(address, "region"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE, getJsonString(address, "postalCode"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredPostal.COUNTRY, getJsonString(address, "country"))
+ .build());
+ }
+
+ /**
+ * Add an email to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param email the item to be inserted
+ */
+ private void insertEmail(ArrayList ops,
+ JSONObject email) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Email.DATA, getJsonString(email, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Email.TYPE, getPhoneType(getJsonString(email, "type")))
+ .build());
+ }
+
+ /**
+ * Add a phone to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param phone the item to be inserted
+ */
+ private void insertPhone(ArrayList ops,
+ JSONObject phone) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, getJsonString(phone, "value"))
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, getPhoneType(getJsonString(phone, "type")))
+ .build());
+ }
+
+ /**
+ * Add a phone to a list of database actions to be performed
+ *
+ * @param ops the list of database actions
+ * @param phone the item to be inserted
+ */
+ private void insertPhoto(ArrayList ops,
+ JSONObject photo) {
+ byte[] bytes = getPhotoBytes(getJsonString(photo, "value"));
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.IS_SUPER_PRIMARY, 1)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, bytes)
+ .build());
+ }
+
+ /**
+ * Gets the raw bytes from the supplied filename
+ *
+ * @param filename the file to read the bytes from
+ * @return a byte array
+ * @throws IOException
+ */
+ private byte[] getPhotoBytes(String filename) {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ try {
+ int bytesRead = 0;
+ long totalBytesRead = 0;
+ byte[] data = new byte[8192];
+ InputStream in = getPathFromUri(filename);
+
+ while ((bytesRead = in.read(data, 0, data.length)) != -1 && totalBytesRead <= MAX_PHOTO_SIZE) {
+ buffer.write(data, 0, bytesRead);
+ totalBytesRead += bytesRead;
+ }
+
+ in.close();
+ buffer.flush();
+ } catch (FileNotFoundException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return buffer.toByteArray();
+ }
+ /**
+ * Get an input stream based on file path or uri content://, http://, file://
+ *
+ * @param path
+ * @return an input stream
+ * @throws IOException
+ */
+ private InputStream getPathFromUri(String path) throws IOException {
+ if (path.startsWith("content:")) {
+ Uri uri = Uri.parse(path);
+ return mApp.getContentResolver().openInputStream(uri);
+ }
+ if (path.startsWith("http:") || path.startsWith("file:")) {
+ URL url = new URL(path);
+ return url.openStream();
+ }
+ else {
+ return new FileInputStream(path);
+ }
+ }
+
+ /**
+ * Creates a new contact and stores it in the database
+ *
+ * @param contact the contact to be saved
+ * @param account the account to be saved under
+ */
+ private boolean createNewContact(JSONObject contact, Account account) {
+ // Create a list of attributes to add to the contact database
+ ArrayList ops = new ArrayList();
+
+ //Add contact type
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, account.type)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, account.name)
+ .build());
+
+ // Add name
+ try {
+ JSONObject name = contact.optJSONObject("name");
+ String displayName = contact.getString("displayName");
+ if (displayName != null || name != null) {
+ ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
+ .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, displayName)
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, getJsonString(name, "familyName"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.MIDDLE_NAME, getJsonString(name, "middleName"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, getJsonString(name, "givenName"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.PREFIX, getJsonString(name, "honorificPrefix"))
+ .withValue(ContactsContract.CommonDataKinds.StructuredName.SUFFIX, getJsonString(name, "honorificSuffix"))
+ .build());
+ }
+ }
+ catch (JSONException e) {
+ Log.d(LOG_TAG, "Could not get name object");
+ }
+
+ //Add phone numbers
+ JSONArray phones = null;
+ try {
+ phones = contact.getJSONArray("phoneNumbers");
+ if (phones != null) {
+ for (int i=0; i 0) ? true : false;
}
+
+/**************************************************************************
+ *
+ * All methods below this comment are used to convert from JavaScript
+ * text types to Android integer types and vice versa.
+ *
+ *************************************************************************/
+
+ /**
+ * Converts a string from the W3C Contact API to it's Android int value.
+ * @param string
+ * @return Android int value
+ */
+ private int getPhoneType(String string) {
+ int type = ContactsContract.CommonDataKinds.Phone.TYPE_OTHER;
+ if ("home".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_HOME;
+ }
+ else if ("mobile".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE;
+ }
+ else if ("work".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_WORK;
+ }
+ else if ("work fax".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK;
+ }
+ else if ("home fax".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME;
+ }
+ else if ("fax".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK;
+ }
+ else if ("pager".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_PAGER;
+ }
+ else if ("other".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_OTHER;
+ }
+ else if ("car".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_CAR;
+ }
+ else if ("company main".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN;
+ }
+ else if ("isdn".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_ISDN;
+ }
+ else if ("main".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_MAIN;
+ }
+ else if ("other fax".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX;
+ }
+ else if ("radio".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_RADIO;
+ }
+ else if ("telex".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_TELEX;
+ }
+ else if ("work mobile".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE;
+ }
+ else if ("work pager".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER;
+ }
+ else if ("assistant".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT;
+ }
+ else if ("mms".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_MMS;
+ }
+ else if ("callback".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK;
+ }
+ else if ("tty ttd".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD;
+ }
+ else if ("custom".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM;
+ }
+ return type;
+ }
+
+ /**
+ * getPhoneType converts an Android phone type into a string
+ * @param type
+ * @return phone type as string.
+ */
+ private String getPhoneType(int type) {
+ String stringType;
+ switch (type) {
+ case ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM:
+ stringType = "custom";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME:
+ stringType = "home fax";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK:
+ stringType = "work fax";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_HOME:
+ stringType = "home";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE:
+ stringType = "mobile";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_PAGER:
+ stringType = "pager";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_WORK:
+ stringType = "work";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK:
+ stringType = "callback";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_CAR:
+ stringType = "car";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN:
+ stringType = "company main";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX:
+ stringType = "other fax";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_RADIO:
+ stringType = "radio";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_TELEX:
+ stringType = "telex";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD:
+ stringType = "tty tdd";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE:
+ stringType = "work mobile";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER:
+ stringType = "work pager";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT:
+ stringType = "assistant";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_MMS:
+ stringType = "mms";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_ISDN:
+ stringType = "isdn";
+ break;
+ case ContactsContract.CommonDataKinds.Phone.TYPE_OTHER:
+ default:
+ stringType = "other";
+ break;
+ }
+ return stringType;
+ }
+
+ /**
+ * Converts a string from the W3C Contact API to it's Android int value.
+ * @param string
+ * @return Android int value
+ */
+ private int getContactType(String string) {
+ int type = ContactsContract.CommonDataKinds.Email.TYPE_OTHER;
+ if (string!=null) {
+ if ("home".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Email.TYPE_HOME;
+ }
+ else if ("work".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Email.TYPE_WORK;
+ }
+ else if ("other".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Email.TYPE_OTHER;
+ }
+ else if ("mobile".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Email.TYPE_MOBILE;
+ }
+ else if ("custom".equals(string.toLowerCase())) {
+ return ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM;
+ }
+ }
+ return type;
+ }
+
+ /**
+ * getPhoneType converts an Android phone type into a string
+ * @param type
+ * @return phone type as string.
+ */
+ private String getContactType(int type) {
+ String stringType;
+ switch (type) {
+ case ContactsContract.CommonDataKinds.Email.TYPE_CUSTOM:
+ stringType = "custom";
+ break;
+ case ContactsContract.CommonDataKinds.Email.TYPE_HOME:
+ stringType = "home";
+ break;
+ case ContactsContract.CommonDataKinds.Email.TYPE_WORK:
+ stringType = "work";
+ break;
+ case ContactsContract.CommonDataKinds.Email.TYPE_MOBILE:
+ stringType = "mobile";
+ break;
+ case ContactsContract.CommonDataKinds.Email.TYPE_OTHER:
+ default:
+ stringType = "other";
+ break;
+ }
+ return stringType;
+ }
}
\ No newline at end of file
diff --git a/framework/src/com/phonegap/ContactManager.java b/framework/src/com/phonegap/ContactManager.java
index 30ff5051..5a79c37a 100755
--- a/framework/src/com/phonegap/ContactManager.java
+++ b/framework/src/com/phonegap/ContactManager.java
@@ -42,11 +42,11 @@ public class ContactManager extends Plugin {
try {
if (action.equals("search")) {
- JSONArray res = contactAccessor.search(args.getJSONArray(0), args.getJSONObject(1));
- return new PluginResult(status, res);
+ JSONArray res = contactAccessor.search(args.getJSONArray(0), args.optJSONObject(1));
+ return new PluginResult(status, res, "navigator.service.contacts.cast");
}
else if (action.equals("save")) {
- // TODO Coming soon!
+ return new PluginResult(status, contactAccessor.save(args.getJSONObject(0)));
}
else if (action.equals("remove")) {
if (contactAccessor.remove(args.getString(0))) {
diff --git a/framework/src/com/phonegap/Device.java b/framework/src/com/phonegap/Device.java
index b450e1ae..049fc3ca 100755
--- a/framework/src/com/phonegap/Device.java
+++ b/framework/src/com/phonegap/Device.java
@@ -11,6 +11,7 @@ import java.util.TimeZone;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import com.phonegap.api.PhonegapActivity;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
import android.content.Context;
@@ -19,7 +20,7 @@ import android.telephony.TelephonyManager;
public class Device extends Plugin {
- public static String phonegapVersion = "0.9.2"; // PhoneGap version
+ public static String phonegapVersion = "0.9.4"; // PhoneGap version
public static String platform = "Android"; // Device OS
public static String uuid; // Device UUID
@@ -35,7 +36,7 @@ public class Device extends Plugin {
*
* @param ctx The context of the main Activity.
*/
- public void setContext(DroidGap ctx) {
+ public void setContext(PhonegapActivity ctx) {
super.setContext(ctx);
Device.uuid = getUuid();
}
diff --git a/framework/src/com/phonegap/DirectoryManager.java b/framework/src/com/phonegap/DirectoryManager.java
index b1270475..a659527d 100644
--- a/framework/src/com/phonegap/DirectoryManager.java
+++ b/framework/src/com/phonegap/DirectoryManager.java
@@ -11,7 +11,6 @@ import java.io.File;
import android.os.Environment;
import android.os.StatFs;
-import android.util.Log;
/**
* This class provides file directory utilities.
@@ -21,6 +20,8 @@ import android.util.Log;
*/
public class DirectoryManager {
+ private static final String LOG_TAG = "DirectoryManager";
+
/**
* Determine if a file or directory exists.
*
@@ -36,7 +37,6 @@ public class DirectoryManager {
File newPath = constructFilePaths(path.toString(), name);
status = newPath.exists();
}
-
// If no SD card
else{
status = false;
@@ -72,29 +72,6 @@ public class DirectoryManager {
return (freeSpace);
}
- /**
- * Create directory on SD card.
- *
- * @param directoryName The name of the directory to create.
- * @return T=successful, F=failed
- */
- protected static boolean createDirectory(String directoryName) {
- boolean status;
-
- // Make sure SD card exists
- if ((testSaveLocationExists()) && (!directoryName.equals(""))) {
- File path = Environment.getExternalStorageDirectory();
- File newPath = constructFilePaths(path.toString(), directoryName);
- status = newPath.mkdir();
- status = true;
- }
-
- // If no SD card or invalid dir name
- else {
- status = false;
- }
- return status;
- }
/**
* Determine if SD card exists.
@@ -117,95 +94,6 @@ public class DirectoryManager {
return status;
}
- /**
- * Delete directory.
- *
- * @param fileName The name of the directory to delete
- * @return T=deleted, F=could not delete
- */
- protected static boolean deleteDirectory(String fileName) {
- boolean status;
- SecurityManager checker = new SecurityManager();
-
- // Make sure SD card exists
- if ((testSaveLocationExists()) && (!fileName.equals(""))) {
- File path = Environment.getExternalStorageDirectory();
- File newPath = constructFilePaths(path.toString(), fileName);
- checker.checkDelete(newPath.toString());
-
- // If dir to delete is really a directory
- if (newPath.isDirectory()) {
- String[] listfile = newPath.list();
-
- // Delete all files within the specified directory and then delete the directory
- try{
- for (int i=0; i < listfile.length; i++){
- File deletedFile = new File (newPath.toString()+"/"+listfile[i].toString());
- deletedFile.delete();
- }
- newPath.delete();
- Log.i("DirectoryManager deleteDirectory", fileName);
- status = true;
- }
- catch (Exception e){
- e.printStackTrace();
- status = false;
- }
- }
-
- // If dir not a directory, then error
- else {
- status = false;
- }
- }
-
- // If no SD card
- else {
- status = false;
- }
- return status;
- }
-
- /**
- * Delete file.
- *
- * @param fileName The name of the file to delete
- * @return T=deleted, F=not deleted
- */
- protected static boolean deleteFile(String fileName) {
- boolean status;
- SecurityManager checker = new SecurityManager();
-
- // Make sure SD card exists
- if ((testSaveLocationExists()) && (!fileName.equals(""))) {
- File path = Environment.getExternalStorageDirectory();
- File newPath = constructFilePaths(path.toString(), fileName);
- checker.checkDelete(newPath.toString());
-
- // If file to delete is really a file
- if (newPath.isFile()){
- try {
- Log.i("DirectoryManager deleteFile", fileName);
- newPath.delete();
- status = true;
- }catch (SecurityException se){
- se.printStackTrace();
- status = false;
- }
- }
- // If not a file, then error
- else {
- status = false;
- }
- }
-
- // If no SD card
- else {
- status = false;
- }
- return status;
- }
-
/**
* Create a new file object from two file paths.
*
@@ -215,8 +103,12 @@ public class DirectoryManager {
*/
private static File constructFilePaths (String file1, String file2) {
File newPath;
- newPath = new File(file1+"/"+file2);
+ if (file2.startsWith(file1)) {
+ newPath = new File(file2);
+ }
+ else {
+ newPath = new File(file1+"/"+file2);
+ }
return newPath;
}
-
}
\ No newline at end of file
diff --git a/framework/src/com/phonegap/DroidGap.java b/framework/src/com/phonegap/DroidGap.java
index a47ec3c0..7953d8ad 100755
--- a/framework/src/com/phonegap/DroidGap.java
+++ b/framework/src/com/phonegap/DroidGap.java
@@ -3,16 +3,12 @@
* MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
*
* Copyright (c) 2005-2010, Nitobi Software Inc.
- * Copyright (c) 2010, IBM Corporation
+ * Copyright (c) 2010-2011, IBM Corporation
*/
package com.phonegap;
-
-
-import com.phonegap.api.Plugin;
-import com.phonegap.api.PluginManager;
-
-import android.app.Activity;
+import org.json.JSONArray;
+import org.json.JSONException;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
@@ -20,23 +16,28 @@ import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Color;
+import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
+import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
+import android.webkit.JsPromptResult;
import android.webkit.WebSettings;
import android.webkit.WebStorage;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.GeolocationPermissions.Callback;
import android.webkit.WebSettings.LayoutAlgorithm;
-import android.widget.ImageView;
import android.widget.LinearLayout;
+import com.phonegap.api.Plugin;
+import com.phonegap.api.PluginManager;
+import com.phonegap.api.PhonegapActivity;
/**
* This class is the main Android activity that represents the PhoneGap
@@ -54,63 +55,144 @@ import android.widget.LinearLayout;
* @Override
* public void onCreate(Bundle savedInstanceState) {
* super.onCreate(savedInstanceState);
- * super.loadUrl("file:///android_asset/www/index.html");
+ *
+ * // Set properties for activity
+ * super.setStringProperty("loadingDialog", "Title,Message"); // show loading dialog
+ * super.setStringProperty("errorUrl", "file:///android_asset/www/error.html"); // if error loading file in super.loadUrl().
+ *
+ * // Initialize activity
+ * super.init();
+ *
+ * // Add your plugins here or in JavaScript
+ * super.addService("MyService", "com.phonegap.examples.MyService");
+ *
+ * // Clear cache if you want
+ * super.appView.clearCache(true);
+ *
+ * // Load your application
+ * super.setIntegerProperty("splashscreen", R.drawable.splash); // load splash.jpg image from the resource drawable directory
+ * super.loadUrl("file:///android_asset/www/index.html", 3000); // show splash screen 3 sec before loading app
* }
* }
+ *
+ * Properties: The application can be configured using the following properties:
+ *
+ * // Display a native loading dialog. Format for value = "Title,Message".
+ * // (String - default=null)
+ * super.setStringProperty("loadingDialog", "Wait,Loading Demo...");
+ *
+ * // Hide loadingDialog when page loaded instead of when deviceready event
+ * // occurs. (Boolean - default=false)
+ * super.setBooleanProperty("hideLoadingDialogOnPage", true);
+ *
+ * // Cause all links on web page to be loaded into existing web view,
+ * // instead of being loaded into new browser. (Boolean - default=false)
+ * super.setBooleanProperty("loadInWebView", true);
+ *
+ * // Load a splash screen image from the resource drawable directory.
+ * // (Integer - default=0)
+ * super.setIntegerProperty("splashscreen", R.drawable.splash);
+ *
+ * // Time in msec to wait before triggering a timeout error when loading
+ * // with super.loadUrl(). (Integer - default=20000)
+ * super.setIntegerProperty("loadUrlTimeoutValue", 60000);
+ *
+ * // URL to load if there's an error loading specified URL with loadUrl().
+ * // Should be a local URL starting with file://. (String - default=null)
+ * super.setStringProperty("errorUrl", "file:///android_asset/www/error.html");
+ *
+ * // Enable app to keep running in background. (Boolean - default=true)
+ * super.setBooleanProperty("keepRunning", false);
*/
-public class DroidGap extends Activity {
+public class DroidGap extends PhonegapActivity {
- private static final String LOG_TAG = "DroidGap";
+ // The webview for our app
+ protected WebView appView;
+ protected WebViewClient webViewClient;
- protected WebView appView; // The webview for our app
- protected Boolean loadInWebView = false;
- private LinearLayout root;
-
- private BrowserKey mKey;
- public CallbackServer callbackServer;
+ protected LinearLayout root;
+ public boolean bound = false;
+ public CallbackServer callbackServer;
protected PluginManager pluginManager;
+ protected boolean cancelLoadUrl = false;
+ protected boolean clearHistory = false;
- private String url; // The initial URL for our app
- private String baseUrl; // The base of the initial URL for our app
+ // The initial URL for our app
+ private String url;
+
+ // The base of the initial URL for our app
+ private String baseUrl;
+
+ // Plugin to call when activity result is received
+ private Plugin activityResultCallback = null;
+ private boolean activityResultKeepRunning;
+
+ // Flag indicates that a loadUrl timeout occurred
+ private int loadUrlTimeout = 0;
+
+ /*
+ * The variables below are used to cache some of the activity properties.
+ */
+
+ // Flag indicates that "app loading" dialog should be hidden once page is loaded.
+ // The default is to hide it once PhoneGap JavaScript code has initialized.
+ protected boolean hideLoadingDialogOnPageLoad = false;
+
+ // Flag indicates that a URL navigated to from PhoneGap app should be loaded into same webview
+ // instead of being loaded into the web browser.
+ protected boolean loadInWebView = false;
+
+ // Draw a splash screen using an image located in the drawable resource directory.
+ // This is not the same as calling super.loadSplashscreen(url)
+ protected int splashscreen = 0;
+
+ // LoadUrl timeout value in msec (default of 20 sec)
+ protected int loadUrlTimeoutValue = 20000;
+
+ // Keep app running when pause is received. (default = true)
+ // If true, then the JavaScript and native code continue to run in the background
+ // when another application (activity) is started.
+ protected boolean keepRunning = true;
- private Plugin activityResultCallback = null; // Plugin to call when activity result is received
-
/**
* Called when the activity is first created.
*
* @param savedInstanceState
*/
- @Override
+ @Override
public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
+ super.onCreate(savedInstanceState);
+ getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
+ // This builds the view. We could probably get away with NOT having a LinearLayout, but I like having a bucket!
- getWindow().requestFeature(Window.FEATURE_NO_TITLE);
- getWindow().setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN,
- WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
- // This builds the view. We could probably get away with NOT having a LinearLayout, but I like having a bucket!
+ root = new LinearLayout(this);
+ root.setOrientation(LinearLayout.VERTICAL);
+ root.setBackgroundColor(Color.BLACK);
+ root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
+ ViewGroup.LayoutParams.FILL_PARENT, 0.0F));
- root = new LinearLayout(this);
- root.setOrientation(LinearLayout.VERTICAL);
- root.setBackgroundColor(Color.BLACK);
- root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
- ViewGroup.LayoutParams.FILL_PARENT, 0.0F));
- // Uncomment if you want to enable a splashscreen
- // Make sure R.drawable.splash exists
- // appView.setBackgroundColor(0);
- // appView.setBackgroundResource(R.drawable.splash);
-
- initWebView();
- root.addView(this.appView);
- setContentView(root);
- }
-
+ // If url was passed in to intent, then init webview, which will load the url
+ Bundle bundle = this.getIntent().getExtras();
+ if (bundle != null) {
+ String url = bundle.getString("url");
+ if (url != null) {
+ this.init();
+ }
+ }
+ // Setup the hardware volume controls to handle volume control
+ setVolumeControlStream(AudioManager.STREAM_MUSIC);
+ }
+
/**
* Create and initialize web container.
*/
- private void initWebView() {
+ public void init() {
// Create web container
this.appView = new WebView(DroidGap.this);
+ this.appView.setId(100);
this.appView.setLayoutParams(new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.FILL_PARENT,
@@ -126,7 +208,7 @@ public class DroidGap extends Activity {
this.appView.setWebChromeClient(new GapClient(DroidGap.this));
}
- this.appView.setWebViewClient(new GapViewClient(this));
+ this.setWebViewClient(this.appView, new GapViewClient(this));
this.appView.setInitialScale(100);
this.appView.setVerticalScrollBarEnabled(false);
@@ -151,19 +233,365 @@ public class DroidGap extends Activity {
// Bind PhoneGap objects to JavaScript
this.bindBrowser(this.appView);
+
+ // Add web view but make it invisible while loading URL
+ this.appView.setVisibility(View.INVISIBLE);
+ root.addView(this.appView);
+ setContentView(root);
+
+ // Clear cancel flag
+ this.cancelLoadUrl = false;
+
+ // If url specified, then load it
+ String url = this.getStringProperty("url", null);
+ if (url != null) {
+ System.out.println("Loading initial URL="+url);
+ this.loadUrl(url);
+ }
+ }
+
+ /**
+ * Set the WebViewClient.
+ *
+ * @param appView
+ * @param client
+ */
+ protected void setWebViewClient(WebView appView, WebViewClient client) {
+ this.webViewClient = client;
+ appView.setWebViewClient(client);
}
+ /**
+ * Bind PhoneGap objects to JavaScript.
+ *
+ * @param appView
+ */
+ private void bindBrowser(WebView appView) {
+ this.callbackServer = new CallbackServer();
+ this.pluginManager = new PluginManager(appView, this);
+
+ this.addService("App", "com.phonegap.App");
+ this.addService("Geolocation", "com.phonegap.GeoBroker");
+ this.addService("Device", "com.phonegap.Device");
+ this.addService("Accelerometer", "com.phonegap.AccelListener");
+ this.addService("Compass", "com.phonegap.CompassListener");
+ this.addService("Media", "com.phonegap.AudioHandler");
+ this.addService("Camera", "com.phonegap.CameraLauncher");
+ this.addService("Contacts", "com.phonegap.ContactManager");
+ this.addService("Crypto", "com.phonegap.CryptoHandler");
+ this.addService("File", "com.phonegap.FileUtils");
+ this.addService("Location", "com.phonegap.GeoBroker"); // Always add Location, even though it is built-in on 2.x devices. Let JavaScript decide which one to use.
+ this.addService("Network Status", "com.phonegap.NetworkManager");
+ this.addService("Notification", "com.phonegap.Notification");
+ this.addService("Storage", "com.phonegap.Storage");
+ this.addService("Temperature", "com.phonegap.TempListener");
+ this.addService("FileTransfer", "com.phonegap.FileTransfer");
+ }
+
+ /**
+ * Look at activity parameters and process them.
+ * This must be called from the main UI thread.
+ */
+ private void handleActivityParameters() {
+
+ // Init web view if not already done
+ if (this.appView == null) {
+ this.init();
+ }
+
+ // If spashscreen
+ this.splashscreen = this.getIntegerProperty("splashscreen", 0);
+ if (this.splashscreen != 0) {
+ root.setBackgroundResource(this.splashscreen);
+ }
+
+ // If hideLoadingDialogOnPageLoad
+ this.hideLoadingDialogOnPageLoad = this.getBooleanProperty("hideLoadingDialogOnPageLoad", false);
+
+ // If loadInWebView
+ this.loadInWebView = this.getBooleanProperty("loadInWebView", false);
+
+ // If loadUrlTimeoutValue
+ int timeout = this.getIntegerProperty("loadUrlTimeoutValue", 0);
+ if (timeout > 0) {
+ this.loadUrlTimeoutValue = timeout;
+ }
+
+ // If keepRunning
+ this.keepRunning = this.getBooleanProperty("keepRunning", true);
+ }
- @Override
+ /**
+ * Load the url into the webview.
+ *
+ * @param url
+ */
+ public void loadUrl(final String url) {
+ System.out.println("loadUrl("+url+")");
+ this.url = url;
+ int i = url.lastIndexOf('/');
+ if (i > 0) {
+ this.baseUrl = url.substring(0, i);
+ }
+ else {
+ this.baseUrl = this.url;
+ }
+ System.out.println("url="+url+" baseUrl="+baseUrl);
+
+ // Load URL on UI thread
+ final DroidGap me = this;
+ this.runOnUiThread(new Runnable() {
+ public void run() {
+
+ // Handle activity parameters
+ me.handleActivityParameters();
+
+ // Initialize callback server
+ me.callbackServer.init(url);
+
+ // If loadingDialog, then show the App loading dialog
+ String loading = me.getStringProperty("loadingDialog", null);
+ if (loading != null) {
+
+ String title = "";
+ String message = "Loading Application...";
+
+ if (loading.length() > 0) {
+ int comma = loading.indexOf(',');
+ if (comma > 0) {
+ title = loading.substring(0, comma);
+ message = loading.substring(comma+1);
+ }
+ else {
+ title = "";
+ message = loading;
+ }
+ }
+ JSONArray parm = new JSONArray();
+ parm.put(title);
+ parm.put(message);
+ me.pluginManager.exec("Notification", "activityStart", null, parm.toString(), false);
+ }
+
+ // Create a timeout timer for loadUrl
+ final int currentLoadUrlTimeout = me.loadUrlTimeout;
+ Runnable runnable = new Runnable() {
+ public void run() {
+ try {
+ synchronized(this) {
+ wait(me.loadUrlTimeoutValue);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ // If timeout, then stop loading and handle error
+ if (me.loadUrlTimeout == currentLoadUrlTimeout) {
+ me.appView.stopLoading();
+ me.webViewClient.onReceivedError(me.appView, -6, "The connection to the server was unsuccessful.", url);
+ }
+ }
+ };
+ Thread thread = new Thread(runnable);
+ thread.start();
+ me.appView.loadUrl(url);
+ }
+ });
+ }
+
+ /**
+ * Load the url into the webview after waiting for period of time.
+ * This is used to display the splashscreen for certain amount of time.
+ *
+ * @param url
+ * @param time The number of ms to wait before loading webview
+ */
+ public void loadUrl(final String url, final int time) {
+ System.out.println("loadUrl("+url+","+time+")");
+ final DroidGap me = this;
+
+ // Handle activity parameters
+ this.runOnUiThread(new Runnable() {
+ public void run() {
+ me.handleActivityParameters();
+ }
+ });
+
+ Runnable runnable = new Runnable() {
+ public void run() {
+ try {
+ synchronized(this) {
+ this.wait(time);
+ }
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (!me.cancelLoadUrl) {
+ me.loadUrl(url);
+ }
+ else{
+ me.cancelLoadUrl = false;
+ System.out.println("Aborting loadUrl("+url+"): Another URL was loaded before timer expired.");
+ }
+ }
+ };
+ Thread thread = new Thread(runnable);
+ thread.start();
+ }
+
+ /**
+ * Cancel loadUrl before it has been loaded.
+ */
+ public void cancelLoadUrl() {
+ this.cancelLoadUrl = true;
+ }
+
+ /**
+ * Clear the resource cache.
+ */
+ public void clearCache() {
+ if (this.appView == null) {
+ this.init();
+ }
+ this.appView.clearCache(true);
+ }
+
+ /**
+ * Clear web history in this web view.
+ */
+ public void clearHistory() {
+ this.clearHistory = true;
+ if (this.appView != null) {
+ this.appView.clearHistory();
+ }
+ }
+
+ @Override
/**
* Called by the system when the device configuration changes while your activity is running.
*
* @param Configuration newConfig
*/
public void onConfigurationChanged(Configuration newConfig) {
- //don't reload the current page when the orientation is changed
- super.onConfigurationChanged(newConfig);
- }
+ //don't reload the current page when the orientation is changed
+ super.onConfigurationChanged(newConfig);
+ }
+
+ /**
+ * Get boolean property for activity.
+ *
+ * @param name
+ * @param defaultValue
+ * @return
+ */
+ public boolean getBooleanProperty(String name, boolean defaultValue) {
+ Bundle bundle = this.getIntent().getExtras();
+ if (bundle == null) {
+ return defaultValue;
+ }
+ Boolean p = (Boolean)bundle.get(name);
+ if (p == null) {
+ return defaultValue;
+ }
+ return p.booleanValue();
+ }
+
+ /**
+ * Get int property for activity.
+ *
+ * @param name
+ * @param defaultValue
+ * @return
+ */
+ public int getIntegerProperty(String name, int defaultValue) {
+ Bundle bundle = this.getIntent().getExtras();
+ if (bundle == null) {
+ return defaultValue;
+ }
+ Integer p = (Integer)bundle.get(name);
+ if (p == null) {
+ return defaultValue;
+ }
+ return p.intValue();
+ }
+
+ /**
+ * Get string property for activity.
+ *
+ * @param name
+ * @param defaultValue
+ * @return
+ */
+ public String getStringProperty(String name, String defaultValue) {
+ Bundle bundle = this.getIntent().getExtras();
+ if (bundle == null) {
+ return defaultValue;
+ }
+ String p = bundle.getString(name);
+ if (p == null) {
+ return defaultValue;
+ }
+ return p;
+ }
+
+ /**
+ * Get double property for activity.
+ *
+ * @param name
+ * @param defaultValue
+ * @return
+ */
+ public double getDoubleProperty(String name, double defaultValue) {
+ Bundle bundle = this.getIntent().getExtras();
+ if (bundle == null) {
+ return defaultValue;
+ }
+ Double p = (Double)bundle.get(name);
+ if (p == null) {
+ return defaultValue;
+ }
+ return p.doubleValue();
+ }
+
+ /**
+ * Set boolean property on activity.
+ *
+ * @param name
+ * @param value
+ */
+ public void setBooleanProperty(String name, boolean value) {
+ this.getIntent().putExtra(name, value);
+ }
+
+ /**
+ * Set int property on activity.
+ *
+ * @param name
+ * @param value
+ */
+ public void setIntegerProperty(String name, int value) {
+ this.getIntent().putExtra(name, value);
+ }
+
+ /**
+ * Set string property on activity.
+ *
+ * @param name
+ * @param value
+ */
+ public void setStringProperty(String name, String value) {
+ this.getIntent().putExtra(name, value);
+ }
+
+ /**
+ * Set double property on activity.
+ *
+ * @param name
+ * @param value
+ */
+ public void setDoubleProperty(String name, double value) {
+ this.getIntent().putExtra(name, value);
+ }
@Override
/**
@@ -171,15 +599,18 @@ public class DroidGap extends Activity {
*/
protected void onPause() {
super.onPause();
+ // Send pause event to JavaScript
+ this.appView.loadUrl("javascript:try{PhoneGap.onPause.fire();}catch(e){};");
- // Forward to plugins
- this.pluginManager.onPause();
-
- // Send pause event to JavaScript
- this.appView.loadUrl("javascript:try{PhoneGap.onPause.fire();}catch(e){};");
-
- // Pause JavaScript timers (including setInterval)
- this.appView.pauseTimers();
+ // If app doesn't want to run in background
+ if (!this.keepRunning) {
+
+ // Forward to plugins
+ this.pluginManager.onPause();
+
+ // Pause JavaScript timers (including setInterval)
+ this.appView.pauseTimers();
+ }
}
@Override
@@ -189,14 +620,24 @@ public class DroidGap extends Activity {
protected void onResume() {
super.onResume();
- // Forward to plugins
- this.pluginManager.onResume();
-
- // Send resume event to JavaScript
- this.appView.loadUrl("javascript:try{PhoneGap.onResume.fire();}catch(e){};");
-
- // Resume JavaScript timers (including setInterval)
- this.appView.resumeTimers();
+ // Send resume event to JavaScript
+ this.appView.loadUrl("javascript:try{PhoneGap.onResume.fire();}catch(e){};");
+
+ // If app doesn't want to run in background
+ if (!this.keepRunning || this.activityResultKeepRunning) {
+
+ // Restore multitasking state
+ if (this.activityResultKeepRunning) {
+ this.keepRunning = this.activityResultKeepRunning;
+ this.activityResultKeepRunning = false;
+ }
+
+ // Forward to plugins
+ this.pluginManager.onResume();
+
+ // Resume JavaScript timers (including setInterval)
+ this.appView.resumeTimers();
+ }
}
@Override
@@ -212,10 +653,6 @@ public class DroidGap extends Activity {
// Load blank page so that JavaScript onunload is called
this.appView.loadUrl("about:blank");
- // Clean up objects
- if (this.mKey != null) {
- }
-
// Forward to plugins
this.pluginManager.onDestroy();
@@ -233,67 +670,10 @@ public class DroidGap extends Activity {
public void addService(String serviceType, String className) {
this.pluginManager.addService(serviceType, className);
}
-
- /**
- * Bind PhoneGap objects to JavaScript.
- *
- * @param appView
- */
- private void bindBrowser(WebView appView) {
- this.callbackServer = new CallbackServer();
- this.pluginManager = new PluginManager(appView, this);
- this.mKey = new BrowserKey(appView, this);
-
- // This creates the new javascript interfaces for PhoneGap
- appView.addJavascriptInterface(this.pluginManager, "PluginManager");
-
- appView.addJavascriptInterface(this.mKey, "BackButton");
-
- appView.addJavascriptInterface(this.callbackServer, "CallbackServer");
- appView.addJavascriptInterface(new SplashScreen(this), "SplashScreen");
-
-
- this.addService("Geolocation", "com.phonegap.GeoBroker");
- this.addService("Device", "com.phonegap.Device");
- this.addService("Accelerometer", "com.phonegap.AccelListener");
- this.addService("Compass", "com.phonegap.CompassListener");
- this.addService("Media", "com.phonegap.AudioHandler");
- this.addService("Camera", "com.phonegap.CameraLauncher");
- this.addService("Contacts", "com.phonegap.ContactManager");
- this.addService("Crypto", "com.phonegap.CryptoHandler");
- this.addService("File", "com.phonegap.FileUtils");
- this.addService("Location", "com.phonegap.GeoBroker"); // Always add Location, even though it is built-in on 2.x devices. Let JavaScript decide which one to use.
- this.addService("Network Status", "com.phonegap.NetworkManager");
- this.addService("Notification", "com.phonegap.Notification");
- this.addService("Storage", "com.phonegap.Storage");
- this.addService("Temperature", "com.phonegap.TempListener");
-
- }
-
- /**
- * Load the url into the webview.
- *
- * @param url
- */
- public void loadUrl(final String url) {
- this.url = url;
- int i = url.lastIndexOf('/');
- if (i > 0) {
- this.baseUrl = url.substring(0, i);
- }
- else {
- this.baseUrl = this.url;
- }
-
- this.runOnUiThread(new Runnable() {
- public void run() {
- DroidGap.this.appView.loadUrl(url);
- }
- });
- }
/**
* Send JavaScript statement back to JavaScript.
+ * (This is a convenience method)
*
* @param message
*/
@@ -301,15 +681,6 @@ public class DroidGap extends Activity {
this.callbackServer.sendJavascript(statement);
}
- /**
- * Get the port that the callback server is listening on.
- *
- * @return
- */
- public int getPort() {
- return this.callbackServer.getPort();
- }
-
/**
* Provides a hook for calling "alert" from javascript. Useful for
* debugging your javascript.
@@ -337,7 +708,6 @@ public class DroidGap extends Activity {
*/
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
- Log.d(LOG_TAG, message);
AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx);
dlg.setMessage(message);
dlg.setTitle("Alert");
@@ -384,73 +754,136 @@ public class DroidGap extends Activity {
return true;
}
+ /**
+ * Tell the client to display a prompt dialog to the user.
+ * If the client returns true, WebView will assume that the client will
+ * handle the prompt dialog and call the appropriate JsPromptResult method.
+ *
+ * @param view
+ * @param url
+ * @param message
+ * @param defaultValue
+ * @param result
+ */
+ @Override
+ public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
+
+ // Calling PluginManager.exec() to call a native service using
+ // prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true]));
+ if (defaultValue.substring(0, 4).equals("gap:")) {
+ JSONArray array;
+ try {
+ array = new JSONArray(defaultValue.substring(4));
+ String service = array.getString(0);
+ String action = array.getString(1);
+ String callbackId = array.getString(2);
+ boolean async = array.getBoolean(3);
+ String r = pluginManager.exec(service, action, callbackId, message, async);
+ result.confirm(r);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // Polling for JavaScript messages
+ else if (defaultValue.equals("gap_poll:")) {
+ String r = callbackServer.getJavascript();
+ result.confirm(r);
+ }
+
+ // Calling into CallbackServer
+ else if (defaultValue.equals("gap_callbackServer:")) {
+ String r = "";
+ if (message.equals("usePolling")) {
+ r = ""+callbackServer.usePolling();
+ }
+ else if (message.equals("restartServer")) {
+ callbackServer.restartServer();
+ }
+ else if (message.equals("getPort")) {
+ r = Integer.toString(callbackServer.getPort());
+ }
+ else if (message.equals("getToken")) {
+ r = callbackServer.getToken();
+ }
+ result.confirm(r);
+ }
+
+ // Show dialog
+ else {
+ //@TODO:
+ result.confirm("");
+ }
+ return true;
+ }
+
}
/**
* WebChromeClient that extends GapClient with additional support for Android 2.X
*/
public final class EclairClient extends GapClient {
-
- private String TAG = "PhoneGapLog";
- private long MAX_QUOTA = 100 * 1024 * 1024;
- /**
- * Constructor.
- *
- * @param ctx
- */
- public EclairClient(Context ctx) {
- super(ctx);
- }
+ private String TAG = "PhoneGapLog";
+ private long MAX_QUOTA = 100 * 1024 * 1024;
+
+ /**
+ * Constructor.
+ *
+ * @param ctx
+ */
+ public EclairClient(Context ctx) {
+ super(ctx);
+ }
+
+ /**
+ * Handle database quota exceeded notification.
+ *
+ * @param url
+ * @param databaseIdentifier
+ * @param currentQuota
+ * @param estimatedSize
+ * @param totalUsedQuota
+ * @param quotaUpdater
+ */
+ @Override
+ public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize,
+ long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)
+ {
+ Log.d(TAG, "event raised onExceededDatabaseQuota estimatedSize: " + Long.toString(estimatedSize) + " currentQuota: " + Long.toString(currentQuota) + " totalUsedQuota: " + Long.toString(totalUsedQuota));
+
+ if( estimatedSize < MAX_QUOTA)
+ {
+ //increase for 1Mb
+ long newQuota = estimatedSize;
+ Log.d(TAG, "calling quotaUpdater.updateQuota newQuota: " + Long.toString(newQuota) );
+ quotaUpdater.updateQuota(newQuota);
+ }
+ else
+ {
+ // Set the quota to whatever it is and force an error
+ // TODO: get docs on how to handle this properly
+ quotaUpdater.updateQuota(currentQuota);
+ }
+ }
+
+ // console.log in api level 7: http://developer.android.com/guide/developing/debug-tasks.html
+ @Override
+ public void onConsoleMessage(String message, int lineNumber, String sourceID)
+ {
+ // This is a kludgy hack!!!!
+ Log.d(TAG, sourceID + ": Line " + Integer.toString(lineNumber) + " : " + message);
+ }
+
+ @Override
+ public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) {
+ // TODO Auto-generated method stub
+ super.onGeolocationPermissionsShowPrompt(origin, callback);
+ callback.invoke(origin, true, false);
+ }
+
+ }
- /**
- * Handle database quota exceeded notification.
- *
- * @param url
- * @param databaseIdentifier
- * @param currentQuota
- * @param estimatedSize
- * @param totalUsedQuota
- * @param quotaUpdater
- */
- @Override
- public void onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize,
- long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)
- {
- Log.d(TAG, "event raised onExceededDatabaseQuota estimatedSize: " + Long.toString(estimatedSize) + " currentQuota: " + Long.toString(currentQuota) + " totalUsedQuota: " + Long.toString(totalUsedQuota));
-
- if( estimatedSize < MAX_QUOTA)
- {
- //increase for 1Mb
- long newQuota = estimatedSize;
- Log.d(TAG, "calling quotaUpdater.updateQuota newQuota: " + Long.toString(newQuota) );
- quotaUpdater.updateQuota(newQuota);
- }
- else
- {
- // Set the quota to whatever it is and force an error
- // TODO: get docs on how to handle this properly
- quotaUpdater.updateQuota(currentQuota);
- }
- }
-
- // console.log in api level 7: http://developer.android.com/guide/developing/debug-tasks.html
- @Override
- public void onConsoleMessage(String message, int lineNumber, String sourceID)
- {
- // This is a kludgy hack!!!!
- Log.d(TAG, sourceID + ": Line " + Integer.toString(lineNumber) + " : " + message);
- }
-
- @Override
- public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) {
- // TODO Auto-generated method stub
- super.onGeolocationPermissionsShowPrompt(origin, callback);
- callback.invoke(origin, true, false);
- }
-
- }
-
/**
* The webview client receives notifications about appView
*/
@@ -477,7 +910,6 @@ public class DroidGap extends Activity {
*/
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
-
// If dialing phone (tel:5551212)
if (url.startsWith(WebView.SCHEME_TEL)) {
try {
@@ -491,7 +923,7 @@ public class DroidGap extends Activity {
}
// If displaying map (geo:0,0?q=address)
- else if (url.startsWith(WebView.SCHEME_GEO)) {
+ else if (url.startsWith("geo:")) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
@@ -528,8 +960,8 @@ public class DroidGap extends Activity {
return true;
}
- // If http, https or file
- else if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) {
+ // All else
+ else {
int i = url.lastIndexOf('/');
String newBaseUrl = url;
@@ -538,6 +970,8 @@ public class DroidGap extends Activity {
}
// If our app or file:, then load into our webview
+ // NOTE: This replaces our app with new URL. When BACK is pressed,
+ // our app is reloaded and restarted. All state is lost.
if (this.ctx.loadInWebView || url.startsWith("file://") || this.ctx.baseUrl.equals(newBaseUrl)) {
this.ctx.appView.loadUrl(url);
}
@@ -554,8 +988,6 @@ public class DroidGap extends Activity {
}
return true;
}
-
- return false;
}
/**
@@ -565,45 +997,102 @@ public class DroidGap extends Activity {
* @param url The url of the page.
*/
@Override
- public void onPageFinished (WebView view, String url) {
+ public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
- // Try firing the onNativeReady event in JS. If it fails because the JS is
- // not loaded yet then just set a flag so that the onNativeReady can be fired
- // from the JS side when the JS gets to that code.
- appView.loadUrl("javascript:try{ PhoneGap.onNativeReady.fire();}catch(e){_nativeReady = true;}");
- }
- }
- public boolean onKeyDown(int keyCode, KeyEvent event)
- {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- if (mKey.isBound())
- {
- //We fire an event here!
- appView.loadUrl("javascript:document.keyEvent.backTrigger()");
- }
- else
- {
- // only go back if the webview tells you that it is possible to go back
- if(appView.canGoBack())
- {
- appView.goBack();
- }
- else // if you can't go back, invoke behavior of super class
- {
- return super.onKeyDown(keyCode, event);
- }
+ // Clear timeout flag
+ this.ctx.loadUrlTimeout++;
+
+ // Try firing the onNativeReady event in JS. If it fails because the JS is
+ // not loaded yet then just set a flag so that the onNativeReady can be fired
+ // from the JS side when the JS gets to that code.
+ appView.loadUrl("javascript:try{ PhoneGap.onNativeReady.fire();}catch(e){_nativeReady = true;}");
+
+ // Make app view visible
+ appView.setVisibility(View.VISIBLE);
+
+ // Stop "app loading" spinner if showing
+ if (this.ctx.hideLoadingDialogOnPageLoad) {
+ this.ctx.hideLoadingDialogOnPageLoad = false;
+ this.ctx.pluginManager.exec("Notification", "activityStop", null, "[]", false);
}
+
+ // Clear history, so that previous screen isn't there when Back button is pressed
+ if (this.ctx.clearHistory) {
+ this.ctx.clearHistory = false;
+ this.ctx.appView.clearHistory();
+ }
}
- if (keyCode == KeyEvent.KEYCODE_MENU)
- {
- // This is where we launch the menu
- appView.loadUrl("javascript:keyEvent.menuTrigger()");
+ /**
+ * Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable).
+ * The errorCode parameter corresponds to one of the ERROR_* constants.
+ *
+ * @param view The WebView that is initiating the callback.
+ * @param errorCode The error code corresponding to an ERROR_* value.
+ * @param description A String describing the error.
+ * @param failingUrl The url that failed to load.
+ */
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
+ System.out.println("onReceivedError: Error code="+errorCode+" Description="+description+" URL="+failingUrl);
+
+ // Clear timeout flag
+ this.ctx.loadUrlTimeout++;
+
+ // Stop "app loading" spinner if showing
+ this.ctx.pluginManager.exec("Notification", "activityStop", null, "[]", false);
+
+ // Handle error
+ this.ctx.onReceivedError(errorCode, description, failingUrl);
}
- return false;
}
-
+
+ /**
+ * Called when a key is pressed.
+ *
+ * @param keyCode
+ * @param event
+ */
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+
+ // If back key
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+
+ // If back key is bound, then send event to JavaScript
+ if (this.bound) {
+ this.appView.loadUrl("javascript:PhoneGap.fireEvent('backbutton');");
+ }
+
+ // If not bound
+ else {
+
+ // Go to previous page in webview if it is possible to go back
+ if (this.appView.canGoBack()) {
+ this.appView.goBack();
+ }
+
+ // If not, then invoke behavior of super class
+ else {
+ return super.onKeyDown(keyCode, event);
+ }
+ }
+ }
+
+ // If menu key
+ else if (keyCode == KeyEvent.KEYCODE_MENU) {
+ this.appView.loadUrl("javascript:PhoneGap.fireEvent('menubutton');");
+ }
+
+ // If search key
+ else if (keyCode == KeyEvent.KEYCODE_SEARCH) {
+ this.appView.loadUrl("javascript:PhoneGap.fireEvent('searchbutton');");
+ }
+
+ return false;
+ }
+
/**
* Any calls to Activity.startActivityForResult must use method below, so
* the result can be routed to them correctly.
@@ -636,6 +1125,14 @@ public class DroidGap extends Activity {
*/
public void startActivityForResult(Plugin command, Intent intent, int requestCode) {
this.activityResultCallback = command;
+ this.activityResultKeepRunning = this.keepRunning;
+
+ // If multitasking turned on, then disable it for activities that return results
+ if (command != null) {
+ this.keepRunning = false;
+ }
+
+ // Start activity
super.startActivityForResult(intent, requestCode);
}
@@ -649,11 +1146,72 @@ public class DroidGap extends Activity {
* @param resultCode The integer result code returned by the child activity through its setResult().
* @param data An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
*/
- protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
- super.onActivityResult(requestCode, resultCode, intent);
- Plugin callback = this.activityResultCallback;
- if (callback != null) {
- callback.onActivityResult(requestCode, resultCode, intent);
- }
- }
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ super.onActivityResult(requestCode, resultCode, intent);
+ Plugin callback = this.activityResultCallback;
+ if (callback != null) {
+ callback.onActivityResult(requestCode, resultCode, intent);
+ }
+ }
+
+ /**
+ * Report an error to the host application. These errors are unrecoverable (i.e. the main resource is unavailable).
+ * The errorCode parameter corresponds to one of the ERROR_* constants.
+ *
+ * @param errorCode The error code corresponding to an ERROR_* value.
+ * @param description A String describing the error.
+ * @param failingUrl The url that failed to load.
+ */
+ public void onReceivedError(int errorCode, String description, String failingUrl) {
+ final DroidGap me = this;
+
+ // If errorUrl specified, then load it
+ final String errorUrl = me.getStringProperty("errorUrl", null);
+ if ((errorUrl != null) && errorUrl.startsWith("file://") && (!failingUrl.equals(errorUrl))) {
+
+ // Load URL on UI thread
+ me.runOnUiThread(new Runnable() {
+ public void run() {
+ me.appView.loadUrl(errorUrl);
+ }
+ });
+ }
+
+ // If not, then display error dialog
+ else {
+ me.appView.loadUrl("about:blank");
+ me.displayError("Application Error", description + " ("+failingUrl+")", "OK", true);
+ }
+ }
+
+ /**
+ * Display an error dialog and optionally exit application.
+ *
+ * @param title
+ * @param message
+ * @param button
+ * @param exit
+ */
+ public void displayError(final String title, final String message, final String button, final boolean exit) {
+ final DroidGap me = this;
+ me.runOnUiThread(new Runnable() {
+ public void run() {
+ AlertDialog.Builder dlg = new AlertDialog.Builder(me);
+ dlg.setMessage(message);
+ dlg.setTitle(title);
+ dlg.setCancelable(false);
+ dlg.setPositiveButton(button,
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ if (exit) {
+ me.finish();
+ }
+ }
+ });
+ dlg.create();
+ dlg.show();
+ }
+ });
+ }
}
diff --git a/framework/src/com/phonegap/FileTransfer.java b/framework/src/com/phonegap/FileTransfer.java
new file mode 100644
index 00000000..24b181fb
--- /dev/null
+++ b/framework/src/com/phonegap/FileTransfer.java
@@ -0,0 +1,359 @@
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+package com.phonegap;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Iterator;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.net.Uri;
+import android.util.Log;
+import android.webkit.CookieManager;
+
+import com.phonegap.api.Plugin;
+import com.phonegap.api.PluginResult;
+
+public class FileTransfer extends Plugin {
+
+ private static final String LOG_TAG = "FileUploader";
+ private static final String LINE_START = "--";
+ private static final String LINE_END = "\r\n";
+ private static final String BOUNDRY = "*****";
+
+ public static int FILE_NOT_FOUND_ERR = 1;
+ public static int INVALID_URL_ERR = 2;
+ public static int CONNECTION_ERR = 3;
+
+ private SSLSocketFactory defaultSSLSocketFactory = null;
+ private HostnameVerifier defaultHostnameVerifier = null;
+
+ /* (non-Javadoc)
+ * @see com.phonegap.api.Plugin#execute(java.lang.String, org.json.JSONArray, java.lang.String)
+ */
+ @Override
+ public PluginResult execute(String action, JSONArray args, String callbackId) {
+ String file = null;
+ String server = null;
+ try {
+ file = args.getString(0);
+ server = args.getString(1);
+ }
+ catch (JSONException e) {
+ Log.d(LOG_TAG, "Missing filename or server name");
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION, "Missing filename or server name");
+ }
+
+ // Setup the options
+ String fileKey = null;
+ String fileName = null;
+ String mimeType = null;
+
+ fileKey = getArgument(args, 2, "file");
+ fileName = getArgument(args, 3, "image.jpg");
+ mimeType = getArgument(args, 4, "image/jpeg");
+
+ try {
+ JSONObject params = args.optJSONObject(5);
+ boolean trustEveryone = args.optBoolean(6);
+
+ if (action.equals("upload")) {
+ FileUploadResult r = upload(file, server, fileKey, fileName, mimeType, params, trustEveryone);
+ Log.d(LOG_TAG, "****** About to return a result from upload");
+ return new PluginResult(PluginResult.Status.OK, r.toJSONObject());
+ } else {
+ return new PluginResult(PluginResult.Status.INVALID_ACTION);
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ JSONObject error = createFileUploadError(FILE_NOT_FOUND_ERR);
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
+ } catch (IllegalArgumentException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ JSONObject error = createFileUploadError(INVALID_URL_ERR);
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
+ } catch (SSLException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ Log.d(LOG_TAG, "Got my ssl exception!!!");
+ JSONObject error = createFileUploadError(CONNECTION_ERR);
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ JSONObject error = createFileUploadError(CONNECTION_ERR);
+ return new PluginResult(PluginResult.Status.IO_EXCEPTION, error);
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
+ }
+ }
+
+ // always verify the host - don't check for certificate
+ final static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ };
+
+ /**
+ * This function will install a trust manager that will blindly trust all SSL
+ * certificates. The reason this code is being added is to enable developers
+ * to do development using self signed SSL certificates on their web server.
+ *
+ * The standard HttpsURLConnection class will throw an exception on self
+ * signed certificates if this code is not run.
+ */
+ private void trustAllHosts() {
+ // Create a trust manager that does not validate certificate chains
+ TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
+ public java.security.cert.X509Certificate[] getAcceptedIssuers() {
+ return new java.security.cert.X509Certificate[] {};
+ }
+
+ public void checkClientTrusted(X509Certificate[] chain,
+ String authType) throws CertificateException {
+ }
+
+ public void checkServerTrusted(X509Certificate[] chain,
+ String authType) throws CertificateException {
+ }
+ } };
+
+ // Install the all-trusting trust manager
+ try {
+ // Backup the current SSL socket factory
+ defaultSSLSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
+ // Install our all trusting manager
+ SSLContext sc = SSLContext.getInstance("TLS");
+ sc.init(null, trustAllCerts, new java.security.SecureRandom());
+ HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
+ } catch (Exception e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Create an error object based on the passed in errorCode
+ * @param errorCode the error
+ * @return JSONObject containing the error
+ */
+ private JSONObject createFileUploadError(int errorCode) {
+ JSONObject error = null;
+ try {
+ error = new JSONObject();
+ error.put("code", errorCode);
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+ return error;
+ }
+
+ /**
+ * Convenience method to read a parameter from the list of JSON args.
+ * @param args the args passed to the Plugin
+ * @param position the position to retrieve the arg from
+ * @param defaultString the default to be used if the arg does not exist
+ * @return String with the retrieved value
+ */
+ private String getArgument(JSONArray args, int position, String defaultString) {
+ String arg = defaultString;
+ if(args.length() >= position) {
+ arg = args.optString(position);
+ if (arg == null || "null".equals(arg)) {
+ arg = defaultString;
+ }
+ }
+ return arg;
+ }
+
+ /**
+ * Uploads the specified file to the server URL provided using an HTTP
+ * multipart request.
+ * @param file Full path of the file on the file system
+ * @param server URL of the server to receive the file
+ * @param fileKey Name of file request parameter
+ * @param fileName File name to be used on server
+ * @param mimeType Describes file content type
+ * @param params key:value pairs of user-defined parameters
+ * @return FileUploadResult containing result of upload request
+ */
+ public FileUploadResult upload(String file, String server, final String fileKey, final String fileName,
+ final String mimeType, JSONObject params, boolean trustEveryone) throws IOException, SSLException {
+ // Create return object
+ FileUploadResult result = new FileUploadResult();
+
+ // Get a input stream of the file on the phone
+ InputStream fileInputStream = getPathFromUri(file);
+
+ HttpURLConnection conn = null;
+ DataOutputStream dos = null;
+
+ int bytesRead, bytesAvailable, bufferSize;
+ long totalBytes;
+ byte[] buffer;
+ int maxBufferSize = 8096;
+
+ //------------------ CLIENT REQUEST
+ // open a URL connection to the server
+ URL url = new URL(server);
+
+ // Open a HTTP connection to the URL based on protocol
+ if (url.getProtocol().toLowerCase().equals("https")) {
+ // Using standard HTTPS connection. Will not allow self signed certificate
+ if (!trustEveryone) {
+ conn = (HttpsURLConnection) url.openConnection();
+ }
+ // Use our HTTPS connection that blindly trusts everyone.
+ // This should only be used in debug environments
+ else {
+ // Setup the HTTPS connection class to trust everyone
+ trustAllHosts();
+ HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
+ // Save the current hostnameVerifier
+ defaultHostnameVerifier = https.getHostnameVerifier();
+ // Setup the connection not to verify hostnames
+ https.setHostnameVerifier(DO_NOT_VERIFY);
+ conn = https;
+ }
+ }
+ // Return a standard HTTP conneciton
+ else {
+ conn = (HttpURLConnection) url.openConnection();
+ }
+
+ // Allow Inputs
+ conn.setDoInput(true);
+
+ // Allow Outputs
+ conn.setDoOutput(true);
+
+ // Don't use a cached copy.
+ conn.setUseCaches(false);
+
+ // Use a post method.
+ conn.setRequestMethod("POST");
+ conn.setRequestProperty("Connection", "Keep-Alive");
+ conn.setRequestProperty("Content-Type", "multipart/form-data;boundary="+BOUNDRY);
+
+ // Set the cookies on the response
+ String cookie = CookieManager.getInstance().getCookie(server);
+ if (cookie != null) {
+ conn.setRequestProperty("Cookie", cookie);
+ }
+
+ dos = new DataOutputStream( conn.getOutputStream() );
+
+ // Send any extra parameters
+ try {
+ for (Iterator iter = params.keys(); iter.hasNext();) {
+ Object key = iter.next();
+ dos.writeBytes(LINE_START + BOUNDRY + LINE_END);
+ dos.writeBytes("Content-Disposition: form-data; name=\"" + key.toString() + "\"; ");
+ dos.writeBytes(LINE_END + LINE_END);
+ dos.writeBytes(params.getString(key.toString()));
+ dos.writeBytes(LINE_END);
+ }
+ } catch (JSONException e) {
+ Log.e(LOG_TAG, e.getMessage(), e);
+ }
+
+ dos.writeBytes(LINE_START + BOUNDRY + LINE_END);
+ dos.writeBytes("Content-Disposition: form-data; name=\"" + fileKey + "\";" + " filename=\"" + fileName +"\"" + LINE_END);
+ dos.writeBytes("Content-Type: " + mimeType + LINE_END);
+ dos.writeBytes(LINE_END);
+
+ // create a buffer of maximum size
+ bytesAvailable = fileInputStream.available();
+ bufferSize = Math.min(bytesAvailable, maxBufferSize);
+ buffer = new byte[bufferSize];
+
+ // read file and write it into form...
+ bytesRead = fileInputStream.read(buffer, 0, bufferSize);
+ totalBytes = 0;
+
+ while (bytesRead > 0) {
+ totalBytes += bytesRead;
+ result.setBytesSent(totalBytes);
+ dos.write(buffer, 0, bufferSize);
+ bytesAvailable = fileInputStream.available();
+ bufferSize = Math.min(bytesAvailable, maxBufferSize);
+ bytesRead = fileInputStream.read(buffer, 0, bufferSize);
+ }
+
+ // send multipart form data necesssary after file data...
+ dos.writeBytes(LINE_END);
+ dos.writeBytes(LINE_START + BOUNDRY + LINE_START + LINE_END);
+
+ // close streams
+ fileInputStream.close();
+ dos.flush();
+ dos.close();
+
+ //------------------ read the SERVER RESPONSE
+ StringBuffer responseString = new StringBuffer("");
+ DataInputStream inStream = new DataInputStream ( conn.getInputStream() );
+ String line;
+ while (( line = inStream.readLine()) != null) {
+ responseString.append(line);
+ }
+ Log.d(LOG_TAG, "got response from server");
+ Log.d(LOG_TAG, responseString.toString());
+
+ // send request and retrieve response
+ result.setResponseCode(conn.getResponseCode());
+ result.setResponse(responseString.toString());
+
+ inStream.close();
+ conn.disconnect();
+
+ // Revert back to the proper verifier and socket factories
+ if (trustEveryone && url.getProtocol().toLowerCase().equals("https")) {
+ ((HttpsURLConnection)conn).setHostnameVerifier(defaultHostnameVerifier);
+ HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
+ }
+
+ return result;
+ }
+
+ /**
+ * Get an input stream based on file path or content:// uri
+ *
+ * @param path
+ * @return an input stream
+ * @throws FileNotFoundException
+ */
+ private InputStream getPathFromUri(String path) throws FileNotFoundException {
+ if (path.startsWith("content:")) {
+ Uri uri = Uri.parse(path);
+ return ctx.getContentResolver().openInputStream(uri);
+ }
+ else {
+ return new FileInputStream(path);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/framework/src/com/phonegap/FileUploadResult.java b/framework/src/com/phonegap/FileUploadResult.java
new file mode 100644
index 00000000..151576bb
--- /dev/null
+++ b/framework/src/com/phonegap/FileUploadResult.java
@@ -0,0 +1,52 @@
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi
+ * Copyright (c) 2010, IBM Corporation
+ */
+package com.phonegap;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Encapsulates the result and/or status of uploading a file to a remote server.
+ */
+public class FileUploadResult {
+
+ private long bytesSent = 0; // bytes sent
+ private int responseCode = -1; // HTTP response code
+ private String response = null; // HTTP response
+
+ public long getBytesSent() {
+ return bytesSent;
+ }
+
+ public void setBytesSent(long bytes) {
+ this.bytesSent = bytes;
+ }
+
+ public int getResponseCode() {
+ return responseCode;
+ }
+
+ public void setResponseCode(int responseCode) {
+ this.responseCode = responseCode;
+ }
+
+ public String getResponse() {
+ return response;
+ }
+
+ public void setResponse(String response) {
+ this.response = response;
+ }
+
+ public JSONObject toJSONObject() throws JSONException {
+ return new JSONObject(
+ "{bytesSent:" + bytesSent +
+ ",responseCode:" + responseCode +
+ ",response:" + JSONObject.quote(response) + "}");
+ }
+}
diff --git a/framework/src/com/phonegap/FileUtils.java b/framework/src/com/phonegap/FileUtils.java
index 217ef27f..b88fc962 100755
--- a/framework/src/com/phonegap/FileUtils.java
+++ b/framework/src/com/phonegap/FileUtils.java
@@ -8,27 +8,54 @@
package com.phonegap;
import java.io.*;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.channels.FileChannel;
import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.net.Uri;
+import android.os.Environment;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
+import com.phonegap.file.EncodingException;
+import com.phonegap.file.FileExistsException;
+import com.phonegap.file.InvalidModificationException;
+import com.phonegap.file.NoModificationAllowedException;
+import com.phonegap.file.TypeMismatchException;
/**
* This class provides SD card file and directory services to JavaScript.
* Only files on the SD card can be accessed.
*/
public class FileUtils extends Plugin {
+ private static final String LOG_TAG = "FileUtils";
+
+ public static int NOT_FOUND_ERR = 1;
+ public static int SECURITY_ERR = 2;
+ public static int ABORT_ERR = 3;
- public static int NOT_FOUND_ERR = 8;
- public static int SECURITY_ERR = 18;
- public static int ABORT_ERR = 20;
-
- public static int NOT_READABLE_ERR = 24;
- public static int ENCODING_ERR = 26;
+ public static int NOT_READABLE_ERR = 4;
+ public static int ENCODING_ERR = 5;
+ public static int NO_MODIFICATION_ALLOWED_ERR = 6;
+ public static int INVALID_STATE_ERR = 7;
+ public static int SYNTAX_ERR = 8;
+ public static int INVALID_MODIFICATION_ERR = 9;
+ public static int QUOTA_EXCEEDED_ERR = 10;
+ public static int TYPE_MISMATCH_ERR = 11;
+ public static int PATH_EXISTS_ERR = 12;
+ public static int TEMPORARY = 0;
+ public static int PERSISTENT = 1;
+ public static int RESOURCE = 2;
+ public static int APPLICATION = 3;
+
FileReader f_in;
FileWriter f_out;
@@ -36,7 +63,6 @@ public class FileUtils extends Plugin {
* Constructor.
*/
public FileUtils() {
- System.out.println("FileUtils()");
}
/**
@@ -53,83 +79,763 @@ public class FileUtils extends Plugin {
//System.out.println("FileUtils.execute("+action+")");
try {
- if (action.equals("testSaveLocationExists")) {
- boolean b = DirectoryManager.testSaveLocationExists();
- return new PluginResult(status, b);
- }
- else if (action.equals("getFreeDiskSpace")) {
- long l = DirectoryManager.getFreeDiskSpace();
- return new PluginResult(status, l);
- }
- else if (action.equals("testFileExists")) {
- boolean b = DirectoryManager.testFileExists(args.getString(0));
- return new PluginResult(status, b);
- }
- else if (action.equals("testDirectoryExists")) {
- boolean b = DirectoryManager.testFileExists(args.getString(0));
- return new PluginResult(status, b);
- }
- else if (action.equals("deleteDirectory")) {
- boolean b = DirectoryManager.deleteDirectory(args.getString(0));
- return new PluginResult(status, b);
- }
- else if (action.equals("deleteFile")) {
- boolean b = DirectoryManager.deleteFile(args.getString(0));
- return new PluginResult(status, b);
- }
- else if (action.equals("createDirectory")) {
- boolean b = DirectoryManager.createDirectory(args.getString(0));
- return new PluginResult(status, b);
- }
- else if (action.equals("readAsText")) {
- try {
- String s = this.readAsText(args.getString(0), args.getString(1));
- return new PluginResult(status, s);
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_FOUND_ERR);
- } catch (IOException e) {
- e.printStackTrace();
- return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR);
+ try {
+ if (action.equals("testSaveLocationExists")) {
+ boolean b = DirectoryManager.testSaveLocationExists();
+ return new PluginResult(status, b);
+ }
+ else if (action.equals("getFreeDiskSpace")) {
+ long l = DirectoryManager.getFreeDiskSpace();
+ return new PluginResult(status, l);
+ }
+ else if (action.equals("testFileExists")) {
+ boolean b = DirectoryManager.testFileExists(args.getString(0));
+ return new PluginResult(status, b);
+ }
+ else if (action.equals("testDirectoryExists")) {
+ boolean b = DirectoryManager.testFileExists(args.getString(0));
+ return new PluginResult(status, b);
+ }
+ else if (action.equals("readAsText")) {
+ try {
+ String s = this.readAsText(args.getString(0), args.getString(1));
+ return new PluginResult(status, s);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR);
+ }
+ }
+ else if (action.equals("readAsDataURL")) {
+ try {
+ String s = this.readAsDataURL(args.getString(0));
+ return new PluginResult(status, s);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR);
+ }
+ }
+ else if (action.equals("writeAsText")) {
+ try {
+ this.writeAsText(args.getString(0), args.getString(1), args.getBoolean(2));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR);
+ }
+ }
+ else if (action.equals("write")) {
+ try {
+ long fileSize = this.write(args.getString(0), args.getString(1), args.getLong(2));
+ return new PluginResult(status, fileSize);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR);
+ }
+ }
+ else if (action.equals("truncate")) {
+ try {
+ long fileSize = this.truncateFile(args.getString(0), args.getLong(1));
+ return new PluginResult(status, fileSize);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR);
+ }
+ }
+ else if (action.equals("requestFileSystem")) {
+ long size = args.optLong(1);
+ if (size != 0) {
+ if (size > DirectoryManager.getFreeDiskSpace()) {
+ JSONObject error = new JSONObject().put("code", FileUtils.QUOTA_EXCEEDED_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ }
+ }
+ JSONObject obj = requestFileSystem(args.getInt(0));
+ return new PluginResult(status, obj, "window.localFileSystem._castFS");
}
- }
- else if (action.equals("readAsDataURL")) {
- try {
- String s = this.readAsDataURL(args.getString(0));
- return new PluginResult(status, s);
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_FOUND_ERR);
- } catch (IOException e) {
- e.printStackTrace();
- return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR);
+ else if (action.equals("resolveLocalFileSystemURI")) {
+ JSONObject obj = resolveLocalFileSystemURI(args.getString(0));
+ return new PluginResult(status, obj, "window.localFileSystem._castEntry");
}
- }
- else if (action.equals("writeAsText")) {
- try {
- this.writeAsText(args.getString(0), args.getString(1), args.getBoolean(2));
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_FOUND_ERR);
- } catch (IOException e) {
- e.printStackTrace();
- return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR);
+ else if (action.equals("getMetadata")) {
+ JSONObject obj = getMetadata(args.getString(0));
+ return new PluginResult(status, obj, "window.localFileSystem._castDate");
}
- }
- return new PluginResult(status, result);
+ else if (action.equals("getFileMetadata")) {
+ JSONObject obj = getFileMetadata(args.getString(0));
+ return new PluginResult(status, obj, "window.localFileSystem._castDate");
+ }
+ else if (action.equals("getParent")) {
+ JSONObject obj = getParent(args.getString(0));
+ return new PluginResult(status, obj, "window.localFileSystem._castEntry");
+ }
+ else if (action.equals("getDirectory")) {
+ JSONObject obj = getFile(args.getString(0), args.getString(1), args.optJSONObject(2), true);
+ return new PluginResult(status, obj, "window.localFileSystem._castEntry");
+ }
+ else if (action.equals("getFile")) {
+ JSONObject obj = getFile(args.getString(0), args.getString(1), args.optJSONObject(2), false);
+ return new PluginResult(status, obj, "window.localFileSystem._castEntry");
+ }
+ else if (action.equals("remove")) {
+ boolean success;
+
+ success = remove(args.getString(0));
+
+ if (success) {
+ return new PluginResult(status);
+ } else {
+ JSONObject error = new JSONObject().put("code", FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ }
+ }
+ else if (action.equals("removeRecursively")) {
+ boolean success = removeRecursively(args.getString(0));
+ if (success) {
+ return new PluginResult(status);
+ } else {
+ JSONObject error = new JSONObject().put("code", FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ }
+ }
+ else if (action.equals("moveTo")) {
+ JSONObject entry = transferTo(args.getString(0), args.getJSONObject(1), args.optString(2), true);
+ return new PluginResult(status, entry, "window.localFileSystem._castEntry");
+ }
+ else if (action.equals("copyTo")) {
+ JSONObject entry = transferTo(args.getString(0), args.getJSONObject(1), args.optString(2), false);
+ return new PluginResult(status, entry, "window.localFileSystem._castEntry");
+ }
+ else if (action.equals("readEntries")) {
+ JSONArray entries = readEntries(args.getString(0));
+ return new PluginResult(status, entries, "window.localFileSystem._castEntries");
+ }
+ return new PluginResult(status, result);
+ } catch (FileNotFoundException e) {
+ JSONObject error = new JSONObject().put("code", FileUtils.NOT_FOUND_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ } catch (FileExistsException e) {
+ JSONObject error = new JSONObject().put("code", FileUtils.PATH_EXISTS_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ } catch (NoModificationAllowedException e) {
+ JSONObject error = new JSONObject().put("code", FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ } catch (JSONException e) {
+ JSONObject error = new JSONObject().put("code", FileUtils.NO_MODIFICATION_ALLOWED_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ } catch (InvalidModificationException e) {
+ JSONObject error = new JSONObject().put("code", FileUtils.INVALID_MODIFICATION_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ } catch (MalformedURLException e) {
+ JSONObject error = new JSONObject().put("code", FileUtils.ENCODING_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ } catch (IOException e) {
+ JSONObject error = new JSONObject().put("code", FileUtils.INVALID_MODIFICATION_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ } catch (EncodingException e) {
+ JSONObject error = new JSONObject().put("code", FileUtils.ENCODING_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ } catch (TypeMismatchException e) {
+ JSONObject error = new JSONObject().put("code", FileUtils.TYPE_MISMATCH_ERR);
+ return new PluginResult(PluginResult.Status.ERROR, error);
+ }
} catch (JSONException e) {
e.printStackTrace();
return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
}
}
+ /**
+ * Allows the user to look up the Entry for a file or directory referred to by a local URI.
+ *
+ * @param url of the file/directory to look up
+ * @return a JSONObject representing a Entry from the filesystem
+ * @throws MalformedURLException if the url is not valid
+ * @throws FileNotFoundException if the file does not exist
+ * @throws IOException if the user can't read the file
+ * @throws JSONException
+ */
+ private JSONObject resolveLocalFileSystemURI(String url) throws IOException, JSONException {
+ // Test to see if this is a valid URL first
+ @SuppressWarnings("unused")
+ URL testUrl = new URL(url);
+
+ File fp = null;
+ if (url.startsWith("file://")) {
+ fp = new File(url.substring(7, url.length()));
+ } else {
+ fp = new File(url);
+ }
+ if (!fp.exists()) {
+ throw new FileNotFoundException();
+ }
+ if (!fp.canRead()) {
+ throw new IOException();
+ }
+ return getEntry(fp);
+ }
+
+ /**
+ * Read the list of files from this directory.
+ *
+ * @param fileName the directory to read from
+ * @return a JSONArray containing JSONObjects that represent Entry objects.
+ * @throws FileNotFoundException if the directory is not found.
+ * @throws JSONException
+ */
+ private JSONArray readEntries(String fileName) throws FileNotFoundException, JSONException {
+ File fp = new File(fileName);
+
+ if (!fp.exists()) {
+ // The directory we are listing doesn't exist so we should fail.
+ throw new FileNotFoundException();
+ }
+
+ JSONArray entries = new JSONArray();
+
+ if (fp.isDirectory()) {
+ File[] files = fp.listFiles();
+ for (int i=0; i 0) {
+ throw new InvalidModificationException("directory is not empty");
+ }
+ }
+
+ // Try to rename the directory
+ if (!srcDir.renameTo(destinationDir)) {
+ // Trying to rename the directory failed. Possibly because we moved across file system on the device.
+ // Now we have to do things the hard way
+ // 1) Copy all the old files
+ // 2) delete the src directory
+ }
+
+ return getEntry(destinationDir);
+ }
+
+ /**
+ * Deletes a directory and all of its contents, if any. In the event of an error
+ * [e.g. trying to delete a directory that contains a file that cannot be removed],
+ * some of the contents of the directory may be deleted.
+ * It is an error to attempt to delete the root directory of a filesystem.
+ *
+ * @param filePath the directory to be removed
+ * @return a boolean representing success of failure
+ * @throws FileExistsException
+ */
+ private boolean removeRecursively(String filePath) throws FileExistsException {
+ File fp = new File(filePath);
+
+ // You can't delete the root directory.
+ if (atRootDirectory(filePath)) {
+ return false;
+ }
+
+ return removeDirRecursively(fp);
+ }
+
+ /**
+ * Loops through a directory deleting all the files.
+ *
+ * @param directory to be removed
+ * @return a boolean representing success of failure
+ * @throws FileExistsException
+ */
+ private boolean removeDirRecursively(File directory) throws FileExistsException {
+ if (directory.isDirectory()) {
+ for (File file : directory.listFiles()) {
+ removeDirRecursively(file);
+ }
+ }
+
+ if (!directory.delete()) {
+ throw new FileExistsException("could not delete: " + directory.getName());
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Deletes a file or directory. It is an error to attempt to delete a directory that is not empty.
+ * It is an error to attempt to delete the root directory of a filesystem.
+ *
+ * @param filePath file or directory to be removed
+ * @return a boolean representing success of failure
+ * @throws NoModificationAllowedException
+ * @throws InvalidModificationException
+ */
+ private boolean remove(String filePath) throws NoModificationAllowedException, InvalidModificationException {
+ File fp = new File(filePath);
+
+ // You can't delete the root directory.
+ if (atRootDirectory(filePath)) {
+ throw new NoModificationAllowedException("You can't delete the root directory");
+ }
+
+ // You can't delete a directory that is not empty
+ if (fp.isDirectory() && fp.list().length > 0) {
+ throw new InvalidModificationException("You can't delete a directory that is not empty.");
+ }
+
+ return fp.delete();
+ }
+
+ /**
+ * Creates or looks up a file.
+ *
+ * @param dirPath base directory
+ * @param fileName file/directory to lookup or create
+ * @param options specify whether to create or not
+ * @param directory if true look up directory, if false look up file
+ * @return a Entry object
+ * @throws FileExistsException
+ * @throws IOException
+ * @throws TypeMismatchException
+ * @throws EncodingException
+ * @throws JSONException
+ */
+ private JSONObject getFile(String dirPath, String fileName, JSONObject options, boolean directory) throws FileExistsException, IOException, TypeMismatchException, EncodingException, JSONException {
+ boolean create = false;
+ boolean exclusive = false;
+ if (options != null) {
+ create = options.optBoolean("create");
+ if (create) {
+ exclusive = options.optBoolean("exclusive");
+ }
+ }
+
+ // Check for a ":" character in the file to line up with BB and iOS
+ if (fileName.contains(":")) {
+ throw new EncodingException("This file has a : in it's name");
+ }
+
+ File fp = createFileObject(dirPath, fileName);
+
+ if (create) {
+ if (exclusive && fp.exists()) {
+ throw new FileExistsException("create/exclusive fails");
+ }
+ if (directory) {
+ fp.mkdir();
+ } else {
+ fp.createNewFile();
+ }
+ if (!fp.exists()) {
+ throw new FileExistsException("create fails");
+ }
+ }
+ else {
+ if (!fp.exists()) {
+ throw new FileNotFoundException("path does not exist");
+ }
+ if (directory) {
+ if (fp.isFile()) {
+ throw new TypeMismatchException("path doesn't exist or is file");
+ }
+ } else {
+ if (fp.isDirectory()) {
+ throw new TypeMismatchException("path doesn't exist or is directory");
+ }
+ }
+ }
+
+ // Return the directory
+ return getEntry(fp);
+ }
+
+ /**
+ * If the path starts with a '/' just return that file object. If not construct the file
+ * object from the path passed in and the file name.
+ *
+ * @param dirPath root directory
+ * @param fileName new file name
+ * @return
+ */
+ private File createFileObject(String dirPath, String fileName) {
+ File fp = null;
+ if (fileName.startsWith("/")) {
+ fp = new File(fileName);
+ } else {
+ fp = new File(dirPath + File.separator + fileName);
+ }
+ return fp;
+ }
+
+ /**
+ * Look up the parent DirectoryEntry containing this Entry.
+ * If this Entry is the root of its filesystem, its parent is itself.
+ *
+ * @param filePath
+ * @return
+ * @throws JSONException
+ */
+ private JSONObject getParent(String filePath) throws JSONException {
+ if (atRootDirectory(filePath)) {
+ return getEntry(filePath);
+ }
+ return getEntry(new File(filePath).getParent());
+ }
+
+ /**
+ * Checks to see if we are at the root directory. Useful since we are
+ * not allow to delete this directory.
+ *
+ * @param filePath to directory
+ * @return true if we are at the root, false otherwise.
+ */
+ private boolean atRootDirectory(String filePath) {
+ if (filePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath() + "/Android/data/" + ctx.getPackageName() + "/cache") ||
+ filePath.equals(Environment.getExternalStorageDirectory().getAbsolutePath())) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Look up metadata about this entry.
+ *
+ * @param filePath to entry
+ * @return a Metadata object
+ * @throws FileNotFoundException
+ * @throws JSONException
+ */
+ private JSONObject getMetadata(String filePath) throws FileNotFoundException, JSONException {
+ File file = new File(filePath);
+
+ if (!file.exists()) {
+ throw new FileNotFoundException("Failed to find file in getMetadata");
+ }
+
+ JSONObject metadata = new JSONObject();
+ metadata.put("modificationTime", file.lastModified());
+
+ return metadata;
+ }
+
+ /**
+ * Returns a File that represents the current state of the file that this FileEntry represents.
+ *
+ * @param filePath to entry
+ * @return returns a JSONObject represent a W3C File object
+ * @throws FileNotFoundException
+ * @throws JSONException
+ */
+ private JSONObject getFileMetadata(String filePath) throws FileNotFoundException, JSONException {
+ File file = new File(filePath);
+
+ if (!file.exists()) {
+ throw new FileNotFoundException("File: " + filePath + " does not exist.");
+ }
+
+ JSONObject metadata = new JSONObject();
+ metadata.put("size", file.length());
+ metadata.put("type", getMimeType(filePath));
+ metadata.put("name", file.getName());
+ metadata.put("fullPath", file.getAbsolutePath());
+ metadata.put("lastModifiedDate", file.lastModified());
+
+ return metadata;
+ }
+
+ /**
+ * Requests a filesystem in which to store application data.
+ *
+ * @param type of file system requested
+ * @return a JSONObject representing the file system
+ * @throws IOException
+ * @throws JSONException
+ */
+ private JSONObject requestFileSystem(int type) throws IOException, JSONException {
+ JSONObject fs = new JSONObject();
+ if (type == TEMPORARY) {
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ fs.put("name", "temporary");
+ fs.put("root", getEntry(Environment.getExternalStorageDirectory().getAbsolutePath() +
+ "/Android/data/" + ctx.getPackageName() + "/cache/"));
+
+ // Create the cache dir if it doesn't exist.
+ File fp = new File(Environment.getExternalStorageDirectory().getAbsolutePath() +
+ "/Android/data/" + ctx.getPackageName() + "/cache/");
+ fp.mkdirs();
+ } else {
+ throw new IOException("SD Card not mounted");
+ }
+ }
+ else if (type == PERSISTENT) {
+ if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+ fs.put("name", "persistent");
+ fs.put("root", getEntry(Environment.getExternalStorageDirectory()));
+ } else {
+ throw new IOException("SD Card not mounted");
+ }
+ }
+ else if (type == RESOURCE) {
+ fs.put("name", "resource");
+
+ }
+ else if (type == APPLICATION) {
+ fs.put("name", "application");
+
+ }
+ else {
+ throw new IOException("No filesystem of type requested");
+ }
+
+ return fs;
+ }
+
+ /**
+ * Returns a JSON Object representing a directory on the device's file system
+ *
+ * @param path to the directory
+ * @return
+ * @throws JSONException
+ */
+ private JSONObject getEntry(File file) throws JSONException {
+ JSONObject entry = new JSONObject();
+
+ entry.put("isFile", file.isFile());
+ entry.put("isDirectory", file.isDirectory());
+ entry.put("name", file.getName());
+ entry.put("fullPath", file.getAbsolutePath());
+ // I can't add the next thing it as it would be an infinite loop
+ //entry.put("filesystem", null);
+
+ return entry;
+ }
+
+ /**
+ * Returns a JSON Object representing a directory on the device's file system
+ *
+ * @param path to the directory
+ * @return
+ * @throws JSONException
+ */
+ private JSONObject getEntry(String path) throws JSONException {
+ return getEntry(new File(path));
+ }
+
/**
* Identifies if action to be executed returns a value and should be run synchronously.
*
* @param action The action to execute
* @return T=returns value
*/
- public boolean isSynch(String action) {
+ public boolean isSynch(String action) {
if (action.equals("readAsText")) {
return false;
}
@@ -139,6 +845,39 @@ public class FileUtils extends Plugin {
else if (action.equals("writeAsText")) {
return false;
}
+ else if (action.equals("requestFileSystem")) {
+ return false;
+ }
+ else if (action.equals("getMetadata")) {
+ return false;
+ }
+ else if (action.equals("toURI")) {
+ return false;
+ }
+ else if (action.equals("getParent")) {
+ return false;
+ }
+ else if (action.equals("getFile")) {
+ return false;
+ }
+ else if (action.equals("getDirectory")) {
+ return false;
+ }
+ else if (action.equals("remove")) {
+ return false;
+ }
+ else if (action.equals("removeRecursively")) {
+ return false;
+ }
+ else if (action.equals("readEntries")) {
+ return false;
+ }
+ else if (action.equals("getFileMetadata")) {
+ return false;
+ }
+ else if (action.equals("resolveLocalFileSystemURI")) {
+ return false;
+ }
return true;
}
@@ -156,15 +895,14 @@ public class FileUtils extends Plugin {
* @throws FileNotFoundException, IOException
*/
public String readAsText(String filename, String encoding) throws FileNotFoundException, IOException {
- System.out.println("FileUtils.readAsText("+filename+", "+encoding+")");
- StringBuilder data = new StringBuilder();
- FileInputStream fis = new FileInputStream(filename);
- BufferedReader reader = new BufferedReader(new InputStreamReader(fis, encoding), 1024);
- String line;
- while ((line = reader.readLine()) != null) {
- data.append(line);
- }
- return data.toString();
+ byte[] bytes = new byte[1000];
+ BufferedInputStream bis = new BufferedInputStream(getPathFromUri(filename), 1024);
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ int numRead = 0;
+ while ((numRead = bis.read(bytes, 0, 1000)) >= 0) {
+ bos.write(bytes, 0, numRead);
+ }
+ return new String(bos.toByteArray(), encoding);
}
/**
@@ -176,7 +914,7 @@ public class FileUtils extends Plugin {
*/
public String readAsDataURL(String filename) throws FileNotFoundException, IOException {
byte[] bytes = new byte[1000];
- BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filename), 1024);
+ BufferedInputStream bis = new BufferedInputStream(getPathFromUri(filename), 1024);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int numRead = 0;
while ((numRead = bis.read(bytes, 0, 1000)) >= 0) {
@@ -184,13 +922,30 @@ public class FileUtils extends Plugin {
}
// Determine content type from file name
- // TODO
- String contentType = "";
+ String contentType = null;
+ if (filename.startsWith("content:")) {
+ Uri fileUri = Uri.parse(filename);
+ contentType = this.ctx.getContentResolver().getType(fileUri);
+ }
+ else {
+ contentType = getMimeType(filename);
+ }
byte[] base64 = Base64.encodeBase64(bos.toByteArray());
String data = "data:" + contentType + ";base64," + new String(base64);
return data;
}
+
+ /**
+ * Looks up the mime type of a given file name.
+ *
+ * @param filename
+ * @return a mime type
+ */
+ private String getMimeType(String filename) {
+ MimeTypeMap map = MimeTypeMap.getSingleton();
+ return map.getMimeTypeFromExtension(map.getFileExtensionFromUrl(filename));
+ }
/**
* Write contents of file.
@@ -212,5 +967,58 @@ public class FileUtils extends Plugin {
out.close();
}
+ /**
+ * Write contents of file.
+ *
+ * @param filename The name of the file.
+ * @param data The contents of the file.
+ * @param offset The position to begin writing the file.
+ * @throws FileNotFoundException, IOException
+ */
+ public long write(String filename, String data, long offset) throws FileNotFoundException, IOException {
+ RandomAccessFile file = new RandomAccessFile(filename, "rw");
+ file.seek(offset);
+ file.writeBytes(data);
+ file.close();
+
+ return data.length();
+ }
+
+
+ /**
+ * Truncate the file to size
+ *
+ * @param filename
+ * @param size
+ * @throws FileNotFoundException, IOException
+ */
+ private long truncateFile(String filename, long size) throws FileNotFoundException, IOException {
+ RandomAccessFile raf = new RandomAccessFile(filename, "rw");
+ if (raf.length() >= size) {
+ FileChannel channel = raf.getChannel();
+ channel.truncate(size);
+ return size;
+ }
+
+ return raf.length();
+ }
+
+ /**
+ * Get an input stream based on file path or content:// uri
+ *
+ * @param path
+ * @return an input stream
+ * @throws FileNotFoundException
+ */
+ private InputStream getPathFromUri(String path) throws FileNotFoundException {
+ if (path.startsWith("content")) {
+ Uri uri = Uri.parse(path);
+ return ctx.getContentResolver().openInputStream(uri);
+ }
+ else {
+ return new FileInputStream(path);
+ }
+ }
}
+
diff --git a/framework/src/com/phonegap/GeoListener.java b/framework/src/com/phonegap/GeoListener.java
index 01818ada..b9e86b6e 100755
--- a/framework/src/com/phonegap/GeoListener.java
+++ b/framework/src/com/phonegap/GeoListener.java
@@ -86,7 +86,7 @@ public class GeoListener {
* @param msg The error message
*/
void fail(int code, String msg) {
- this.broker.sendJavascript("navigator._geo.fail('" + this.id + "', " + ", " + code + ", '" + msg + "');");
+ this.broker.sendJavascript("navigator._geo.fail('" + this.id + "', '" + code + "', '" + msg + "');");
this.stop();
}
diff --git a/framework/src/com/phonegap/GpsListener.java b/framework/src/com/phonegap/GpsListener.java
index 14a29f1e..767a489d 100755
--- a/framework/src/com/phonegap/GpsListener.java
+++ b/framework/src/com/phonegap/GpsListener.java
@@ -7,6 +7,8 @@
*/
package com.phonegap;
+import com.phonegap.api.PhonegapActivity;
+
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
@@ -19,7 +21,7 @@ import android.os.Bundle;
*/
public class GpsListener implements LocationListener {
- private DroidGap mCtx; // DroidGap object
+ private PhonegapActivity mCtx; // PhonegapActivity object
private LocationManager mLocMan; // Location manager object
private GeoListener owner; // Geolistener object (parent)
@@ -35,7 +37,7 @@ public class GpsListener implements LocationListener {
* @param interval
* @param m
*/
- public GpsListener(DroidGap ctx, int interval, GeoListener m) {
+ public GpsListener(PhonegapActivity ctx, int interval, GeoListener m) {
this.owner = m;
this.mCtx = ctx;
this.mLocMan = (LocationManager) this.mCtx.getSystemService(Context.LOCATION_SERVICE);
diff --git a/framework/src/com/phonegap/HttpHandler.java b/framework/src/com/phonegap/HttpHandler.java
old mode 100644
new mode 100755
index ab210fe4..4218b928
--- a/framework/src/com/phonegap/HttpHandler.java
+++ b/framework/src/com/phonegap/HttpHandler.java
@@ -62,7 +62,6 @@ public class HttpHandler {
if (numread <= 0)
break;
out.write(buff, 0, numread);
- System.out.println("numread" + numread);
i++;
} while (true);
out.flush();
diff --git a/framework/src/com/phonegap/NetworkListener.java b/framework/src/com/phonegap/NetworkListener.java
index 3ec107ce..6c225f2e 100755
--- a/framework/src/com/phonegap/NetworkListener.java
+++ b/framework/src/com/phonegap/NetworkListener.java
@@ -7,6 +7,8 @@
*/
package com.phonegap;
+import com.phonegap.api.PhonegapActivity;
+
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
@@ -15,7 +17,7 @@ import android.os.Bundle;
public class NetworkListener implements LocationListener {
- private DroidGap mCtx; // DroidGap object
+ private PhonegapActivity mCtx; // PhonegapActivity object
private LocationManager mLocMan; // Location manager object
private GeoListener owner; // Geolistener object (parent)
@@ -31,7 +33,7 @@ public class NetworkListener implements LocationListener {
* @param interval
* @param m
*/
- public NetworkListener(DroidGap ctx, int interval, GeoListener m) {
+ public NetworkListener(PhonegapActivity ctx, int interval, GeoListener m) {
this.owner = m;
this.mCtx = ctx;
this.mLocMan = (LocationManager) this.mCtx.getSystemService(Context.LOCATION_SERVICE);
diff --git a/framework/src/com/phonegap/NetworkManager.java b/framework/src/com/phonegap/NetworkManager.java
index 4ff6c971..d7dfa6dc 100755
--- a/framework/src/com/phonegap/NetworkManager.java
+++ b/framework/src/com/phonegap/NetworkManager.java
@@ -12,6 +12,7 @@ import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONArray;
import org.json.JSONException;
+import com.phonegap.api.PhonegapActivity;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
@@ -38,7 +39,7 @@ public class NetworkManager extends Plugin {
*
* @param ctx The context of the main Activity.
*/
- public void setContext(DroidGap ctx) {
+ public void setContext(PhonegapActivity ctx) {
super.setContext(ctx);
this.sockMan = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE);
}
diff --git a/framework/src/com/phonegap/Notification.java b/framework/src/com/phonegap/Notification.java
index 538a86b2..360ea617 100755
--- a/framework/src/com/phonegap/Notification.java
+++ b/framework/src/com/phonegap/Notification.java
@@ -10,6 +10,7 @@ package com.phonegap;
import org.json.JSONArray;
import org.json.JSONException;
import com.phonegap.api.Plugin;
+import com.phonegap.api.PhonegapActivity;
import com.phonegap.api.PluginResult;
import android.app.AlertDialog;
import android.app.ProgressDialog;
@@ -55,11 +56,16 @@ public class Notification extends Plugin {
this.vibrate(args.getLong(0));
}
else if (action.equals("alert")) {
- this.alert(args.getString(0),args.getString(1),args.getString(2));
+ this.alert(args.getString(0),args.getString(1),args.getString(2), callbackId);
+ PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
+ r.setKeepCallback(true);
+ return r;
}
else if (action.equals("confirm")) {
- int i = this.confirm(args.getString(0),args.getString(1),args.getString(2));
- return new PluginResult(status, i);
+ this.confirm(args.getString(0),args.getString(1),args.getString(2), callbackId);
+ PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
+ r.setKeepCallback(true);
+ return r;
}
else if (action.equals("activityStart")) {
this.activityStart(args.getString(0),args.getString(1));
@@ -160,105 +166,99 @@ public class Notification extends Plugin {
/**
* Builds and shows a native Android alert with given Strings
- * @param message The message the alert should display
- * @param title The title of the alert
- * @param buttonLabel The label of the button
+ * @param message The message the alert should display
+ * @param title The title of the alert
+ * @param buttonLabel The label of the button
+ * @param callbackId The callback id
*/
- public synchronized void alert(String message,String title,String buttonLabel){
- AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx);
- dlg.setMessage(message);
- dlg.setTitle(title);
- dlg.setCancelable(false);
- dlg.setPositiveButton(buttonLabel,
- new AlertDialog.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- }
- });
- dlg.create();
- dlg.show();
+ public synchronized void alert(final String message, final String title, final String buttonLabel, final String callbackId) {
+
+ final PhonegapActivity ctx = this.ctx;
+ final Notification notification = this;
+
+ Runnable runnable = new Runnable() {
+ public void run() {
+
+ AlertDialog.Builder dlg = new AlertDialog.Builder(ctx);
+ dlg.setMessage(message);
+ dlg.setTitle(title);
+ dlg.setCancelable(false);
+ dlg.setPositiveButton(buttonLabel,
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ notification.success(new PluginResult(PluginResult.Status.OK, 0), callbackId);
+ }
+ });
+ dlg.create();
+ dlg.show();
+ };
+ };
+ this.ctx.runOnUiThread(runnable);
}
/**
* Builds and shows a native Android confirm dialog with given title, message, buttons.
* This dialog only shows up to 3 buttons. Any labels after that will be ignored.
+ * The index of the button pressed will be returned to the JavaScript callback identified by callbackId.
*
* @param message The message the dialog should display
* @param title The title of the dialog
* @param buttonLabels A comma separated list of button labels (Up to 3 buttons)
- * @return The index of the button clicked (1,2 or 3)
+ * @param callbackId The callback id
*/
- public synchronized int confirm(final String message, final String title, String buttonLabels) {
-
- // Create dialog on UI thread
- final DroidGap ctx = this.ctx;
+ public synchronized void confirm(final String message, final String title, String buttonLabels, final String callbackId) {
+
+ final PhonegapActivity ctx = this.ctx;
final Notification notification = this;
final String[] fButtons = buttonLabels.split(",");
+
Runnable runnable = new Runnable() {
public void run() {
AlertDialog.Builder dlg = new AlertDialog.Builder(ctx);
dlg.setMessage(message);
dlg.setTitle(title);
dlg.setCancelable(false);
-
+
// First button
if (fButtons.length > 0) {
dlg.setPositiveButton(fButtons[0],
- new AlertDialog.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- synchronized(notification) {
- notification.confirmResult = 1;
- notification.notifyAll();
- }
- }
- });
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ notification.success(new PluginResult(PluginResult.Status.OK, 1), callbackId);
+ }
+ });
}
-
+
// Second button
if (fButtons.length > 1) {
dlg.setNeutralButton(fButtons[1],
- new AlertDialog.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- synchronized(notification) {
- notification.confirmResult = 2;
- notification.notifyAll();
- }
- }
- });
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ notification.success(new PluginResult(PluginResult.Status.OK, 2), callbackId);
+ }
+ });
}
-
+
// Third button
if (fButtons.length > 2) {
dlg.setNegativeButton(fButtons[2],
- new AlertDialog.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) {
- dialog.dismiss();
- synchronized(notification) {
- notification.confirmResult = 3;
- notification.notifyAll();
- }
- }
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ notification.success(new PluginResult(PluginResult.Status.OK, 3), callbackId);
}
+ }
);
}
dlg.create();
dlg.show();
- }
+ };
};
this.ctx.runOnUiThread(runnable);
-
- // Wait for dialog to close
- synchronized(runnable) {
- try {
- this.wait();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- return this.confirmResult;
}
/**
@@ -273,7 +273,7 @@ public class Notification extends Plugin {
this.spinnerDialog = null;
}
final Notification notification = this;
- final DroidGap ctx = this.ctx;
+ final PhonegapActivity ctx = this.ctx;
Runnable runnable = new Runnable() {
public void run() {
notification.spinnerDialog = ProgressDialog.show(ctx, title , message, true, true,
@@ -309,7 +309,7 @@ public class Notification extends Plugin {
this.progressDialog = null;
}
final Notification notification = this;
- final DroidGap ctx = this.ctx;
+ final PhonegapActivity ctx = this.ctx;
Runnable runnable = new Runnable() {
public void run() {
notification.progressDialog = new ProgressDialog(ctx);
diff --git a/framework/src/com/phonegap/Storage.java b/framework/src/com/phonegap/Storage.java
index 3cf798c5..de30afb2 100755
--- a/framework/src/com/phonegap/Storage.java
+++ b/framework/src/com/phonegap/Storage.java
@@ -160,24 +160,25 @@ public class Storage extends Plugin {
*/
public void processResults(Cursor cur, String tx_id) {
+ String result = "[]";
// If query result has rows
+
if (cur.moveToFirst()) {
+ JSONArray fullresult = new JSONArray();
String key = "";
String value = "";
int colCount = cur.getColumnCount();
// Build up JSON result object for each row
do {
- JSONObject result = new JSONObject();
+ JSONObject row = 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);
+ value = cur.getString(i);
+ row.put(key, value);
}
-
- // Send row back to JavaScript
- this.sendJavascript("droiddb.addResult('" + result.toString() + "','" + tx_id + "');");
+ fullresult.put(row);
} catch (JSONException e) {
e.printStackTrace();
@@ -185,9 +186,11 @@ public class Storage extends Plugin {
} while (cur.moveToNext());
+ result = fullresult.toString();
}
+
// Let JavaScript know that there are no more rows
- this.sendJavascript("droiddb.completeQuery('" + tx_id + "');");
+ this.sendJavascript("droiddb.completeQuery('" + tx_id + "', "+result+");");
}
diff --git a/framework/src/com/phonegap/TempListener.java b/framework/src/com/phonegap/TempListener.java
old mode 100644
new mode 100755
index fc0a3a7a..a8abab47
--- a/framework/src/com/phonegap/TempListener.java
+++ b/framework/src/com/phonegap/TempListener.java
@@ -11,6 +11,7 @@ import java.util.List;
import org.json.JSONArray;
+import com.phonegap.api.PhonegapActivity;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
@@ -37,7 +38,7 @@ public class TempListener extends Plugin implements SensorEventListener {
*
* @param ctx The context of the main Activity.
*/
- public void setContext(DroidGap ctx) {
+ public void setContext(PhonegapActivity ctx) {
super.setContext(ctx);
this.sensorManager = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE);
}
diff --git a/framework/src/com/phonegap/api/IPlugin.java b/framework/src/com/phonegap/api/IPlugin.java
index f36d00b2..b3b3a1da 100755
--- a/framework/src/com/phonegap/api/IPlugin.java
+++ b/framework/src/com/phonegap/api/IPlugin.java
@@ -8,7 +8,6 @@
package com.phonegap.api;
import org.json.JSONArray;
-import com.phonegap.DroidGap;
import android.content.Intent;
import android.webkit.WebView;
@@ -43,7 +42,7 @@ public interface IPlugin {
*
* @param ctx The context of the main Activity.
*/
- void setContext(DroidGap ctx);
+ void setContext(PhonegapActivity ctx);
/**
* Sets the main View of the application, this is the WebView within which
diff --git a/framework/src/com/phonegap/api/PhonegapActivity.java b/framework/src/com/phonegap/api/PhonegapActivity.java
new file mode 100755
index 00000000..dcadfa2b
--- /dev/null
+++ b/framework/src/com/phonegap/api/PhonegapActivity.java
@@ -0,0 +1,43 @@
+/*
+ * PhoneGap is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ *
+ * Copyright (c) 2005-2010, Nitobi Software Inc.
+ * Copyright (c) 2010, IBM Corporation
+ */
+package com.phonegap.api;
+
+import android.app.Activity;
+import android.content.Intent;
+
+/**
+ * The Phonegap activity abstract class that is extended by DroidGap.
+ * It is used to isolate plugin development, and remove dependency on entire Phonegap library.
+ */
+public abstract class PhonegapActivity extends Activity {
+
+ /**
+ * Add a class that implements a service.
+ *
+ * @param serviceType
+ * @param className
+ */
+ abstract public void addService(String serviceType, String className);
+
+ /**
+ * Send JavaScript statement back to JavaScript.
+ *
+ * @param message
+ */
+ abstract public void sendJavascript(String statement);
+
+ /**
+ * Launch an activity for which you would like a result when it finished. When this activity exits,
+ * your onActivityResult() method will be called.
+ *
+ * @param command The command object
+ * @param intent The intent to start
+ * @param requestCode The request code that is passed to callback to identify the activity
+ */
+ abstract public void startActivityForResult(Plugin command, Intent intent, int requestCode);
+}
diff --git a/framework/src/com/phonegap/api/Plugin.java b/framework/src/com/phonegap/api/Plugin.java
index c16c6960..a527159a 100755
--- a/framework/src/com/phonegap/api/Plugin.java
+++ b/framework/src/com/phonegap/api/Plugin.java
@@ -8,7 +8,7 @@
package com.phonegap.api;
import org.json.JSONArray;
-import com.phonegap.DroidGap;
+
import android.content.Intent;
import android.webkit.WebView;
@@ -20,7 +20,7 @@ import android.webkit.WebView;
public abstract class Plugin implements IPlugin {
public WebView webView; // WebView object
- public DroidGap ctx; // DroidGap object
+ public PhonegapActivity ctx; // PhonegapActivity object
/**
* Executes the request and returns PluginResult.
@@ -48,7 +48,7 @@ public abstract class Plugin implements IPlugin {
*
* @param ctx The context of the main Activity.
*/
- public void setContext(DroidGap ctx) {
+ public void setContext(PhonegapActivity ctx) {
this.ctx = ctx;
}
@@ -99,7 +99,7 @@ public abstract class Plugin implements IPlugin {
* @param statement
*/
public void sendJavascript(String statement) {
- this.ctx.callbackServer.sendJavascript(statement);
+ this.ctx.sendJavascript(statement);
}
/**
@@ -113,7 +113,7 @@ public abstract class Plugin implements IPlugin {
* @param callbackId The callback id used when calling back into JavaScript.
*/
public void success(PluginResult pluginResult, String callbackId) {
- this.ctx.callbackServer.sendJavascript(pluginResult.toSuccessCallbackString(callbackId));
+ this.ctx.sendJavascript(pluginResult.toSuccessCallbackString(callbackId));
}
/**
@@ -123,6 +123,6 @@ public abstract class Plugin implements IPlugin {
* @param callbackId The callback id used when calling back into JavaScript.
*/
public void error(PluginResult pluginResult, String callbackId) {
- this.ctx.callbackServer.sendJavascript(pluginResult.toErrorCallbackString(callbackId));
+ this.ctx.sendJavascript(pluginResult.toErrorCallbackString(callbackId));
}
}
diff --git a/framework/src/com/phonegap/api/PluginManager.java b/framework/src/com/phonegap/api/PluginManager.java
index a1e85474..ad1cf20b 100755
--- a/framework/src/com/phonegap/api/PluginManager.java
+++ b/framework/src/com/phonegap/api/PluginManager.java
@@ -14,7 +14,6 @@ import org.json.JSONArray;
import org.json.JSONException;
import android.webkit.WebView;
-import com.phonegap.DroidGap;
/**
* PluginManager is exposed to JavaScript in the PhoneGap WebView.
@@ -27,7 +26,7 @@ public final class PluginManager {
private HashMap plugins = new HashMap();
private HashMap services = new HashMap();
- private final DroidGap ctx;
+ private final PhonegapActivity ctx;
private final WebView app;
/**
@@ -36,8 +35,7 @@ public final class PluginManager {
* @param app
* @param ctx
*/
- public PluginManager(WebView app, DroidGap ctx) {
- System.out.println("PluginManager()");
+ public PluginManager(WebView app, PhonegapActivity ctx) {
this.ctx = ctx;
this.app = app;
}
@@ -66,7 +64,6 @@ public final class PluginManager {
*/
@SuppressWarnings("unchecked")
public String exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {
- System.out.println("PluginManager.exec("+service+", "+action+", "+callbackId+", "+jsonArgs+", "+async+")");
PluginResult cr = null;
boolean runAsync = async;
try {
@@ -78,7 +75,7 @@ public final class PluginManager {
}
if (isPhoneGapPlugin(c)) {
final Plugin plugin = this.addPlugin(clazz, c);
- final DroidGap ctx = this.ctx;
+ final PhonegapActivity ctx = this.ctx;
runAsync = async && !plugin.isSynch(action);
if (runAsync) {
// Run this on a different thread so that this one can return back to JS
@@ -87,10 +84,19 @@ public final class PluginManager {
try {
// Call execute on the plugin so that it can do it's thing
PluginResult cr = plugin.execute(action, args, callbackId);
- // Check the status for 0 (success) or otherwise
- if (cr.getStatus() == 0) {
+ int status = cr.getStatus();
+
+ // If no result to be sent and keeping callback, then no need to sent back to JavaScript
+ if ((status == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
+ }
+
+ // Check the success (OK, NO_RESULT & !KEEP_CALLBACK)
+ else if ((status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal())) {
ctx.sendJavascript(cr.toSuccessCallbackString(callbackId));
- } else {
+ }
+
+ // If error
+ else {
ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
}
} catch (Exception e) {
@@ -104,6 +110,11 @@ public final class PluginManager {
} else {
// Call execute on the plugin so that it can do it's thing
cr = plugin.execute(action, args, callbackId);
+
+ // If no result to be sent and keeping callback, then no need to sent back to JavaScript
+ if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
+ return "";
+ }
}
}
} catch (ClassNotFoundException e) {
@@ -119,9 +130,6 @@ public final class PluginManager {
}
ctx.sendJavascript(cr.toErrorCallbackString(callbackId));
}
- if (cr != null) {
- System.out.println(" -- returning result: "+cr.getJSONString());
- }
return ( cr != null ? cr.getJSONString() : "{ status: 0, message: 'all good' }" );
}
@@ -183,11 +191,10 @@ public final class PluginManager {
if (this.plugins.containsKey(className)) {
return this.getPlugin(className);
}
- System.out.println("PluginManager.addPlugin("+className+")");
try {
Plugin plugin = (Plugin)clazz.newInstance();
this.plugins.put(className, plugin);
- plugin.setContext((DroidGap)this.ctx);
+ plugin.setContext(this.ctx);
plugin.setView(this.app);
return plugin;
}
@@ -219,7 +226,7 @@ public final class PluginManager {
public void addService(String serviceType, String className) {
this.services.put(serviceType, className);
}
-
+
/**
* Called when the system is about to start resuming a previous activity.
*/
diff --git a/framework/src/com/phonegap/api/PluginResult.java b/framework/src/com/phonegap/api/PluginResult.java
index d089deda..41371702 100755
--- a/framework/src/com/phonegap/api/PluginResult.java
+++ b/framework/src/com/phonegap/api/PluginResult.java
@@ -10,18 +10,34 @@ package com.phonegap.api;
import org.json.JSONArray;
import org.json.JSONObject;
+import android.util.Log;
+
public class PluginResult {
private final int status;
private final String message;
+ private boolean keepCallback = false;
+ private String cast = null;
public PluginResult(Status status) {
this.status = status.ordinal();
- this.message = PluginResult.StatusMessages[this.status];
+ this.message = "'" + PluginResult.StatusMessages[this.status] + "'";
}
public PluginResult(Status status, String message) {
this.status = status.ordinal();
- this.message = "'" + message + "'";
+ this.message = JSONObject.quote(message);
+ }
+
+ public PluginResult(Status status, JSONArray message, String cast) {
+ this.status = status.ordinal();
+ this.message = message.toString();
+ this.cast = cast;
+ }
+
+ public PluginResult(Status status, JSONObject message, String cast) {
+ this.status = status.ordinal();
+ this.message = message.toString();
+ this.cast = cast;
}
public PluginResult(Status status, JSONArray message) {
@@ -33,21 +49,26 @@ public class PluginResult {
this.status = status.ordinal();
this.message = message.toString();
}
-
- // TODO: BC: Added
+
public PluginResult(Status status, int i) {
this.status = status.ordinal();
this.message = ""+i;
}
+
public PluginResult(Status status, float f) {
this.status = status.ordinal();
this.message = ""+f;
}
+
public PluginResult(Status status, boolean b) {
this.status = status.ordinal();
this.message = ""+b;
}
+ public void setKeepCallback(boolean b) {
+ this.keepCallback = b;
+ }
+
public int getStatus() {
return status;
}
@@ -56,12 +77,24 @@ public class PluginResult {
return message;
}
+ public boolean getKeepCallback() {
+ return this.keepCallback;
+ }
+
public String getJSONString() {
- return "{ status: " + this.getStatus() + ", message: " + this.getMessage() + " }";
+ return "{status:" + this.status + ",message:" + this.message + ",keepCallback:" + this.keepCallback + "}";
}
public String toSuccessCallbackString(String callbackId) {
- return "PhoneGap.callbackSuccess('"+callbackId+"', " + this.getJSONString() + " );";
+ StringBuffer buf = new StringBuffer("");
+ if (cast != null) {
+ buf.append("var temp = "+cast+"("+this.getJSONString() + ");\n");
+ buf.append("PhoneGap.callbackSuccess('"+callbackId+"',temp);");
+ }
+ else {
+ buf.append("PhoneGap.callbackSuccess('"+callbackId+"',"+this.getJSONString()+");");
+ }
+ return buf.toString();
}
public String toErrorCallbackString(String callbackId) {
@@ -69,6 +102,7 @@ public class PluginResult {
}
public static String[] StatusMessages = new String[] {
+ "No result",
"OK",
"Class not found",
"Illegal access",
@@ -81,6 +115,7 @@ public class PluginResult {
};
public enum Status {
+ NO_RESULT,
OK,
CLASS_NOT_FOUND_EXCEPTION,
ILLEGAL_ACCESS_EXCEPTION,
diff --git a/framework/src/com/phonegap/file/EncodingException.java b/framework/src/com/phonegap/file/EncodingException.java
new file mode 100644
index 00000000..7dcf7af0
--- /dev/null
+++ b/framework/src/com/phonegap/file/EncodingException.java
@@ -0,0 +1,9 @@
+package com.phonegap.file;
+
+public class EncodingException extends Exception {
+
+ public EncodingException(String message) {
+ super(message);
+ }
+
+}
diff --git a/framework/src/com/phonegap/file/FileExistsException.java b/framework/src/com/phonegap/file/FileExistsException.java
new file mode 100644
index 00000000..22c40ab2
--- /dev/null
+++ b/framework/src/com/phonegap/file/FileExistsException.java
@@ -0,0 +1,9 @@
+package com.phonegap.file;
+
+public class FileExistsException extends Exception {
+
+ public FileExistsException(String msg) {
+ super(msg);
+ }
+
+}
diff --git a/framework/src/com/phonegap/file/InvalidModificationException.java b/framework/src/com/phonegap/file/InvalidModificationException.java
new file mode 100644
index 00000000..bd706299
--- /dev/null
+++ b/framework/src/com/phonegap/file/InvalidModificationException.java
@@ -0,0 +1,9 @@
+package com.phonegap.file;
+
+public class InvalidModificationException extends Exception {
+
+ public InvalidModificationException(String message) {
+ super(message);
+ }
+
+}
diff --git a/framework/src/com/phonegap/file/NoModificationAllowedException.java b/framework/src/com/phonegap/file/NoModificationAllowedException.java
new file mode 100644
index 00000000..d120fb8f
--- /dev/null
+++ b/framework/src/com/phonegap/file/NoModificationAllowedException.java
@@ -0,0 +1,9 @@
+package com.phonegap.file;
+
+public class NoModificationAllowedException extends Exception {
+
+ public NoModificationAllowedException(String message) {
+ super(message);
+ }
+
+}
diff --git a/framework/src/com/phonegap/file/TypeMismatchException.java b/framework/src/com/phonegap/file/TypeMismatchException.java
new file mode 100644
index 00000000..7af86698
--- /dev/null
+++ b/framework/src/com/phonegap/file/TypeMismatchException.java
@@ -0,0 +1,9 @@
+package com.phonegap.file;
+
+public class TypeMismatchException extends Exception {
+
+ public TypeMismatchException(String message) {
+ super(message);
+ }
+
+}
diff --git a/lib/classic.rb b/lib/classic.rb
old mode 100644
new mode 100755
index f28e4e72..8c1ca2d8
--- a/lib/classic.rb
+++ b/lib/classic.rb
@@ -19,9 +19,15 @@ class Classic
end
def setup
- @android_dir = File.expand_path(File.dirname(__FILE__).gsub('lib',''))
+ @android_dir = File.expand_path(File.dirname(__FILE__).gsub(/lib$/,''))
@framework_dir = File.join(@android_dir, "framework")
- @icon = File.join(@www, 'icon.png')
+ @icon = File.join(@www, 'icon.png') unless File.exists?(@icon)
+ # Hash that stores the location of icons for each resolution type. Uses the default icon for all resolutions as a baseline.
+ @icons = {
+ :"drawable-ldpi" => @icon,
+ :"drawable-mdpi" => @icon,
+ :"drawable-hdpi" => @icon
+ } if @icons.nil?
@app_js_dir = ''
@content = 'index.html'
end
@@ -82,11 +88,12 @@ class Classic
# copies stuff from src directory into the android project directory (@path)
def copy_libs
+ version = IO.read(File.join(@framework_dir, '../VERSION'))
framework_res_dir = File.join(@framework_dir, "res")
app_res_dir = File.join(@path, "res")
# copies in the jar
FileUtils.mkdir_p File.join(@path, "libs")
- FileUtils.cp File.join(@framework_dir, "phonegap.jar"), File.join(@path, "libs")
+ FileUtils.cp File.join(@framework_dir, "phonegap.#{ version }.jar"), File.join(@path, "libs")
# copies in the strings.xml
FileUtils.mkdir_p File.join(app_res_dir, "values")
FileUtils.cp File.join(framework_res_dir, "values","strings.xml"), File.join(app_res_dir, "values", "strings.xml")
@@ -96,11 +103,19 @@ class Classic
FileUtils.cp File.join(framework_res_dir, "layout", f), File.join(app_res_dir, "layout", f)
end
# icon file copy
- # if it is not in the www directory use the default one in the src dir
- @icon = File.join(framework_res_dir, "drawable", "icon.png") unless File.exists?(@icon)
%w(drawable-hdpi drawable-ldpi drawable-mdpi).each do |e|
+ # if specific resolution icons are specified, use those. if not, see if a general purpose icon was defined.
+ # finally, fall back to using the default PhoneGap one.
+ currentIcon = ""
+ if !@icons[e.to_sym].nil? && File.exists?(File.join(@www, @icons[e.to_sym]))
+ currentIcon = File.join(@www, @icons[e.to_sym])
+ elsif File.exists?(@icon)
+ currentIcon = @icon
+ else
+ currentIcon = File.join(framework_res_dir, "drawable", "icon.png")
+ end
FileUtils.mkdir_p(File.join(app_res_dir, e))
- FileUtils.cp(@icon, File.join(app_res_dir, e, "icon.png"))
+ FileUtils.cp(currentIcon, File.join(app_res_dir, e, "icon.png"))
end
# concat JS and put into www folder. this can be overridden in the config.xml via @app_js_dir
js_dir = File.join(@framework_dir, "assets", "js")
@@ -110,7 +125,7 @@ class Classic
phonegapjs << IO.read(File.join(js_dir, script))
phonegapjs << "\n\n"
end
- File.open(File.join(@path, "assets", "www", @app_js_dir, "phonegap.js"), 'w') {|f| f.write(phonegapjs) }
+ File.open(File.join(@path, "assets", "www", @app_js_dir, "phonegap.#{ version }.js"), 'w') {|f| f.write(phonegapjs) }
end
# puts app name in strings
@@ -126,8 +141,7 @@ class Classic
end
end
- # this is so fucking unholy yet oddly beautiful
- # not sure if I should thank Ruby or apologize for this abusive use of string interpolation
+ # create java source file
def write_java
j = "
package #{ @pkg };
diff --git a/lib/create.rb b/lib/create.rb
index eec2c154..aef3dd49 100644
--- a/lib/create.rb
+++ b/lib/create.rb
@@ -29,7 +29,7 @@ class Create < Classic
@content = 'index.html'
# stop executation on errors
- raise 'Expected index.html in the following folder #{ path }.\nThe path is expected to be the directory droidgap create is run from or specified as a command line arg like droidgap create my_path.' unless File.exists? File.join(path, 'index.html')
+ raise "Expected index.html in the following folder #{ path }.\nThe path is expected to be the directory droidgap create is run from or specified as a command line arg like droidgap create my_path." unless File.exists? File.join(path, 'index.html')
raise 'Could not find android in your PATH!' if @android_sdk_path.empty?
end
@@ -40,16 +40,40 @@ class Create < Classic
if File.exists?(config_file)
require 'rexml/document'
f = File.new config_file
- doc = REXML::Document.new(f)
- @config = {}
+ doc = REXML::Document.new(f)
+ @config = {}
@config[:id] = doc.root.attributes["id"]
@config[:version] = doc.root.attributes["version"]
-
+ @config[:icons] = {}
+ defaultIconSize = 0
doc.root.elements.each do |n|
@config[:name] = n.text.gsub('-','').gsub(' ','') if n.name == 'name'
@config[:description] = n.text if n.name == 'description'
- @config[:icon] = n.attributes["src"] if n.name == 'icon'
- @config[:content] = n.attributes["src"] if n.name == 'content'
+ @config[:content] = n.attributes["src"] if n.name == 'content'
+ if n.name == 'icon'
+ if n.attributes["width"] == '72' && n.attributes["height"] == '72'
+ @config[:icons]["drawable-hdpi".to_sym] = n.attributes["src"]
+ if 72 > defaultIconSize
+ @config[:icon] = n.attributes["src"]
+ defaultIconSize = 72
+ end
+ elsif n.attributes["width"] == '48' && n.attributes["height"] == '48'
+ @config[:icons]["drawable-mdpi".to_sym] = n.attributes["src"]
+ if 48 > defaultIconSize
+ @config[:icon] = n.attributes["src"]
+ defaultIconSize = 48
+ end
+ elsif n.attributes["width"] == '36' && n.attributes["height"] == '36'
+ @config[:icons]["drawable-ldpi".to_sym] = n.attributes["src"]
+ if 36 > defaultIconSize
+ @config[:icon] = n.attributes["src"]
+ defaultIconSize = 36
+ end
+ else
+ @config[:icon] = n.attributes["src"]
+ end
+ end
+
if n.name == "preference" && n.attributes["name"] == 'javascript_folder'
@config[:js_dir] = n.attributes["value"]
@@ -62,7 +86,8 @@ class Create < Classic
# will change the name from the directory to the name element text
@name = @config[:name] if @config[:name]
# set the icon from the config
- @icon = File.join(@www, @config[:icon])
+ @icon = File.join(@www, @config[:icon]) if @config[:icon]
+ @icons = @config[:icons] if @config[:icons].length > 0
# sets the app js dir where phonegap.js gets copied
@app_js_dir = @config[:js_dir] ? @config[:js_dir] : ''
# sets the start page
diff --git a/lib/update.rb b/lib/update.rb
old mode 100644
new mode 100755
index 26814070..aa1459a1
--- a/lib/update.rb
+++ b/lib/update.rb
@@ -15,9 +15,9 @@ class Update
end
# removes local.properties and recreates based on android_sdk_path
- # then generates framework/phonegap.jar
+ # then generates framework/phonegap.jar & framework/assets/www/phonegap.js
def build_jar
- puts "Building the JAR..."
+ puts "Building the JAR and combining JS files..."
%w(local.properties phonegap.js phonegap.jar).each do |f|
FileUtils.rm File.join(@framework_dir, f) if File.exists? File.join(@framework_dir, f)
end
@@ -32,23 +32,13 @@ class Update
# copies stuff from framework into the project
# TODO need to allow for www import inc icon
def copy_libs
- puts "Copying over libraries and assets and creating phonegap.js..."
+ puts "Copying over libraries and assets..."
FileUtils.mkdir_p File.join(@path, "libs")
FileUtils.cp File.join(@framework_dir, "phonegap.jar"), File.join(@path, "libs")
- # concat JS and put into www folder.
- js_dir = File.join(@framework_dir, "assets", "js")
-
- phonegapjs = IO.read(File.join(js_dir, 'phonegap.js.base'))
-
- Dir.new(js_dir).entries.each do |script|
- next if script[0].chr == "." or script == "phonegap.js.base"
- phonegapjs << IO.read(File.join(js_dir, script))
- phonegapjs << "\n\n"
- end
-
- File.open(File.join(@path, "assets", "www", "phonegap.js"), 'w') {|f| f.write(phonegapjs) }
+ FileUtils.mkdir_p File.join(@path, "assets", "www")
+ FileUtils.cp File.join(@framework_dir, "assets", "www", "phonegap.js"), File.join(@path, "assets", "www")
end
#
end
\ No newline at end of file
diff --git a/util/yuicompressor/LICENSE b/util/yuicompressor/LICENSE
new file mode 100755
index 00000000..c364b9da
--- /dev/null
+++ b/util/yuicompressor/LICENSE
@@ -0,0 +1,31 @@
+YUI is issued by Yahoo! under the BSD License below.
+
+Copyright (c) 2010, Yahoo! Inc.
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms, with or
+without modification, are permitted provided that the following conditions
+are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of Yahoo! Inc. nor the names of its contributors may be
+ used to endorse or promote products derived from this software without
+ specific prior written permission of Yahoo! Inc.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+http://developer.yahoo.com/yui/license.html
+
diff --git a/util/yuicompressor/README b/util/yuicompressor/README
new file mode 100755
index 00000000..1604846c
--- /dev/null
+++ b/util/yuicompressor/README
@@ -0,0 +1,140 @@
+==============================================================================
+YUI Compressor
+==============================================================================
+
+NAME
+
+ YUI Compressor - The Yahoo! JavaScript and CSS Compressor
+
+SYNOPSIS
+
+ Usage: java -jar yuicompressor-x.y.z.jar [options] [input file]
+
+ Global Options
+ -h, --help Displays this information
+ --type Specifies the type of the input file
+ --charset Read the input file using
+ --line-break Insert a line break after the specified column number
+ -v, --verbose Display informational messages and warnings
+ -o Place the output into . Defaults to stdout.
+
+ JavaScript Options
+ --nomunge Minify only, do not obfuscate
+ --preserve-semi Preserve all semicolons
+ --disable-optimizations Disable all micro optimizations
+
+DESCRIPTION
+
+ The YUI Compressor is a JavaScript compressor which, in addition to removing
+ comments and white-spaces, obfuscates local variables using the smallest
+ possible variable name. This obfuscation is safe, even when using constructs
+ such as 'eval' or 'with' (although the compression is not optimal is those
+ cases) Compared to jsmin, the average savings is around 20%.
+
+ The YUI Compressor is also able to safely compress CSS files. The decision
+ on which compressor is being used is made on the file extension (js or css)
+
+GLOBAL OPTIONS
+
+ -h, --help
+ Prints help on how to use the YUI Compressor
+
+ --line-break
+ Some source control tools don't like files containing lines longer than,
+ say 8000 characters. The linebreak option is used in that case to split
+ long lines after a specific column. It can also be used to make the code
+ more readable, easier to debug (especially with the MS Script Debugger)
+ Specify 0 to get a line break after each semi-colon in JavaScript, and
+ after each rule in CSS.
+
+ --type js|css
+ The type of compressor (JavaScript or CSS) is chosen based on the
+ extension of the input file name (.js or .css) This option is required
+ if no input file has been specified. Otherwise, this option is only
+ required if the input file extension is neither 'js' nor 'css'.
+
+ --charset character-set
+ If a supported character set is specified, the YUI Compressor will use it
+ to read the input file. Otherwise, it will assume that the platform's
+ default character set is being used. The output file is encoded using
+ the same character set.
+
+ -o outfile
+ Place output in file outfile. If not specified, the YUI Compressor will
+ default to the standard output, which you can redirect to a file.
+
+ -v, --verbose
+ Display informational messages and warnings.
+
+JAVASCRIPT ONLY OPTIONS
+
+ --nomunge
+ Minify only. Do not obfuscate local symbols.
+
+ --preserve-semi
+ Preserve unnecessary semicolons (such as right before a '}') This option
+ is useful when compressed code has to be run through JSLint (which is the
+ case of YUI for example)
+
+ --disable-optimizations
+ Disable all the built-in micro optimizations.
+
+NOTES
+
+ + If no input file is specified, it defaults to stdin.
+
+ + The YUI Compressor requires Java version >= 1.4.
+
+ + It is possible to prevent a local variable, nested function or function
+ argument from being obfuscated by using "hints". A hint is a string that
+ is located at the very beginning of a function body like so:
+
+ function fn (arg1, arg2, arg3) {
+ "arg2:nomunge, localVar:nomunge, nestedFn:nomunge";
+
+ ...
+ var localVar;
+ ...
+
+ function nestedFn () {
+ ....
+ }
+
+ ...
+ }
+
+ The hint itself disappears from the compressed file.
+
+ + C-style comments starting with /*! are preserved. This is useful with
+ comments containing copyright/license information. For example:
+
+ /*!
+ * TERMS OF USE - EASING EQUATIONS
+ * Open source under the BSD License.
+ * Copyright 2001 Robert Penner All rights reserved.
+ */
+
+ becomes:
+
+ /*
+ * TERMS OF USE - EASING EQUATIONS
+ * Open source under the BSD License.
+ * Copyright 2001 Robert Penner All rights reserved.
+ */
+
+AUTHOR
+
+ The YUI Compressor was written and is maintained by:
+ Julien Lecomte
+ The CSS portion is a port of Isaac Schlueter's cssmin utility.
+
+COPYRIGHT
+
+ Copyright (c) 2007-2009, Yahoo! Inc. All rights reserved.
+
+LICENSE
+
+ All code specific to YUI Compressor is issued under a BSD license.
+ YUI Compressor extends and implements code from Mozilla's Rhino project.
+ Rhino is issued under the Mozilla Public License (MPL), and MPL applies
+ to the Rhino source and binaries that are distributed with YUI Compressor.
\ No newline at end of file
diff --git a/util/yuicompressor/yuicompressor-2.4.2.jar b/util/yuicompressor/yuicompressor-2.4.2.jar
new file mode 100755
index 00000000..c29470bd
Binary files /dev/null and b/util/yuicompressor/yuicompressor-2.4.2.jar differ