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