diff --git a/.gitignore b/.gitignore index 5c53ca30..1ee61d4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ +.DS_Store default.properties gen assets/www/phonegap.js local.properties framework/phonegap.jar +framework/phonegap-*.jar framework/bin framework/assets/www/.DS_Store +framework/assets/www/phonegap-*.js +.DS_Store diff --git a/VERSION b/VERSION index 2bd77c74..8862dbae 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.4 \ No newline at end of file +1.0.0rc2 diff --git a/example/index.html b/example/index.html index 89e302ef..10a69d06 100644 --- a/example/index.html +++ b/example/index.html @@ -5,8 +5,8 @@ PhoneGap - - + + @@ -31,8 +31,8 @@ Get a Picture Get Phone's Contacts Check Network - diff --git a/example/main.js b/example/main.js index ae447aa9..110d3089 100644 --- a/example/main.js +++ b/example/main.js @@ -88,16 +88,6 @@ function close() { 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.' @@ -109,27 +99,24 @@ 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, {}); + var networkState = navigator.network.connection.type; + + var states = {}; + states[Connection.UNKNOWN] = 'Unknown connection'; + states[Connection.ETHERNET] = 'Ethernet connection'; + states[Connection.WIFI] = 'WiFi connection'; + states[Connection.CELL_2G] = 'Cell 2G connection'; + states[Connection.CELL_3G] = 'Cell 3G connection'; + states[Connection.CELL_4G] = 'Cell 4G connection'; + states[Connection.NONE] = 'No network connection'; + + confirm('Connection type:\n ' + states[networkState]); } function init() { diff --git a/framework/AndroidManifest.xml b/framework/AndroidManifest.xml index a97572ba..c6eeb949 100644 --- a/framework/AndroidManifest.xml +++ b/framework/AndroidManifest.xml @@ -5,34 +5,45 @@ android:largeScreens="true" android:normalScreens="true" android:smallScreens="true" + android:xlargeScreens="true" android:resizeable="true" android:anyDensity="true" /> + - + + + + + - + + + + - + diff --git a/framework/assets/js/accelerometer.js b/framework/assets/js/accelerometer.js index 2368b596..54af21dc 100755 --- a/framework/assets/js/accelerometer.js +++ b/framework/assets/js/accelerometer.js @@ -3,21 +3,25 @@ * 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 */ -function Acceleration(x, y, z) { +if (!PhoneGap.hasResource("accelerometer")) { +PhoneGap.addResource("accelerometer"); + +/** @constructor */ +var Acceleration = function(x, y, z) { this.x = x; this.y = y; this.z = z; this.timestamp = new Date().getTime(); -} +}; /** * This class provides access to device accelerometer data. * @constructor */ -function Accelerometer() { +var Accelerometer = function() { /** * The last known acceleration. type=Acceleration() @@ -28,7 +32,7 @@ function Accelerometer() { * List of accelerometer watch timers */ this.timers = {}; -} +}; Accelerometer.ERROR_MSG = ["Not running", "Starting", "", "Failed to start"]; @@ -119,3 +123,4 @@ PhoneGap.addConstructor(function() { navigator.accelerometer = new Accelerometer(); } }); +} diff --git a/framework/assets/js/app.js b/framework/assets/js/app.js index 5a45cdda..788e8187 100755 --- a/framework/assets/js/app.js +++ b/framework/assets/js/app.js @@ -6,10 +6,15 @@ * Copyright (c) 2010-2011, IBM Corporation */ +if (!PhoneGap.hasResource("app")) { +PhoneGap.addResource("app"); +(function() { + /** * Constructor + * @constructor */ -function App() {} +var App = function() {}; /** * Clear the resource cache. @@ -20,7 +25,7 @@ App.prototype.clearCache = function() { /** * 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 @@ -30,7 +35,7 @@ App.prototype.clearCache = function() { * 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}); @@ -54,23 +59,13 @@ 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) { @@ -85,5 +80,7 @@ App.prototype.exitApp = function() { }; PhoneGap.addConstructor(function() { - navigator.app = window.app = new App(); + navigator.app = new App(); }); +}()); +} diff --git a/framework/assets/js/camera.js b/framework/assets/js/camera.js index 1e4a75a6..1c4be96d 100755 --- a/framework/assets/js/camera.js +++ b/framework/assets/js/camera.js @@ -3,15 +3,18 @@ * 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 */ +if (!PhoneGap.hasResource("camera")) { +PhoneGap.addResource("camera"); + /** * This class provides access to the device camera. * * @constructor */ -Camera = function() { +var Camera = function() { this.successCallback = null; this.errorCallback = null; this.options = null; @@ -91,3 +94,4 @@ PhoneGap.addConstructor(function() { navigator.camera = new Camera(); } }); +} diff --git a/framework/assets/js/capture.js b/framework/assets/js/capture.js new file mode 100644 index 00000000..1c19357d --- /dev/null +++ b/framework/assets/js/capture.js @@ -0,0 +1,191 @@ +/* + * 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 + */ + +if (!PhoneGap.hasResource("capture")) { +PhoneGap.addResource("capture"); + +/** + * Represents a single file. + * + * name {DOMString} name of the file, without path information + * fullPath {DOMString} the full path of the file, including the name + * type {DOMString} mime type + * lastModifiedDate {Date} last modified date + * size {Number} size of the file in bytes + */ +var MediaFile = function(name, fullPath, type, lastModifiedDate, size){ + this.name = name || null; + this.fullPath = fullPath || null; + this.type = type || null; + this.lastModifiedDate = lastModifiedDate || null; + this.size = size || 0; +}; + +/** + * Launch device camera application for recording video(s). + * + * @param {Function} successCB + * @param {Function} errorCB + */ +MediaFile.prototype.getFormatData = function(successCallback, errorCallback){ + PhoneGap.exec(successCallback, errorCallback, "Capture", "getFormatData", [this.fullPath, this.type]); +}; + +/** + * MediaFileData encapsulates format information of a media file. + * + * @param {DOMString} codecs + * @param {long} bitrate + * @param {long} height + * @param {long} width + * @param {float} duration + */ +var MediaFileData = function(codecs, bitrate, height, width, duration){ + this.codecs = codecs || null; + this.bitrate = bitrate || 0; + this.height = height || 0; + this.width = width || 0; + this.duration = duration || 0; +}; + +/** + * The CaptureError interface encapsulates all errors in the Capture API. + */ +var CaptureError = function(){ + this.code = null; +}; + +// Capture error codes +CaptureError.CAPTURE_INTERNAL_ERR = 0; +CaptureError.CAPTURE_APPLICATION_BUSY = 1; +CaptureError.CAPTURE_INVALID_ARGUMENT = 2; +CaptureError.CAPTURE_NO_MEDIA_FILES = 3; +CaptureError.CAPTURE_NOT_SUPPORTED = 20; + +/** + * The Capture interface exposes an interface to the camera and microphone of the hosting device. + */ +var Capture = function(){ + this.supportedAudioModes = []; + this.supportedImageModes = []; + this.supportedVideoModes = []; +}; + +/** + * Launch audio recorder application for recording audio clip(s). + * + * @param {Function} successCB + * @param {Function} errorCB + * @param {CaptureAudioOptions} options + */ +Capture.prototype.captureAudio = function(successCallback, errorCallback, options){ + PhoneGap.exec(successCallback, errorCallback, "Capture", "captureAudio", [options]); +}; + +/** + * Launch camera application for taking image(s). + * + * @param {Function} successCB + * @param {Function} errorCB + * @param {CaptureImageOptions} options + */ +Capture.prototype.captureImage = function(successCallback, errorCallback, options){ + PhoneGap.exec(successCallback, errorCallback, "Capture", "captureImage", [options]); +}; + +/** + * Launch camera application for taking image(s). + * + * @param {Function} successCB + * @param {Function} errorCB + * @param {CaptureImageOptions} options + */ +Capture.prototype._castMediaFile = function(pluginResult){ + var mediaFiles = []; + var i; + for (i = 0; i < pluginResult.message.length; i++) { + var mediaFile = new MediaFile(); + mediaFile.name = pluginResult.message[i].name; + mediaFile.fullPath = pluginResult.message[i].fullPath; + mediaFile.type = pluginResult.message[i].type; + mediaFile.lastModifiedDate = pluginResult.message[i].lastModifiedDate; + mediaFile.size = pluginResult.message[i].size; + mediaFiles.push(mediaFile); + } + pluginResult.message = mediaFiles; + return pluginResult; +}; + +/** + * Launch device camera application for recording video(s). + * + * @param {Function} successCB + * @param {Function} errorCB + * @param {CaptureVideoOptions} options + */ +Capture.prototype.captureVideo = function(successCallback, errorCallback, options){ + PhoneGap.exec(successCallback, errorCallback, "Capture", "captureVideo", [options]); +}; + +/** + * Encapsulates a set of parameters that the capture device supports. + */ +var ConfigurationData = function(){ + // The ASCII-encoded string in lower case representing the media type. + this.type = null; + // The height attribute represents height of the image or video in pixels. + // In the case of a sound clip this attribute has value 0. + this.height = 0; + // The width attribute represents width of the image or video in pixels. + // In the case of a sound clip this attribute has value 0 + this.width = 0; +}; + +/** + * Encapsulates all image capture operation configuration options. + */ +var CaptureImageOptions = function(){ + // Upper limit of images user can take. Value must be equal or greater than 1. + this.limit = 1; + // The selected image mode. Must match with one of the elements in supportedImageModes array. + this.mode = null; +}; + +/** + * Encapsulates all video capture operation configuration options. + */ +var CaptureVideoOptions = function(){ + // Upper limit of videos user can record. Value must be equal or greater than 1. + this.limit = 1; + // Maximum duration of a single video clip in seconds. + this.duration = 0; + // The selected video mode. Must match with one of the elements in supportedVideoModes array. + this.mode = null; +}; + +/** + * Encapsulates all audio capture operation configuration options. + */ +var CaptureAudioOptions = function(){ + // Upper limit of sound clips user can record. Value must be equal or greater than 1. + this.limit = 1; + // Maximum duration of a single sound clip in seconds. + this.duration = 0; + // The selected audio mode. Must match with one of the elements in supportedAudioModes array. + this.mode = null; +}; + +PhoneGap.addConstructor(function(){ + if (typeof navigator.device === "undefined") { + navigator.device = window.device = new Device(); + } + if (typeof navigator.device.capture === "undefined") { + navigator.device.capture = window.device.capture = new Capture(); + } +}); +} \ No newline at end of file diff --git a/framework/assets/js/compass.js b/framework/assets/js/compass.js index ffb16463..09a3115f 100755 --- a/framework/assets/js/compass.js +++ b/framework/assets/js/compass.js @@ -3,14 +3,17 @@ * 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 */ +if (!PhoneGap.hasResource("compass")) { +PhoneGap.addResource("compass"); + /** * This class provides access to device Compass data. * @constructor */ -function Compass() { +var Compass = function() { /** * The last known Compass position. */ @@ -20,7 +23,7 @@ function Compass() { * List of compass watch timers */ this.timers = {}; -} +}; Compass.ERROR_MSG = ["Not running", "Starting", "", "Failed to start"]; @@ -113,3 +116,4 @@ PhoneGap.addConstructor(function() { navigator.compass = new Compass(); } }); +} diff --git a/framework/assets/js/contact.js b/framework/assets/js/contact.js index 67559c60..a0ddf8a0 100755 --- a/framework/assets/js/contact.js +++ b/framework/assets/js/contact.js @@ -3,31 +3,32 @@ * 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 */ +if (!PhoneGap.hasResource("contact")) { +PhoneGap.addResource("contact"); + /** * Contains information about a single contact. +* @constructor * @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} revision date contact was last updated +* @param {Array.} phoneNumbers array of phone numbers +* @param {Array.} emails array of email addresses +* @param {Array.} addresses array of addresses +* @param {Array.} ims instant messaging user ids +* @param {Array.} organizations * @param {DOMString} birthday contact's birthday -* @param {DOMString} gender contact's gender * @param {DOMString} note user notes about contact -* @param {ContactField[]} photos -* @param {ContactField[]} categories -* @param {ContactField[]} urls contact's web sites -* @param {DOMString} timezone the contacts time zone +* @param {Array.} photos +* @param {Array.} categories +* @param {Array.} urls contact's web sites */ var Contact = function (id, displayName, name, nickname, phoneNumbers, emails, addresses, - ims, organizations, revision, birthday, gender, note, photos, categories, urls, timezone) { + ims, organizations, birthday, note, photos, categories, urls) { this.id = id || null; this.rawId = null; this.displayName = displayName || null; @@ -38,19 +39,17 @@ var Contact = function (id, displayName, name, nickname, phoneNumbers, emails, a this.addresses = addresses || null; // ContactAddress[] this.ims = ims || null; // ContactField[] this.organizations = organizations || null; // ContactOrganization[] - this.revision = revision || null; this.birthday = birthday || null; - this.gender = gender || null; this.note = note || null; this.photos = photos || null; // ContactField[] this.categories = categories || null; // ContactField[] this.urls = urls || null; // ContactField[] - this.timezone = timezone || null; }; /** * ContactError. - * An error code assigned by an implementation when an error has occurred + * An error code assigned by an implementation when an error has occurreds + * @constructor */ var ContactError = function() { this.code=null; @@ -61,11 +60,10 @@ var ContactError = function() { */ ContactError.UNKNOWN_ERROR = 0; ContactError.INVALID_ARGUMENT_ERROR = 1; -ContactError.NOT_FOUND_ERROR = 2; -ContactError.TIMEOUT_ERROR = 3; -ContactError.PENDING_OPERATION_ERROR = 4; -ContactError.IO_ERROR = 5; -ContactError.NOT_SUPPORTED_ERROR = 6; +ContactError.TIMEOUT_ERROR = 2; +ContactError.PENDING_OPERATION_ERROR = 3; +ContactError.IO_ERROR = 4; +ContactError.NOT_SUPPORTED_ERROR = 5; ContactError.PERMISSION_DENIED_ERROR = 20; /** @@ -76,7 +74,7 @@ ContactError.PERMISSION_DENIED_ERROR = 20; Contact.prototype.remove = function(successCB, errorCB) { if (this.id === null) { var errorObj = new ContactError(); - errorObj.code = ContactError.NOT_FOUND_ERROR; + errorObj.code = ContactError.UNKNOWN_ERROR; errorCB(errorObj); } else { @@ -149,6 +147,7 @@ Contact.prototype.save = function(successCB, errorCB) { /** * Contact name. +* @constructor * @param formatted * @param familyName * @param givenName @@ -167,6 +166,7 @@ var ContactName = function(formatted, familyName, givenName, middle, prefix, suf /** * Generic contact field. +* @constructor * @param {DOMString} id unique identifier, should only be set by native code * @param type * @param value @@ -181,6 +181,7 @@ var ContactField = function(type, value, pref) { /** * Contact address. +* @constructor * @param {DOMString} id unique identifier, should only be set by native code * @param formatted * @param streetAddress @@ -189,8 +190,10 @@ var ContactField = function(type, value, pref) { * @param postalCode * @param country */ -var ContactAddress = function(formatted, streetAddress, locality, region, postalCode, country) { +var ContactAddress = function(pref, type, formatted, streetAddress, locality, region, postalCode, country) { this.id = null; + this.pref = pref || null; + this.type = type || null; this.formatted = formatted || null; this.streetAddress = streetAddress || null; this.locality = locality || null; @@ -201,6 +204,7 @@ var ContactAddress = function(formatted, streetAddress, locality, region, postal /** * Contact organization. +* @constructor * @param {DOMString} id unique identifier, should only be set by native code * @param name * @param dept @@ -210,8 +214,10 @@ var ContactAddress = function(formatted, streetAddress, locality, region, postal * @param location * @param desc */ -var ContactOrganization = function(name, dept, title) { +var ContactOrganization = function(pref, type, name, dept, title) { this.id = null; + this.pref = pref || null; + this.type = type || null; this.name = name || null; this.department = dept || null; this.title = title || null; @@ -219,6 +225,7 @@ var ContactOrganization = function(name, dept, title) { /** * Represents a group of Contacts. +* @constructor */ var Contacts = function() { this.inProgress = false; @@ -233,7 +240,16 @@ var Contacts = function() { * @return array of Contacts matching search criteria */ Contacts.prototype.find = function(fields, successCB, errorCB, options) { - PhoneGap.exec(successCB, errorCB, "Contacts", "search", [fields, options]); + if (successCB === null) { + throw new TypeError("You must specify a success callback for the find command."); + } + if (fields === null || fields === "undefined" || fields.length === "undefined" || fields.length <= 0) { + if (typeof errorCB === "function") { + errorCB({"code": ContactError.INVALID_ARGUMENT_ERROR}); + } + } else { + PhoneGap.exec(successCB, errorCB, "Contacts", "search", [fields, options]); + } }; /** @@ -255,10 +271,10 @@ Contacts.prototype.create = function(properties) { }; /** -* 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. -* +* 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.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 */ @@ -266,7 +282,7 @@ Contacts.prototype.cast = function(pluginResult) { var contacts = []; var i; for (i=0; i this.length) { this.position = this.length; @@ -553,12 +554,12 @@ FileWriter.prototype.seek = function(offset) { // 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) { @@ -633,169 +634,69 @@ FileWriter.prototype.truncate = function(size) { ); }; -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 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 (var i=0; i 0) { s = s + ","; } @@ -419,7 +464,7 @@ PhoneGap.stringify = function(args) { // don't copy the functions s = s + '""'; } else if (args[i][name] instanceof Object) { - s = s + this.stringify(args[i][name]); + s = s + PhoneGap.stringify(args[i][name]); } else { s = s + '"' + args[i][name] + '"'; } @@ -445,41 +490,41 @@ PhoneGap.stringify = function(args) { * Does a deep clone of the object. * * @param obj - * @return + * @return {Object} */ PhoneGap.clone = function(obj) { var i, retVal; - if(!obj) { - return obj; - } - - if(obj instanceof Array){ - retVal = []; - for(i = 0; i < obj.length; ++i){ - retVal.push(PhoneGap.clone(obj[i])); - } - return retVal; - } - - if (obj instanceof Function) { - return obj; - } - - if(!(obj instanceof Object)){ - return obj; - } - + if(!obj) { + return obj; + } + + if(obj instanceof Array){ + retVal = []; + for(i = 0; i < obj.length; ++i){ + retVal.push(PhoneGap.clone(obj[i])); + } + return retVal; + } + + if (typeof obj === "function") { + return obj; + } + + if(!(obj instanceof Object)){ + return obj; + } + if (obj instanceof Date) { return obj; } - retVal = {}; - for(i in obj){ - if(!(i in retVal) || retVal[i] !== obj[i]) { - retVal[i] = PhoneGap.clone(obj[i]); - } - } - return retVal; + retVal = {}; + for(i in obj){ + if(!(i in retVal) || retVal[i] !== obj[i]) { + retVal[i] = PhoneGap.clone(obj[i]); + } + } + return retVal; }; PhoneGap.callbackId = 0; @@ -499,7 +544,7 @@ PhoneGap.callbackStatus = { /** - * Execute a PhoneGap command. It is up to the native side whether this action is synch or async. + * Execute a PhoneGap command. It is up to the native side whether this action is synch or async. * The native side can return: * Synchronous: PluginResult object as a JSON string * Asynchrounous: Empty string "" @@ -510,7 +555,7 @@ PhoneGap.callbackStatus = { * @param {Function} fail The fail callback * @param {String} service The name of the service to use * @param {String} action Action to be run in PhoneGap - * @param {String[]} [args] Zero or more arguments to pass to the method + * @param {Array.} [args] Zero or more arguments to pass to the method */ PhoneGap.exec = function(success, fail, service, action, args) { try { @@ -518,13 +563,13 @@ PhoneGap.exec = function(success, fail, service, action, args) { if (success || fail) { PhoneGap.callbacks[callbackId] = {success:success, fail:fail}; } - - var r = prompt(this.stringify(args), "gap:"+this.stringify([service, action, callbackId, true])); - + + var r = prompt(PhoneGap.stringify(args), "gap:"+PhoneGap.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 === PhoneGap.callbackStatus.OK) { @@ -547,7 +592,7 @@ PhoneGap.exec = function(success, fail, service, action, args) { // 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]; @@ -600,7 +645,7 @@ PhoneGap.callbackSuccess = function(callbackId, args) { console.log("Error in success callback: "+callbackId+" = "+e); } } - + // Clear callback if not expecting any more results if (!args.keepCallback) { delete PhoneGap.callbacks[callbackId]; @@ -624,7 +669,7 @@ PhoneGap.callbackError = function(callbackId, args) { catch (e) { console.log("Error in error callback: "+callbackId+" = "+e); } - + // Clear callback if not expecting any more results if (!args.keepCallback) { delete PhoneGap.callbacks[callbackId]; @@ -690,12 +735,17 @@ PhoneGap.JSCallbackToken = null; /** * This is only for Android. * - * Internal function that uses XHR to call into PhoneGap Java code and retrieve + * Internal function that uses XHR 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.JSCallback = function() { + // Exit if shutting down app + if (PhoneGap.shuttingDown) { + return; + } + // If polling flag was changed, start using polling from now on if (PhoneGap.UsePolling) { PhoneGap.JSCallbackPolling(); @@ -708,10 +758,16 @@ PhoneGap.JSCallback = function() { xmlhttp.onreadystatechange=function(){ if(xmlhttp.readyState === 4){ + // Exit if shutting down app + if (PhoneGap.shuttingDown) { + return; + } + // If callback has JavaScript statement to execute if (xmlhttp.status === 200) { - var msg = xmlhttp.responseText; + // Need to url decode the response + var msg = decodeURIComponent(xmlhttp.responseText); setTimeout(function() { try { var t = eval(msg); @@ -745,13 +801,11 @@ PhoneGap.JSCallback = function() { console.log("JSCallback Error: Bad request. Stopping callbacks."); } - // If error, restart callback server + // If error, revert to polling else { console.log("JSCallback Error: Request failed."); - prompt("restartServer", "gap_callbackServer:"); - PhoneGap.JSCallbackPort = null; - PhoneGap.JSCallbackToken = null; - setTimeout(PhoneGap.JSCallback, 100); + PhoneGap.UsePolling = true; + PhoneGap.JSCallbackPolling(); } } }; @@ -780,12 +834,17 @@ 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 + * 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() { + // Exit if shutting down app + if (PhoneGap.shuttingDown) { + return; + } + // If polling flag was changed, stop using polling from now on if (!PhoneGap.UsePolling) { PhoneGap.JSCallback(); @@ -813,7 +872,7 @@ PhoneGap.JSCallbackPolling = function() { /** * Create a UUID * - * @return + * @return {String} */ PhoneGap.createUUID = function() { return PhoneGap.UUIDcreatePart(4) + '-' + @@ -855,7 +914,7 @@ PhoneGap.close = function(context, func, params) { * @param {Function} successCallback The callback to call when the file has been loaded. */ PhoneGap.includeJavascript = function(jsfile, successCallback) { - var id = document.getElementsByTagName("head")[0]; + var id = document.getElementsByTagName("head")[0]; var el = document.createElement('script'); el.type = 'text/javascript'; if (typeof successCallback === 'function') { @@ -864,3 +923,5 @@ PhoneGap.includeJavascript = function(jsfile, successCallback) { el.src = jsfile; id.appendChild(el); }; + +} diff --git a/framework/assets/js/position.js b/framework/assets/js/position.js index 94468a37..d6b4e613 100755 --- a/framework/assets/js/position.js +++ b/framework/assets/js/position.js @@ -3,9 +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 */ +if (!PhoneGap.hasResource("position")) { +PhoneGap.addResource("position"); + /** * This class contains position information. * @param {Object} lat @@ -17,12 +20,13 @@ * @param {Object} vel * @constructor */ -function Position(coords, timestamp) { +var Position = function(coords, timestamp) { this.coords = coords; this.timestamp = (timestamp !== 'undefined') ? timestamp : new Date().getTime(); -} +}; -function Coordinates(lat, lng, alt, acc, head, vel, altacc) { +/** @constructor */ +var Coordinates = function(lat, lng, alt, acc, head, vel, altacc) { /** * The latitude of the position. */ @@ -50,14 +54,14 @@ function Coordinates(lat, lng, alt, acc, head, vel, altacc) { /** * The altitude accuracy of the position. */ - this.altitudeAccuracy = (altacc !== 'undefined') ? altacc : null; -} + this.altitudeAccuracy = (altacc !== 'undefined') ? altacc : null; +}; /** * This class specifies the options for requesting position data. * @constructor */ -function PositionOptions() { +var PositionOptions = function() { /** * Specifies the desired position accuracy. */ @@ -67,18 +71,19 @@ function PositionOptions() { * is called. */ this.timeout = 10000; -} +}; /** * This class contains information about any GSP errors. * @constructor */ -function PositionError() { +var PositionError = function() { this.code = null; this.message = ""; -} +}; PositionError.UNKNOWN_ERROR = 0; PositionError.PERMISSION_DENIED = 1; PositionError.POSITION_UNAVAILABLE = 2; PositionError.TIMEOUT = 3; +} diff --git a/framework/assets/js/storage.js b/framework/assets/js/storage.js index b47bd670..794aba11 100755 --- a/framework/assets/js/storage.js +++ b/framework/assets/js/storage.js @@ -3,7 +3,7 @@ * 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 */ /* @@ -12,9 +12,42 @@ * most manufacturers ship with Android 1.5 and do not do OTA Updates, this is required */ +if (!PhoneGap.hasResource("storage")) { +PhoneGap.addResource("storage"); + +/** + * SQL result set object + * PRIVATE METHOD + * @constructor + */ +var DroidDB_Rows = function() { + this.resultSet = []; // results array + this.length = 0; // number of rows +}; + +/** + * 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]; +}; + +/** + * SQL result set that is returned to user. + * PRIVATE METHOD + * @constructor + */ +var DroidDB_Result = function() { + this.rows = new DroidDB_Rows(); +}; + /** * Storage object that is called by native code when performing queries. * PRIVATE METHOD + * @constructor */ var DroidDB = function() { this.queryQueue = {}; @@ -99,9 +132,40 @@ DroidDB.prototype.fail = function(reason, id) { } }; +/** + * SQL query object + * PRIVATE METHOD + * + * @constructor + * @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; + +}; + /** * Transaction object * PRIVATE METHOD + * @constructor */ var DroidDB_Tx = function() { @@ -116,37 +180,6 @@ var DroidDB_Tx = function() { this.queryList = {}; }; - -var DatabaseShell = function() { -}; - -/** - * 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, errorCallback, successCallback) { - 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); - } - } - } -}; - - /** * Mark query in transaction as complete. * If all queries are complete, call the user's transaction success callback. @@ -162,7 +195,7 @@ DroidDB_Tx.prototype.queryComplete = function(id) { var i; for (i in this.queryList) { if (this.queryList.hasOwnProperty(i)) { - count++; + count++; } } if (count === 0) { @@ -198,35 +231,6 @@ DroidDB_Tx.prototype.queryFailed = function(id, reason) { } }; -/** - * 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 * @@ -254,31 +258,33 @@ DroidDB_Tx.prototype.executeSql = function(sql, params, successCallback, errorCa PhoneGap.exec(null, null, "Storage", "executeSql", [sql, params, query.id]); }; -/** - * SQL result set that is returned to user. - * PRIVATE METHOD - */ -DroidDB_Result = function() { - this.rows = new DroidDB_Rows(); +var DatabaseShell = function() { }; /** - * SQL result set object - * PRIVATE METHOD - */ -DroidDB_Rows = function() { - this.resultSet = []; // results array - this.length = 0; // number of rows -}; - -/** - * Get item from SQL result set + * Start a transaction. + * Does not support rollback in event of failure. * - * @param row The row number to return - * @return The row object + * @param process {Function} The transaction function + * @param successCallback {Function} + * @param errorCallback {Function} */ -DroidDB_Rows.prototype.item = function(row) { - return this.resultSet[row]; +DatabaseShell.prototype.transaction = function(process, errorCallback, successCallback) { + 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); + } + } + } }; /** @@ -290,22 +296,24 @@ DroidDB_Rows.prototype.item = function(row) { * @param size Database size in bytes * @return Database object */ -DroidDB_openDatabase = function(name, version, display_name, size) { +var 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; }; - /** - * For browsers with no localStorage we emulate it with SQLite. Follows the w3c api. - * TODO: Do similar for sessionStorage. + * For browsers with no localStorage we emulate it with SQLite. Follows the w3c api. + * TODO: Do similar for sessionStorage. */ +/** + * @constructor + */ var CupcakeLocalStorage = function() { try { - this.db = openDatabase('localStorage', '1.0', 'localStorage', 2621440); + this.db = openDatabase('localStorage', '1.0', 'localStorage', 2621440); var storage = {}; this.length = 0; function setLength (length) { @@ -323,8 +331,8 @@ var CupcakeLocalStorage = function() { setLength(result.rows.length); PhoneGap.initializationComplete("cupcakeStorage"); }); - - }, + + }, function (err) { alert(err.message); } @@ -341,7 +349,7 @@ var CupcakeLocalStorage = function() { } ); }; - this.getItem = function(key) { + this.getItem = function(key) { return storage[key]; }; this.removeItem = function(key) { @@ -374,22 +382,47 @@ var CupcakeLocalStorage = function() { } } return null; - } + }; } catch(e) { alert("Database error "+e+"."); return; } }; + PhoneGap.addConstructor(function() { - if (typeof window.openDatabase === "undefined") { + var setupDroidDB = function() { navigator.openDatabase = window.openDatabase = DroidDB_openDatabase; window.droiddb = new DroidDB(); } + if (typeof window.openDatabase === "undefined") { + setupDroidDB(); + } else { + window.openDatabase_orig = window.openDatabase; + window.openDatabase = function(name, version, desc, size){ + // Some versions of Android will throw a SECURITY_ERR so we need + // to catch the exception and seutp our own DB handling. + var db = null; + try { + db = window.openDatabase_orig(name, version, desc, size); + } + catch (ex) { + db = null; + } + + if (db == null) { + setupDroidDB(); + return DroidDB_openDatabase(name, version, desc, size); + } + else { + return db; + } + } + } if (typeof window.localStorage === "undefined") { navigator.localStorage = window.localStorage = new CupcakeLocalStorage(); PhoneGap.waitForInitialization("cupcakeStorage"); } }); - +} diff --git a/framework/assets/www/index.html b/framework/assets/www/index.html index aa9d4f74..e742b835 100644 --- a/framework/assets/www/index.html +++ b/framework/assets/www/index.html @@ -1,7 +1,7 @@ - + diff --git a/framework/build.xml b/framework/build.xml index 1ad9bb75..d48e8a1f 100755 --- a/framework/build.xml +++ b/framework/build.xml @@ -40,6 +40,9 @@ This file is an integral part of the build system for your application and should be checked in in Version Control Systems. --> + + + " @@ -71,88 +74,57 @@ --> - - - - var alert=function(){},device={},Element={},debug={}; - - - - - - - - + + + + var alert=function(){},device={},Element={},debug={}; + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + - - - + - - - - - - - - - - + + + + @@ -160,14 +132,9 @@ - + diff --git a/framework/default.properties b/framework/default.properties index c5d5335e..aa2fcdf7 100644 --- a/framework/default.properties +++ b/framework/default.properties @@ -10,5 +10,5 @@ # Indicates whether an apk should be generated for each density. split.density=false # Project target. -target=android-8 +target=android-12 apk-configurations= diff --git a/framework/gen/com/phonegap/R.java b/framework/gen/com/phonegap/R.java deleted file mode 100644 index 7db0ceb3..00000000 --- a/framework/gen/com/phonegap/R.java +++ /dev/null @@ -1,27 +0,0 @@ -/* AUTO-GENERATED FILE. DO NOT MODIFY. - * - * This class was automatically generated by the - * aapt tool from the resource data it found. It - * should not be modified by hand. - */ - -package com.phonegap; - -public final class R { - public static final class attr { - } - public static final class drawable { - public static final int icon=0x7f020000; - public static final int splash=0x7f020001; - } - public static final class id { - public static final int appView=0x7f050000; - } - public static final class layout { - public static final int main=0x7f030000; - } - public static final class string { - public static final int app_name=0x7f040000; - public static final int go=0x7f040001; - } -} diff --git a/framework/res/xml/plugins.xml b/framework/res/xml/plugins.xml new file mode 100755 index 00000000..e62c959f --- /dev/null +++ b/framework/res/xml/plugins.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/framework/src/com/phonegap/App.java b/framework/src/com/phonegap/App.java index cb3342bc..d2a35fa9 100755 --- a/framework/src/com/phonegap/App.java +++ b/framework/src/com/phonegap/App.java @@ -12,6 +12,7 @@ import org.json.JSONException; import org.json.JSONObject; import com.phonegap.api.Plugin; import com.phonegap.api.PluginResult; +import java.util.HashMap; /** * This class exposes methods in DroidGap that can be called from JavaScript. @@ -42,9 +43,6 @@ public class App extends Plugin { } 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)); @@ -83,8 +81,11 @@ public class App extends Plugin { public void loadUrl(String url, JSONObject props) throws JSONException { System.out.println("App.loadUrl("+url+","+props+")"); int wait = 0; - + boolean usePhoneGap = true; + boolean clearPrev = false; + // If there are properties, then set them on the Activity + HashMap params = new HashMap(); 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); + try { + synchronized(this) { + this.wait(wait); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } } + ((DroidGap)this.ctx).showWebPage(url, usePhoneGap, clearPrev, params); } /** @@ -133,16 +145,6 @@ public class App extends Plugin { ((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. diff --git a/framework/src/com/phonegap/AudioHandler.java b/framework/src/com/phonegap/AudioHandler.java index 33d05805..9673b076 100755 --- a/framework/src/com/phonegap/AudioHandler.java +++ b/framework/src/com/phonegap/AudioHandler.java @@ -63,6 +63,9 @@ public class AudioHandler extends Plugin { else if (action.equals("startPlayingAudio")) { this.startPlayingAudio(args.getString(0), args.getString(1)); } + else if (action.equals("seekToAudio")) { + this.seekToAudio(args.getString(0), args.getInt(1)); + } else if (action.equals("pausePlayingAudio")) { this.pausePlayingAudio(args.getString(0)); } @@ -70,12 +73,12 @@ public class AudioHandler extends Plugin { this.stopPlayingAudio(args.getString(0)); } else if (action.equals("getCurrentPositionAudio")) { - long l = this.getCurrentPositionAudio(args.getString(0)); - return new PluginResult(status, l); + float f = this.getCurrentPositionAudio(args.getString(0)); + return new PluginResult(status, f); } else if (action.equals("getDurationAudio")) { - long l = this.getDurationAudio(args.getString(0), args.getString(1)); - return new PluginResult(status, l); + float f = this.getDurationAudio(args.getString(0), args.getString(1)); + return new PluginResult(status, f); } else if (action.equals("release")) { boolean b = this.release(args.getString(0)); @@ -181,6 +184,20 @@ public class AudioHandler extends Plugin { audio.startPlaying(file); } + /** + * Seek to a location. + * + * + * @param id The id of the audio player + * @param miliseconds int: number of milliseconds to skip 1000 = 1 second + */ + public void seekToAudio(String id, int milliseconds) { + AudioPlayer audio = this.players.get(id); + if (audio != null) { + audio.seekToPlaying(milliseconds); + } + } + /** * Pause playing. * @@ -213,10 +230,10 @@ public class AudioHandler extends Plugin { * @param id The id of the audio player * @return position in msec */ - public long getCurrentPositionAudio(String id) { + public float getCurrentPositionAudio(String id) { AudioPlayer audio = this.players.get(id); if (audio != null) { - return(audio.getCurrentPosition()); + return(audio.getCurrentPosition()/1000.0f); } return -1; } @@ -228,7 +245,7 @@ public class AudioHandler extends Plugin { * @param file The name of the audio file. * @return The duration in msec. */ - public long getDurationAudio(String id, String file) { + public float getDurationAudio(String id, String file) { // Get audio file AudioPlayer audio = this.players.get(id); diff --git a/framework/src/com/phonegap/AudioPlayer.java b/framework/src/com/phonegap/AudioPlayer.java index 849ca44d..8f84a90e 100755 --- a/framework/src/com/phonegap/AudioPlayer.java +++ b/framework/src/com/phonegap/AudioPlayer.java @@ -15,6 +15,7 @@ import android.media.MediaPlayer.OnErrorListener; import android.media.MediaRecorder; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnPreparedListener; +import android.os.Environment; /** * This class implements the audio playback and recording capabilities used by PhoneGap. @@ -53,7 +54,7 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On private String id; // The id of this player (used to identify Media object in JavaScript) private int state = MEDIA_NONE; // State of recording or playback private String audioFile = null; // File name to play or record to - private long duration = -1; // Duration of audio + private float duration = -1; // Duration of audio private MediaRecorder recorder = null; // Audio recording object private String tempFile = null; // Temporary recording file name @@ -70,10 +71,7 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On public AudioPlayer(AudioHandler handler, String id) { this.handler = handler; this.id = id; - - // YES, I know this is bad, but I can't do it the right way because Google didn't have the - // foresight to add android.os.environment.getExternalDataDirectory until Android 2.2 - this.tempFile = "/sdcard/tmprecording.mp3"; + this.tempFile = Environment.getExternalStorageDirectory().getAbsolutePath() + "/tmprecording.mp3"; } /** @@ -209,7 +207,7 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On this.mPlayer.prepare(); // Get duration - this.duration = this.mPlayer.getDuration(); + this.duration = getDurationInSeconds(); } } catch (Exception e) { @@ -233,6 +231,15 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On } } + /** + * Seek or jump to a new time in the track. + */ + public void seekToPlaying(int milliseconds) { + if (this.mPlayer != null) { + this.mPlayer.seekTo(milliseconds); + } + } + /** * Pause playing. */ @@ -310,7 +317,7 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On * -1=can't be determined * -2=not allowed */ - public long getDuration(String file) { + public float getDuration(String file) { // Can't get duration of recording if (this.recorder != null) { @@ -353,7 +360,7 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On } // Save off duration - this.duration = this.mPlayer.getDuration(); + this.duration = getDurationInSeconds(); this.prepareOnly = false; // Send status notification to JavaScript @@ -361,6 +368,15 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On } + /** + * By default Android returns the length of audio in mills but we want seconds + * + * @return length of clip in seconds + */ + private float getDurationInSeconds() { + return (this.mPlayer.getDuration() / 1000.0f); + } + /** * Callback to be invoked when there has been an error during an asynchronous operation * (other errors will throw exceptions at method call time). diff --git a/framework/src/com/phonegap/CallbackServer.java b/framework/src/com/phonegap/CallbackServer.java index 2fce8a1e..2c2a9c84 100755 --- a/framework/src/com/phonegap/CallbackServer.java +++ b/framework/src/com/phonegap/CallbackServer.java @@ -11,10 +11,14 @@ import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; import java.net.ServerSocket; import java.net.Socket; +import java.net.URLEncoder; import java.util.LinkedList; +import android.util.Log; + /** * 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 polling server with a list of JavaScript @@ -41,6 +45,8 @@ import java.util.LinkedList; */ public class CallbackServer implements Runnable { + private static final String LOG_TAG = "CallbackServer"; + /** * The list of JavaScript statements to be sent to JavaScript. */ @@ -221,7 +227,11 @@ public class CallbackServer implements Runnable { } else { //System.out.println("CallbackServer -- sending item"); - response = "HTTP/1.1 200 OK\r\n\r\n"+this.getJavascript(); + response = "HTTP/1.1 200 OK\r\n\r\n"; + String js = this.getJavascript(); + if (js != null) { + response += encode(js, "UTF-8"); + } } } else { @@ -317,4 +327,81 @@ public class CallbackServer implements Runnable { } } + /* The Following code has been modified from original implementation of URLEncoder */ + + /* start */ + + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + static final String digits = "0123456789ABCDEF"; + + /** + * This will encode the return value to JavaScript. We revert the encoding for + * common characters that don't require encoding to reduce the size of the string + * being passed to JavaScript. + * + * @param s to be encoded + * @param enc encoding type + * @return encoded string + */ + public static String encode(String s, String enc) throws UnsupportedEncodingException { + if (s == null || enc == null) { + throw new NullPointerException(); + } + // check for UnsupportedEncodingException + "".getBytes(enc); + + // Guess a bit bigger for encoded form + StringBuilder buf = new StringBuilder(s.length() + 16); + int start = -1; + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') + || (ch >= '0' && ch <= '9') + || " .-*_'(),<>=?@[]{}:~\"\\/;!".indexOf(ch) > -1) { + if (start >= 0) { + convert(s.substring(start, i), buf, enc); + start = -1; + } + if (ch != ' ') { + buf.append(ch); + } else { + buf.append(' '); + } + } else { + if (start < 0) { + start = i; + } + } + } + if (start >= 0) { + convert(s.substring(start, s.length()), buf, enc); + } + return buf.toString(); + } + + private static void convert(String s, StringBuilder buf, String enc) throws UnsupportedEncodingException { + byte[] bytes = s.getBytes(enc); + for (int j = 0; j < bytes.length; j++) { + buf.append('%'); + buf.append(digits.charAt((bytes[j] & 0xf0) >> 4)); + buf.append(digits.charAt(bytes[j] & 0xf)); + } + } + + /* end */ } diff --git a/framework/src/com/phonegap/CameraLauncher.java b/framework/src/com/phonegap/CameraLauncher.java index 24e72d7c..1ad4bac0 100755 --- a/framework/src/com/phonegap/CameraLauncher.java +++ b/framework/src/com/phonegap/CameraLauncher.java @@ -28,6 +28,7 @@ import android.graphics.Bitmap.CompressFormat; import android.net.Uri; import android.os.Environment; import android.provider.MediaStore; +import android.util.Log; /** * This class launches the camera view, allows the user to take a picture, closes the camera view, @@ -119,14 +120,14 @@ public class CameraLauncher extends Plugin { // Specify file so that large image is captured and returned // TODO: What if there isn't any external storage? - File photo = new File(Environment.getExternalStorageDirectory(), "Pic.jpg"); + File photo = new File(DirectoryManager.getTempDirectoryPath(ctx), "Pic.jpg"); intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photo)); this.imageUri = Uri.fromFile(photo); this.ctx.startActivityForResult((Plugin) this, intent, (CAMERA+1)*16 + returnType+1); } - - /** + + /** * Get image from photo library. * * @param quality Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality) @@ -161,12 +162,18 @@ public class CameraLauncher extends Plugin { // If CAMERA if (srcType == CAMERA) { - // If image available if (resultCode == Activity.RESULT_OK) { try { // Read in bitmap of captured image - Bitmap bitmap = android.provider.MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), imageUri); + Bitmap bitmap; + try { + bitmap = android.provider.MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), imageUri); + } catch (FileNotFoundException e) { + Uri uri = intent.getData(); + android.content.ContentResolver resolver = this.ctx.getContentResolver(); + bitmap = android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri)); + } // If sending base64 image back if (destType == DATA_URL) { diff --git a/framework/src/com/phonegap/Capture.java b/framework/src/com/phonegap/Capture.java new file mode 100644 index 00000000..b078e5df --- /dev/null +++ b/framework/src/com/phonegap/Capture.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) 2011, IBM Corporation + */ +package com.phonegap; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.app.Activity; +import android.content.ContentValues; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Environment; +import android.util.Log; + +import com.phonegap.api.Plugin; +import com.phonegap.api.PluginResult; + +public class Capture extends Plugin { + + private static final String _DATA = "_data"; // The column name where the file path is stored + private static final int CAPTURE_AUDIO = 0; // Constant for capture audio + private static final int CAPTURE_IMAGE = 1; // Constant for capture image + private static final int CAPTURE_VIDEO = 2; // Constant for capture video + private static final String LOG_TAG = "Capture"; + private String callbackId; // The ID of the callback to be invoked with our result + private long limit; // the number of pics/vids/clips to take + private double duration; // optional duration parameter for video recording + private JSONArray results; // The array of results to be returned to the user + private Uri imageUri; // Uri of captured image + + @Override + public PluginResult execute(String action, JSONArray args, String callbackId) { + this.callbackId = callbackId; + this.limit = 1; + this.duration = 0.0f; + this.results = new JSONArray(); + + JSONObject options = args.optJSONObject(0); + if (options != null) { + limit = options.optLong("limit", 1); + duration = options.optDouble("duration", 0.0f); + } + + if (action.equals("getFormatData")) { + try { + JSONObject obj = getFormatData(args.getString(0), args.getString(1)); + return new PluginResult(PluginResult.Status.OK, obj); + } catch (JSONException e) { + return new PluginResult(PluginResult.Status.ERROR); + } + } + else if (action.equals("captureAudio")) { + this.captureAudio(); + } + else if (action.equals("captureImage")) { + this.captureImage(); + } + else if (action.equals("captureVideo")) { + this.captureVideo(duration); + } + + PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT); + r.setKeepCallback(true); + return r; + } + + /** + * Provides the media data file data depending on it's mime type + * + * @param filePath path to the file + * @param mimeType of the file + * @return a MediaFileData object + */ + private JSONObject getFormatData(String filePath, String mimeType) { + JSONObject obj = new JSONObject(); + try { + // setup defaults + obj.put("height", 0); + obj.put("width", 0); + obj.put("bitrate", 0); + obj.put("duration", 0); + obj.put("codecs", ""); + + // If the mimeType isn't set the rest will fail + // so let's see if we can determine it. + if (mimeType == null || mimeType.equals("")) { + mimeType = FileUtils.getMimeType(filePath); + } + + if (mimeType.equals("image/jpeg") || filePath.endsWith(".jpg")) { + obj = getImageData(filePath, obj); + } + else if (filePath.endsWith("audio/3gpp")) { + obj = getAudioVideoData(filePath, obj, false); + } + else if (mimeType.equals("video/3gpp")) { + obj = getAudioVideoData(filePath, obj, true); + } + } + catch (JSONException e) { + Log.d(LOG_TAG, "Error: setting media file data object"); + } + return obj; + } + + /** + * Get the Image specific attributes + * + * @param filePath path to the file + * @param obj represents the Media File Data + * @return a JSONObject that represents the Media File Data + * @throws JSONException + */ + private JSONObject getImageData(String filePath, JSONObject obj) throws JSONException { + Bitmap bitmap = BitmapFactory.decodeFile(filePath); + obj.put("height", bitmap.getHeight()); + obj.put("width", bitmap.getWidth()); + return obj; + } + + /** + * Get the Image specific attributes + * + * @param filePath path to the file + * @param obj represents the Media File Data + * @param video if true get video attributes as well + * @return a JSONObject that represents the Media File Data + * @throws JSONException + */ + private JSONObject getAudioVideoData(String filePath, JSONObject obj, boolean video) throws JSONException { + MediaPlayer player = new MediaPlayer(); + try { + player.setDataSource(filePath); + player.prepare(); + obj.put("duration", player.getDuration()); + if (video) { + obj.put("height", player.getVideoHeight()); + obj.put("width", player.getVideoWidth()); + } + } + catch (IOException e) { + Log.d(LOG_TAG, "Error: loading video file"); + } + return obj; + } + + /** + * Sets up an intent to capture audio. Result handled by onActivityResult() + */ + private void captureAudio() { + Intent intent = new Intent(android.provider.MediaStore.Audio.Media.RECORD_SOUND_ACTION); + + this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_AUDIO); + } + + /** + * Sets up an intent to capture images. Result handled by onActivityResult() + */ + private void captureImage() { + Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); + + // Specify file so that large image is captured and returned + File photo = new File(DirectoryManager.getTempDirectoryPath(ctx), "Capture.jpg"); + intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photo)); + this.imageUri = Uri.fromFile(photo); + + this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_IMAGE); + } + + /** + * Sets up an intent to capture video. Result handled by onActivityResult() + */ + private void captureVideo(double duration) { + Intent intent = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE); + // Introduced in API 8 + //intent.putExtra(android.provider.MediaStore.EXTRA_DURATION_LIMIT, duration); + + this.ctx.startActivityForResult((Plugin) this, intent, CAPTURE_VIDEO); + } + + /** + * Called when the video view exits. + * + * @param requestCode The request code originally supplied to startActivityForResult(), + * allowing you to identify who this result came from. + * @param resultCode The integer result code returned by the child activity through its setResult(). + * @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). + * @throws JSONException + */ + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + + // Result received okay + if (resultCode == Activity.RESULT_OK) { + // An audio clip was requested + if (requestCode == CAPTURE_AUDIO) { + // Get the uri of the audio clip + Uri data = intent.getData(); + // create a file object from the uri + results.put(createMediaFile(data)); + + if (results.length() >= limit) { + // Send Uri back to JavaScript for listening to audio + this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId); + } else { + // still need to capture more audio clips + captureAudio(); + } + } else if (requestCode == CAPTURE_IMAGE) { + // For some reason if I try to do: + // Uri data = intent.getData(); + // It crashes in the emulator and on my phone with a null pointer exception + // To work around it I had to grab the code from CameraLauncher.java + try { + // Read in bitmap of captured image + Bitmap bitmap = android.provider.MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), imageUri); + + // Create entry in media store for image + // (Don't use insertImage() because it uses default compression setting of 50 - no way to change it) + ContentValues values = new ContentValues(); + values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); + Uri uri = null; + try { + uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + } catch (UnsupportedOperationException e) { + System.out.println("Can't write to external media storage."); + try { + uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values); + } catch (UnsupportedOperationException ex) { + System.out.println("Can't write to internal media storage."); + this.fail("Error capturing image - no media storage found."); + return; + } + } + + // Add compressed version of captured image to returned media store Uri + OutputStream os = this.ctx.getContentResolver().openOutputStream(uri); + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os); + os.close(); + + bitmap.recycle(); + bitmap = null; + System.gc(); + + // Add image to results + results.put(createMediaFile(uri)); + + if (results.length() >= limit) { + // Send Uri back to JavaScript for viewing image + this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId); + } else { + // still need to capture more images + captureImage(); + } + } catch (IOException e) { + e.printStackTrace(); + this.fail("Error capturing image."); + } + } else if (requestCode == CAPTURE_VIDEO) { + // Get the uri of the video clip + Uri data = intent.getData(); + // create a file object from the uri + results.put(createMediaFile(data)); + + if (results.length() >= limit) { + // Send Uri back to JavaScript for viewing video + this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId); + } else { + // still need to capture more video clips + captureVideo(duration); + } + } + } + // If canceled + else if (resultCode == Activity.RESULT_CANCELED) { + // If we have partial results send them back to the user + if (results.length() > 0) { + this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId); + } + // user canceled the action + else { + this.fail("Canceled."); + } + } + // If something else + else { + // If we have partial results send them back to the user + if (results.length() > 0) { + this.success(new PluginResult(PluginResult.Status.OK, results, "navigator.device.capture._castMediaFile"), this.callbackId); + } + // something bad happened + else { + this.fail("Did not complete!"); + } + } + } + + /** + * Creates a JSONObject that represents a File from the Uri + * + * @param data the Uri of the audio/image/video + * @return a JSONObject that represents a File + */ + private JSONObject createMediaFile(Uri data) { + File fp = new File(getRealPathFromURI(data)); + + JSONObject obj = new JSONObject(); + + try { + // File properties + obj.put("name", fp.getName()); + obj.put("fullPath", fp.getAbsolutePath()); + obj.put("type", FileUtils.getMimeType(fp.getAbsolutePath())); + obj.put("lastModifiedDate", fp.lastModified()); + obj.put("size", fp.length()); + } catch (JSONException e) { + // this will never happen + e.printStackTrace(); + } + + return obj; + } + + /** + * Queries the media store to find out what the file path is for the Uri we supply + * + * @param contentUri the Uri of the audio/image/video + * @return the full path to the file + */ + private String getRealPathFromURI(Uri contentUri) { + String[] proj = { _DATA }; + Cursor cursor = this.ctx.managedQuery(contentUri, proj, null, null, null); + int column_index = cursor.getColumnIndexOrThrow(_DATA); + cursor.moveToFirst(); + return cursor.getString(column_index); + } + + /** + * Send error message to JavaScript. + * + * @param err + */ + public void fail(String err) { + this.error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId); + } +} \ No newline at end of file diff --git a/framework/src/com/phonegap/ContactAccessor.java b/framework/src/com/phonegap/ContactAccessor.java index 1cc49644..d17528ac 100644 --- a/framework/src/com/phonegap/ContactAccessor.java +++ b/framework/src/com/phonegap/ContactAccessor.java @@ -24,7 +24,6 @@ package com.phonegap; -import java.lang.reflect.Constructor; import java.util.HashMap; import android.app.Activity; @@ -44,53 +43,9 @@ import org.json.JSONObject; */ public abstract class ContactAccessor { - /** - * Static singleton instance of {@link ContactAccessor} holding the - * SDK-specific implementation of the class. - */ - private static ContactAccessor sInstance; protected final String LOG_TAG = "ContactsAccessor"; protected Activity mApp; protected WebView mView; - - public static ContactAccessor getInstance(WebView view, Activity app) { - if (sInstance == null) { - String className; - - /* - * Check the version of the SDK we are running on. Choose an - * implementation class designed for that version of the SDK. - * - * Unfortunately we have to use strings to represent the class - * names. If we used the conventional ContactAccessorSdk5.class.getName() - * syntax, we would get a ClassNotFoundException at runtime on pre-Eclair SDKs. - * Using the above syntax would force Dalvik to load the class and try to - * resolve references to all other classes it uses. Since the pre-Eclair - * does not have those classes, the loading of ContactAccessorSdk5 would fail. - */ - - if (android.os.Build.VERSION.RELEASE.startsWith("1.")) { - className = "com.phonegap.ContactAccessorSdk3_4"; - } else { - className = "com.phonegap.ContactAccessorSdk5"; - } - - /* - * Find the required class by name and instantiate it. - */ - try { - Class clazz = - Class.forName(className).asSubclass(ContactAccessor.class); - // Grab constructor for contactsmanager class dynamically. - Constructor classConstructor = clazz.getConstructor(Class.forName("android.webkit.WebView"), Class.forName("android.app.Activity")); - sInstance = classConstructor.newInstance(view, app); - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - - return sInstance; - } /** * Check to see if the data associated with the key is required to @@ -114,51 +69,65 @@ public abstract class ContactAccessor { String key; try { - for (int i=0; i - * There are several reasons why we wouldn't want to use this class on an Eclair device: - *
    - *
  • It would see at most one account, namely the first Google account created on the device. - *
  • It would work through a compatibility layer, which would make it inherently less efficient. - *
  • Not relevant to this particular example, but it would not have access to new kinds - * of data available through current APIs. - *
- */ -@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. - */ - private static final Map dbMap = new HashMap(); - static { - dbMap.put("id", People._ID); - dbMap.put("displayName", People.DISPLAY_NAME); - dbMap.put("phoneNumbers", Phones.NUMBER); - dbMap.put("phoneNumbers.value", Phones.NUMBER); - dbMap.put("emails", ContactMethods.DATA); - dbMap.put("emails.value", ContactMethods.DATA); - dbMap.put("addresses", ContactMethodsColumns.DATA); - dbMap.put("addresses.formatted", ContactMethodsColumns.DATA); - dbMap.put("ims", ContactMethodsColumns.DATA); - dbMap.put("ims.value", ContactMethodsColumns.DATA); - dbMap.put("organizations", Organizations.COMPANY); - dbMap.put("organizations.name", Organizations.COMPANY); - dbMap.put("organizations.title", Organizations.TITLE); - dbMap.put("note", People.NOTES); - } - - /** - * Create an contact accessor. - */ - public ContactAccessorSdk3_4(WebView view, Activity app) - { - mApp = app; - mView = view; - } - - @Override - /** - * This method takes the fields required and search options in order to produce an - * array of contacts that matches the criteria provided. - * @param fields an array of items to be used as search criteria - * @param options that can be applied to contact searching - * @return an array of contacts - */ - public JSONArray search(JSONArray fields, JSONObject options) { - String searchTerm = ""; - int limit = Integer.MAX_VALUE; - boolean multiple = true; - - if (options != null) { - searchTerm = options.optString("filter"); - if (searchTerm.length()==0) { - searchTerm = "%"; - } - else { - searchTerm = "%" + searchTerm + "%"; - } - try { - multiple = options.getBoolean("multiple"); - if (!multiple) { - limit = 1; - } - } catch (JSONException e) { - // Multiple was not specified so we assume the default is true. - } - } - else { - searchTerm = "%"; - } - - ContentResolver cr = mApp.getContentResolver(); - - Set contactIds = buildSetOfContactIds(fields, searchTerm); - HashMap populate = buildPopulationSet(fields); - - Iterator it = contactIds.iterator(); - - JSONArray contacts = new JSONArray(); - JSONObject contact; - String contactId; - int pos = 0; - while (it.hasNext() && (pos < limit)) { - contact = new JSONObject(); - try { - contactId = it.next(); - contact.put("id", contactId); - - // Do query for name and note - Cursor cur = cr.query(People.CONTENT_URI, - new String[] {People.DISPLAY_NAME, People.NOTES}, - PEOPLE_ID_EQUALS, - new String[] {contactId}, - null); - cur.moveToFirst(); - - if (isRequired("displayName",populate)) { - contact.put("displayName", cur.getString(cur.getColumnIndex(People.DISPLAY_NAME))); - } - if (isRequired("phoneNumbers",populate)) { - contact.put("phoneNumbers", phoneQuery(cr, contactId)); - } - if (isRequired("emails",populate)) { - contact.put("emails", emailQuery(cr, contactId)); - } - if (isRequired("addresses",populate)) { - contact.put("addresses", addressQuery(cr, contactId)); - } - if (isRequired("organizations",populate)) { - contact.put("organizations", organizationQuery(cr, contactId)); - } - if (isRequired("ims",populate)) { - contact.put("ims", imQuery(cr, contactId)); - } - if (isRequired("note",populate)) { - contact.put("note", cur.getString(cur.getColumnIndex(People.NOTES))); - } - // nickname - // urls - // relationship - // birthdays - // anniversary - - pos++; - cur.close(); - } catch (JSONException e) { - Log.e(LOG_TAG, e.getMessage(), e); - } - contacts.put(contact); - } - return contacts; - } - - /** - * Query the database using the search term to build up a list of contact ID's - * matching the search term - * @param fields - * @param searchTerm - * @return a set of contact ID's - */ - private Set buildSetOfContactIds(JSONArray fields, String searchTerm) { - Set contactIds = new HashSet(); - - String key; - try { - for (int i=0; i contactIds, - Uri uri, String projection, String selection, String[] selectionArgs) { - ContentResolver cr = mApp.getContentResolver(); - - Cursor cursor = cr.query( - uri, - null, - selection, - selectionArgs, - null); - - while (cursor.moveToNext()) { - contactIds.add(cursor.getString(cursor.getColumnIndex(projection))); - } - cursor.close(); - } - - /** - * Create a ContactField JSONArray - * @param cr database access object - * @param contactId the ID to search the database for - * @return a JSONArray representing a set of ContactFields - */ - private JSONArray imQuery(ContentResolver cr, String contactId) { - String imWhere = ContactMethods.PERSON_ID - + " = ? AND " + ContactMethods.KIND + " = ?"; - String[] imWhereParams = new String[]{contactId, ContactMethods.CONTENT_IM_ITEM_TYPE}; - Cursor cursor = cr.query(ContactMethods.CONTENT_URI, - null, imWhere, imWhereParams, null); - JSONArray ims = new JSONArray(); - JSONObject im; - while (cursor.moveToNext()) { - im = new JSONObject(); - try{ - 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", getContactType(cursor.getInt( - cursor.getColumnIndex(ContactMethodsColumns.TYPE)))); - ims.put(im); - } catch (JSONException e) { - Log.e(LOG_TAG, e.getMessage(), e); - } - } - cursor.close(); - return null; - } - - /** - * Create a ContactOrganization JSONArray - * @param cr database access object - * @param contactId the ID to search the database for - * @return a JSONArray representing a set of ContactOrganization - */ - private JSONArray organizationQuery(ContentResolver cr, String contactId) { - String orgWhere = ContactMethods.PERSON_ID + " = ?"; - String[] orgWhereParams = new String[]{contactId}; - Cursor cursor = cr.query(Organizations.CONTENT_URI, - null, orgWhere, orgWhereParams, null); - JSONArray organizations = new JSONArray(); - JSONObject organization; - 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))); - organizations.put(organization); - } catch (JSONException e) { - Log.e(LOG_TAG, e.getMessage(), e); - } - } - return organizations; - } - - /** - * Create a ContactAddress JSONArray - * @param cr database access object - * @param contactId the ID to search the database for - * @return a JSONArray representing a set of ContactAddress - */ - private JSONArray addressQuery(ContentResolver cr, String contactId) { - String addrWhere = ContactMethods.PERSON_ID - + " = ? AND " + ContactMethods.KIND + " = ?"; - String[] addrWhereParams = new String[]{contactId, - ContactMethods.CONTENT_POSTAL_ITEM_TYPE}; - Cursor cursor = cr.query(ContactMethods.CONTENT_URI, - null, addrWhere, addrWhereParams, null); - JSONArray addresses = new JSONArray(); - JSONObject address; - 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) { - Log.e(LOG_TAG, e.getMessage(), e); - } - } - return addresses; - } - - /** - * Create a ContactField JSONArray - * @param cr database access object - * @param contactId the ID to search the database for - * @return a JSONArray representing a set of ContactFields - */ - private JSONArray phoneQuery(ContentResolver cr, String contactId) { - Cursor cursor = cr.query( - Phones.CONTENT_URI, - null, - Phones.PERSON_ID +" = ?", - new String[]{contactId}, null); - JSONArray phones = new JSONArray(); - JSONObject phone; - while (cursor.moveToNext()) { - phone = new JSONObject(); - try{ - 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", getPhoneType(cursor.getInt(cursor.getColumnIndex(Phones.TYPE)))); - phones.put(phone); - } catch (JSONException e) { - Log.e(LOG_TAG, e.getMessage(), e); - } - } - return phones; - } - - /** - * Create a ContactField JSONArray - * @param cr database access object - * @param contactId the ID to search the database for - * @return a JSONArray representing a set of ContactFields - */ - private JSONArray emailQuery(ContentResolver cr, String contactId) { - Cursor cursor = cr.query( - ContactMethods.CONTENT_EMAIL_URI, - null, - ContactMethods.PERSON_ID +" = ?", - new String[]{contactId}, null); - JSONArray emails = new JSONArray(); - JSONObject email; - while (cursor.moveToNext()) { - email = new JSONObject(); - try{ - 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))); - emails.put(email); - } catch (JSONException e) { - Log.e(LOG_TAG, e.getMessage(), e); - } - } - 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 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; - } -} \ No newline at end of file diff --git a/framework/src/com/phonegap/ContactAccessorSdk5.java b/framework/src/com/phonegap/ContactAccessorSdk5.java index a1e7d428..bcb25227 100644 --- a/framework/src/com/phonegap/ContactAccessorSdk5.java +++ b/framework/src/com/phonegap/ContactAccessorSdk5.java @@ -46,6 +46,7 @@ import android.accounts.Account; import android.accounts.AccountManager; import android.app.Activity; import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; import android.content.ContentUris; import android.content.ContentValues; import android.content.OperationApplicationException; @@ -88,7 +89,7 @@ public class ContactAccessorSdk5 extends ContactAccessor { */ private static final Map dbMap = new HashMap(); static { - dbMap.put("id", ContactsContract.Contacts._ID); + dbMap.put("id", ContactsContract.Data.CONTACT_ID); dbMap.put("displayName", ContactsContract.Contacts.DISPLAY_NAME); dbMap.put("name", ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); dbMap.put("name.formatted", ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME); @@ -115,14 +116,12 @@ 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("revision", null); dbMap.put("birthday", ContactsContract.CommonDataKinds.Event.CONTENT_ITEM_TYPE); dbMap.put("note", ContactsContract.CommonDataKinds.Note.NOTE); 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("timezone", null); } /** @@ -142,9 +141,6 @@ public class ContactAccessorSdk5 extends ContactAccessor { */ @Override public JSONArray search(JSONArray fields, JSONObject options) { - long totalEnd; - long totalStart = System.currentTimeMillis(); - // Get the find options String searchTerm = ""; int limit = Integer.MAX_VALUE; @@ -180,7 +176,7 @@ 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 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 }, @@ -206,8 +202,49 @@ public class ContactAccessorSdk5 extends ContactAccessor { idOptions.getWhereArgs(), ContactsContract.Data.CONTACT_ID + " ASC"); - - //Log.d(LOG_TAG, "Cursor length = " + c.getCount()); + JSONArray contacts = populateContactArray(limit, populate, c); + return contacts; + } + + /** + * A special search that finds one contact by id + * + * @param id contact to find by id + * @return a JSONObject representing the contact + * @throws JSONException + */ + public JSONObject getContactById(String id) throws JSONException { + // Do the id query + Cursor c = mApp.getContentResolver().query(ContactsContract.Data.CONTENT_URI, + null, + ContactsContract.Data.CONTACT_ID + " = ? ", + new String[] { id }, + ContactsContract.Data.CONTACT_ID + " ASC"); + + JSONArray fields = new JSONArray(); + fields.put("*"); + + HashMap populate = buildPopulationSet(fields); + + JSONArray contacts = populateContactArray(1, populate, c); + + if (contacts.length() == 1) { + return contacts.getJSONObject(0); + } else { + return null; + } + } + + /** + * Creates an array of contacts from the cursor you pass in + * + * @param limit max number of contacts for the array + * @param populate whether or not you should populate a certain value + * @param c the cursor + * @return a JSONArray of contacts + */ + private JSONArray populateContactArray(int limit, + HashMap populate, Cursor c) { String contactId = ""; String rawId = ""; @@ -264,16 +301,18 @@ public class ContactAccessorSdk5 extends ContactAccessor { 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)); - } + if (mimetype.equals(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) { + contact.put("displayName", c.getString(c.getColumnIndex(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME))); + } + 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)); @@ -332,12 +371,8 @@ public class ContactAccessorSdk5 extends ContactAccessor { } } c.close(); - - - totalEnd = System.currentTimeMillis(); - Log.d(LOG_TAG,"Total time = " + (totalEnd-totalStart)); - return contacts; - } + return contacts; + } /** * Builds a where clause all all the ids passed into the method @@ -416,15 +451,67 @@ public class ContactAccessorSdk5 extends ContactAccessor { ArrayList whereArgs = new ArrayList(); WhereOptions options = new WhereOptions(); + + /* + * Special case where the user wants all fields returned + */ + if (isWildCardSearch(fields)) { + // Get all contacts with all properties + if ("%".equals(searchTerm)) { + options.setWhere("(" + ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? )"); + options.setWhereArgs(new String[] {searchTerm}); + return options; + } else { + // Get all contacts that match the filter but return all properties + where.add("(" + dbMap.get("displayName") + " LIKE ? )"); + whereArgs.add(searchTerm); + where.add("(" + dbMap.get("name") + " LIKE ? AND " + + ContactsContract.Data.MIMETYPE + " = ? )"); + whereArgs.add(searchTerm); + whereArgs.add(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); + where.add("(" + dbMap.get("nickname") + " LIKE ? AND " + + ContactsContract.Data.MIMETYPE + " = ? )"); + whereArgs.add(searchTerm); + whereArgs.add(ContactsContract.CommonDataKinds.Nickname.CONTENT_ITEM_TYPE); + where.add("(" + dbMap.get("phoneNumbers") + " LIKE ? AND " + + ContactsContract.Data.MIMETYPE + " = ? )"); + whereArgs.add(searchTerm); + whereArgs.add(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE); + where.add("(" + dbMap.get("emails") + " LIKE ? AND " + + ContactsContract.Data.MIMETYPE + " = ? )"); + whereArgs.add(searchTerm); + whereArgs.add(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE); + where.add("(" + dbMap.get("addresses") + " LIKE ? AND " + + ContactsContract.Data.MIMETYPE + " = ? )"); + whereArgs.add(searchTerm); + whereArgs.add(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE); + where.add("(" + dbMap.get("ims") + " LIKE ? AND " + + ContactsContract.Data.MIMETYPE + " = ? )"); + whereArgs.add(searchTerm); + whereArgs.add(ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE); + where.add("(" + dbMap.get("organizations") + " LIKE ? AND " + + ContactsContract.Data.MIMETYPE + " = ? )"); + whereArgs.add(searchTerm); + whereArgs.add(ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE); + where.add("(" + dbMap.get("note") + " LIKE ? AND " + + ContactsContract.Data.MIMETYPE + " = ? )"); + whereArgs.add(searchTerm); + whereArgs.add(ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE); + where.add("(" + dbMap.get("urls") + " LIKE ? AND " + + ContactsContract.Data.MIMETYPE + " = ? )"); + whereArgs.add(searchTerm); + whereArgs.add(ContactsContract.CommonDataKinds.Website.CONTENT_ITEM_TYPE); + } + } /* - * Special case for when the user wants all the contacts + * Special case for when the user wants all the contacts but */ if ("%".equals(searchTerm)) { options.setWhere("(" + ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? )"); options.setWhereArgs(new String[] {searchTerm}); return options; - } + } String key; try { @@ -432,10 +519,14 @@ public class ContactAccessorSdk5 extends ContactAccessor { for (int i=0; i ops = new ArrayList(); @@ -1420,7 +1547,6 @@ public class ContactAccessorSdk5 extends ContactAccessor { .build()); } - // Add urls JSONArray websites = null; try { @@ -1462,19 +1588,21 @@ public class ContactAccessorSdk5 extends ContactAccessor { Log.d(LOG_TAG, "Could not get photos"); } - boolean retVal = true; + String newId = null; //Add contact try { - mApp.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); + ContentProviderResult[] cpResults = mApp.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); + if (cpResults.length >= 0) { + newId = cpResults[0].uri.getLastPathSegment(); + } } catch (RemoteException e) { Log.e(LOG_TAG, e.getMessage(), e); - retVal = false; + newId = null; } catch (OperationApplicationException e) { Log.e(LOG_TAG, e.getMessage(), e); - retVal = false; + newId = null; } - - return retVal; + return newId; } @Override @@ -1645,58 +1773,144 @@ public class ContactAccessorSdk5 extends ContactAccessor { 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; - } + /** + * 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; - } + /** + * 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; + } + + /** + * Converts a string from the W3C Contact API to it's Android int value. + * @param string + * @return Android int value + */ + private int getOrgType(String string) { + int type = ContactsContract.CommonDataKinds.Organization.TYPE_OTHER; + if (string!=null) { + if ("work".equals(string.toLowerCase())) { + return ContactsContract.CommonDataKinds.Organization.TYPE_WORK; + } + else if ("other".equals(string.toLowerCase())) { + return ContactsContract.CommonDataKinds.Organization.TYPE_OTHER; + } + else if ("custom".equals(string.toLowerCase())) { + return ContactsContract.CommonDataKinds.Organization.TYPE_CUSTOM; + } + } + return type; + } + + /** + * getPhoneType converts an Android phone type into a string + * @param type + * @return phone type as string. + */ + private String getOrgType(int type) { + String stringType; + switch (type) { + case ContactsContract.CommonDataKinds.Organization.TYPE_CUSTOM: + stringType = "custom"; + break; + case ContactsContract.CommonDataKinds.Organization.TYPE_WORK: + stringType = "work"; + break; + case ContactsContract.CommonDataKinds.Organization.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 getAddressType(String string) { + int type = ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER; + if (string!=null) { + if ("work".equals(string.toLowerCase())) { + return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK; + } + else if ("other".equals(string.toLowerCase())) { + return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_OTHER; + } + else if ("home".equals(string.toLowerCase())) { + return ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME; + } + } + return type; + } + + /** + * getPhoneType converts an Android phone type into a string + * @param type + * @return phone type as string. + */ + private String getAddressType(int type) { + String stringType; + switch (type) { + case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_HOME: + stringType = "home"; + break; + case ContactsContract.CommonDataKinds.StructuredPostal.TYPE_WORK: + stringType = "work"; + break; + case ContactsContract.CommonDataKinds.StructuredPostal.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 5a79c37a..6e41f0f2 100755 --- a/framework/src/com/phonegap/ContactManager.java +++ b/framework/src/com/phonegap/ContactManager.java @@ -16,9 +16,18 @@ import android.util.Log; public class ContactManager extends Plugin { - private static ContactAccessor contactAccessor; + private ContactAccessor contactAccessor; private static final String LOG_TAG = "Contact Query"; + public static final int UNKNOWN_ERROR = 0; + public static final int INVALID_ARGUMENT_ERROR = 1; + public static final int TIMEOUT_ERROR = 2; + public static final int PENDING_OPERATION_ERROR = 3; + public static final int IO_ERROR = 4; + public static final int NOT_SUPPORTED_ERROR = 5; + public static final int PERMISSION_DENIED_ERROR = 20; + + /** * Constructor. */ @@ -34,31 +43,57 @@ public class ContactManager extends Plugin { * @return A PluginResult object with a status and message. */ public PluginResult execute(String action, JSONArray args, String callbackId) { - if (contactAccessor == null) { - contactAccessor = ContactAccessor.getInstance(webView, ctx); - } - PluginResult.Status status = PluginResult.Status.OK; - String result = ""; - + PluginResult.Status status = PluginResult.Status.OK; + String result = ""; + + /** + * Check to see if we are on an Android 1.X device. If we are return an error as we + * do not support this as of PhoneGap 1.0. + */ + if (android.os.Build.VERSION.RELEASE.startsWith("1.")) { + JSONObject res = null; + try { + res = new JSONObject(); + res.put("code", NOT_SUPPORTED_ERROR); + res.put("message", "Contacts are not supported in Android 1.X devices"); + } catch (JSONException e) { + // This should never happen + Log.e(LOG_TAG, e.getMessage(), e); + } + return new PluginResult(PluginResult.Status.ERROR, res); + } + + /** + * Only create the contactAccessor after we check the Android version or the program will crash + * older phones. + */ + if (this.contactAccessor == null) { + this.contactAccessor = new ContactAccessorSdk5(this.webView, this.ctx); + } + try { if (action.equals("search")) { JSONArray res = contactAccessor.search(args.getJSONArray(0), args.optJSONObject(1)); - return new PluginResult(status, res, "navigator.service.contacts.cast"); + return new PluginResult(status, res, "navigator.contacts.cast"); } else if (action.equals("save")) { - return new PluginResult(status, contactAccessor.save(args.getJSONObject(0))); + String id = contactAccessor.save(args.getJSONObject(0)); + if (id != null) { + JSONObject res = contactAccessor.getContactById(id); + if (res != null) { + return new PluginResult(status, res); + } + } } else if (action.equals("remove")) { if (contactAccessor.remove(args.getString(0))) { return new PluginResult(status, result); } - else { - JSONObject r = new JSONObject(); - r.put("code", 2); - return new PluginResult(PluginResult.Status.ERROR, r); - } } - return new PluginResult(status, result); + // If we get to this point an error has occurred + JSONObject r = new JSONObject(); + r.put("code", UNKNOWN_ERROR); + return new PluginResult(PluginResult.Status.ERROR, r); } catch (JSONException e) { Log.e(LOG_TAG, e.getMessage(), e); return new PluginResult(PluginResult.Status.JSON_EXCEPTION); diff --git a/framework/src/com/phonegap/Device.java b/framework/src/com/phonegap/Device.java old mode 100755 new mode 100644 index 049fc3ca..60858bc9 --- a/framework/src/com/phonegap/Device.java +++ b/framework/src/com/phonegap/Device.java @@ -14,13 +14,11 @@ import org.json.JSONObject; import com.phonegap.api.PhonegapActivity; import com.phonegap.api.Plugin; import com.phonegap.api.PluginResult; -import android.content.Context; import android.provider.Settings; -import android.telephony.TelephonyManager; public class Device extends Plugin { - public static String phonegapVersion = "0.9.4"; // PhoneGap version + public static String phonegapVersion = "1.0.0rc2"; // PhoneGap version public static String platform = "Android"; // Device OS public static String uuid; // Device UUID @@ -116,34 +114,13 @@ public class Device extends Plugin { public String getPhonegapVersion() { return Device.phonegapVersion; } - - public String getLine1Number(){ - TelephonyManager operator = (TelephonyManager)this.ctx.getSystemService(Context.TELEPHONY_SERVICE); - return operator.getLine1Number(); - } - public String getDeviceId(){ - TelephonyManager operator = (TelephonyManager)this.ctx.getSystemService(Context.TELEPHONY_SERVICE); - return operator.getDeviceId(); - } - - public String getSimSerialNumber(){ - TelephonyManager operator = (TelephonyManager)this.ctx.getSystemService(Context.TELEPHONY_SERVICE); - return operator.getSimSerialNumber(); - } - - public String getSubscriberId(){ - TelephonyManager operator = (TelephonyManager)this.ctx.getSystemService(Context.TELEPHONY_SERVICE); - return operator.getSubscriberId(); - } - - public String getModel() - { + public String getModel() { String model = android.os.Build.MODEL; return model; } - public String getProductName() - { + + public String getProductName() { String productname = android.os.Build.PRODUCT; return productname; } @@ -158,8 +135,7 @@ public class Device extends Plugin { return osversion; } - public String getSDKVersion() - { + public String getSDKVersion() { String sdkversion = android.os.Build.VERSION.SDK; return sdkversion; } diff --git a/framework/src/com/phonegap/DirectoryManager.java b/framework/src/com/phonegap/DirectoryManager.java index a659527d..e3ac3074 100644 --- a/framework/src/com/phonegap/DirectoryManager.java +++ b/framework/src/com/phonegap/DirectoryManager.java @@ -9,6 +9,7 @@ package com.phonegap; import java.io.File; +import android.content.Context; import android.os.Environment; import android.os.StatFs; @@ -111,4 +112,31 @@ public class DirectoryManager { } return newPath; } + + /** + * Determine if we can use the SD Card to store the temporary file. If not then use + * the internal cache directory. + * + * @return the absolute path of where to store the file + */ + protected static String getTempDirectoryPath(Context ctx) { + File cache = null; + + // SD Card Mounted + if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { + cache = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + + "/Android/data/" + ctx.getPackageName() + "/cache/"); + } + // Use internal storage + else { + cache = ctx.getCacheDir(); + } + + // Create the cache directory if it doesn't exist + if (!cache.exists()) { + cache.mkdirs(); + } + + return cache.getAbsolutePath(); + } } \ No newline at end of file diff --git a/framework/src/com/phonegap/DroidGap.java b/framework/src/com/phonegap/DroidGap.java index 470ed9f8..a22622ba 100755 --- a/framework/src/com/phonegap/DroidGap.java +++ b/framework/src/com/phonegap/DroidGap.java @@ -7,37 +7,51 @@ */ package com.phonegap; +import java.util.HashMap; +import java.util.Map.Entry; + import org.json.JSONArray; import org.json.JSONException; + +import android.app.Activity; import android.app.AlertDialog; +import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; -import android.graphics.Bitmap; import android.graphics.Color; +import android.graphics.Rect; import android.media.AudioManager; import android.net.Uri; +import android.net.http.SslError; import android.os.Bundle; import android.util.Log; +import android.view.Display; 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.GeolocationPermissions.Callback; import android.webkit.JsPromptResult; +import android.webkit.JsResult; +import android.webkit.SslErrorHandler; +import android.webkit.WebChromeClient; import android.webkit.WebSettings; +import android.webkit.WebSettings.LayoutAlgorithm; 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.EditText; import android.widget.LinearLayout; -import com.phonegap.api.Plugin; -import com.phonegap.api.PluginManager; + import com.phonegap.api.PhonegapActivity; +import com.phonegap.api.IPlugin; +import com.phonegap.api.PluginManager; /** * This class is the main Android activity that represents the PhoneGap @@ -81,10 +95,6 @@ import com.phonegap.api.PhonegapActivity; * // (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); @@ -116,16 +126,20 @@ public class DroidGap extends PhonegapActivity { protected PluginManager pluginManager; protected boolean cancelLoadUrl = false; protected boolean clearHistory = false; + protected ProgressDialog spinnerDialog = null; // The initial URL for our app + // ie http://server/path/index.html#abc?query private String url; - // The base of the initial URL for our app - private String baseUrl; + // The base of the initial URL for our app. + // Does not include file name. Ends with / + // ie http://server/path/ + private String baseUrl = null; // Plugin to call when activity result is received - private Plugin activityResultCallback = null; - private boolean activityResultKeepRunning; + protected IPlugin activityResultCallback = null; + protected boolean activityResultKeepRunning; // Flag indicates that a loadUrl timeout occurred private int loadUrlTimeout = 0; @@ -133,10 +147,6 @@ public class DroidGap extends PhonegapActivity { /* * 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. @@ -167,7 +177,11 @@ public class DroidGap extends PhonegapActivity { 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); + Display display = getWindowManager().getDefaultDisplay(); + int width = display.getWidth(); + int height = display.getHeight(); + + root = new LinearLayoutSoftKeyboardDetect(this, width, height); root.setOrientation(LinearLayout.VERTICAL); root.setBackgroundColor(Color.BLACK); root.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, @@ -201,11 +215,11 @@ public class DroidGap extends PhonegapActivity { WebViewReflect.checkCompatibility(); - if (android.os.Build.VERSION.RELEASE.startsWith("2.")) { - this.appView.setWebChromeClient(new EclairClient(DroidGap.this)); + if (android.os.Build.VERSION.RELEASE.startsWith("1.")) { + this.appView.setWebChromeClient(new GapClient(DroidGap.this)); } else { - this.appView.setWebChromeClient(new GapClient(DroidGap.this)); + this.appView.setWebChromeClient(new EclairClient(DroidGap.this)); } this.setWebViewClient(this.appView, new GapViewClient(this)); @@ -221,9 +235,9 @@ public class DroidGap extends PhonegapActivity { settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL); // Enable database - Package pack = this.getClass().getPackage(); - String appPackage = pack.getName(); - WebViewReflect.setStorage(settings, true, "/data/data/" + appPackage + "/app_database/"); + settings.setDatabaseEnabled(true); + String databasePath = this.getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath(); + settings.setDatabasePath(databasePath); // Enable DOM storage WebViewReflect.setDomStorage(settings); @@ -270,22 +284,6 @@ public class DroidGap extends PhonegapActivity { 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"); } /** @@ -305,9 +303,6 @@ public class DroidGap extends PhonegapActivity { root.setBackgroundResource(this.splashscreen); } - // If hideLoadingDialogOnPageLoad - this.hideLoadingDialogOnPageLoad = this.getBooleanProperty("hideLoadingDialogOnPageLoad", false); - // If loadInWebView this.loadInWebView = this.getBooleanProperty("loadInWebView", false); @@ -329,12 +324,14 @@ public class DroidGap extends PhonegapActivity { 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; + if (this.baseUrl == null) { + int i = url.lastIndexOf('/'); + if (i > 0) { + this.baseUrl = url.substring(0, i+1); + } + else { + this.baseUrl = this.url + "/"; + } } System.out.println("url="+url+" baseUrl="+baseUrl); @@ -367,10 +364,7 @@ public class DroidGap extends PhonegapActivity { message = loading; } } - JSONArray parm = new JSONArray(); - parm.put(title); - parm.put(message); - me.pluginManager.exec("Notification", "activityStart", null, parm.toString(), false); + me.spinnerStart(title, message); } // Create a timeout timer for loadUrl @@ -592,37 +586,58 @@ public class DroidGap extends PhonegapActivity { public void setDoubleProperty(String name, double value) { this.getIntent().putExtra(name, value); } - + @Override /** * Called when the system is about to start resuming a previous activity. */ protected void onPause() { super.onPause(); + if (this.appView == null) { + return; + } + // Send pause event to JavaScript this.appView.loadUrl("javascript:try{PhoneGap.onPause.fire();}catch(e){};"); + // Forward to plugins + this.pluginManager.onPause(this.keepRunning); + // 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 + /** + * Called when the activity receives a new intent + **/ + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + //Forward to plugins + this.pluginManager.onNewIntent(intent); + } + @Override /** * Called when the activity will start interacting with the user. */ protected void onResume() { super.onResume(); + if (this.appView == null) { + return; + } // Send resume event to JavaScript this.appView.loadUrl("javascript:try{PhoneGap.onResume.fire();}catch(e){};"); + // Forward to plugins + this.pluginManager.onResume(this.keepRunning || this.activityResultKeepRunning); + // If app doesn't want to run in background if (!this.keepRunning || this.activityResultKeepRunning) { @@ -632,9 +647,6 @@ public class DroidGap extends PhonegapActivity { this.activityResultKeepRunning = false; } - // Forward to plugins - this.pluginManager.onResume(); - // Resume JavaScript timers (including setInterval) this.appView.resumeTimers(); } @@ -647,18 +659,21 @@ public class DroidGap extends PhonegapActivity { public void onDestroy() { super.onDestroy(); - // Make sure pause event is sent if onPause hasn't been called before onDestroy - this.appView.loadUrl("javascript:try{PhoneGap.onPause.fire();}catch(e){};"); + if (this.appView != null) { - // Load blank page so that JavaScript onunload is called - this.appView.loadUrl("about:blank"); - - // Forward to plugins - this.pluginManager.onDestroy(); + // Make sure pause event is sent if onPause hasn't been called before onDestroy + this.appView.loadUrl("javascript:try{PhoneGap.onPause.fire();}catch(e){};"); - if (this.callbackServer != null) { - this.callbackServer.destroy(); - } + // Send destroy event to JavaScript + this.appView.loadUrl("javascript:try{PhoneGap.onDestroy.fire();}catch(e){};"); + + // Load blank page so that JavaScript onunload is called + this.appView.loadUrl("about:blank"); + + // Forward to plugins + this.pluginManager.onDestroy(); + + } } /** @@ -681,13 +696,97 @@ public class DroidGap extends PhonegapActivity { this.callbackServer.sendJavascript(statement); } + + /** + * Display a new browser with the specified URL. + * + * NOTE: If usePhoneGap is set, only trusted PhoneGap URLs should be loaded, + * since any PhoneGap API can be called by the loaded HTML page. + * + * @param url The url to load. + * @param usePhoneGap Load url in PhoneGap webview. + * @param clearPrev Clear the activity stack, so new app becomes top of stack + * @param params DroidGap parameters for new app + * @throws android.content.ActivityNotFoundException + */ + public void showWebPage(String url, boolean usePhoneGap, boolean clearPrev, HashMap params) throws android.content.ActivityNotFoundException { + Intent intent = null; + if (usePhoneGap) { + intent = new Intent().setClass(this, com.phonegap.DroidGap.class); + intent.putExtra("url", url); + + // Add parameters + if (params != null) { + java.util.Set> s = params.entrySet(); + java.util.Iterator> it = s.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + String key = entry.getKey(); + Object value = entry.getValue(); + if (value == null) { + } + else if (value.getClass().equals(String.class)) { + intent.putExtra(key, (String)value); + } + else if (value.getClass().equals(Boolean.class)) { + intent.putExtra(key, (Boolean)value); + } + else if (value.getClass().equals(Integer.class)) { + intent.putExtra(key, (Integer)value); + } + } + + } + } + else { + intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(url)); + } + this.startActivity(intent); + + // Finish current activity + if (clearPrev) { + this.finish(); + } + } + + /** + * Show the spinner. Must be called from the UI thread. + * + * @param title Title of the dialog + * @param message The message of the dialog + */ + public void spinnerStart(final String title, final String message) { + if (this.spinnerDialog != null) { + this.spinnerDialog.dismiss(); + this.spinnerDialog = null; + } + final DroidGap me = this; + this.spinnerDialog = ProgressDialog.show(DroidGap.this, title , message, true, true, + new DialogInterface.OnCancelListener() { + public void onCancel(DialogInterface dialog) { + me.spinnerDialog = null; + } + }); + } + + /** + * Stop spinner. + */ + public void spinnerStop() { + if (this.spinnerDialog != null) { + this.spinnerDialog.dismiss(); + this.spinnerDialog = null; + } + } + /** * Provides a hook for calling "alert" from javascript. Useful for * debugging your javascript. */ public class GapClient extends WebChromeClient { - private Context ctx; + private DroidGap ctx; /** * Constructor. @@ -695,7 +794,7 @@ public class DroidGap extends PhonegapActivity { * @param ctx */ public GapClient(Context ctx) { - this.ctx = ctx; + this.ctx = (DroidGap)ctx; } /** @@ -767,10 +866,17 @@ public class DroidGap extends PhonegapActivity { */ @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { + + // Security check to make sure any requests are coming from the page initially + // loaded in webview and not another loaded in an iframe. + boolean reqOk = false; + if (url.indexOf(this.ctx.baseUrl) == 0) { + reqOk = true; + } // 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:")) { + if (reqOk && defaultValue != null && defaultValue.length() > 3 && defaultValue.substring(0, 4).equals("gap:")) { JSONArray array; try { array = new JSONArray(defaultValue.substring(4)); @@ -786,13 +892,13 @@ public class DroidGap extends PhonegapActivity { } // Polling for JavaScript messages - else if (defaultValue.equals("gap_poll:")) { + else if (reqOk && defaultValue.equals("gap_poll:")) { String r = callbackServer.getJavascript(); result.confirm(r); } // Calling into CallbackServer - else if (defaultValue.equals("gap_callbackServer:")) { + else if (reqOk && defaultValue.equals("gap_callbackServer:")) { String r = ""; if (message.equals("usePolling")) { r = ""+callbackServer.usePolling(); @@ -811,18 +917,40 @@ public class DroidGap extends PhonegapActivity { // Show dialog else { - //@TODO: - result.confirm(""); - } + final JsPromptResult res = result; + AlertDialog.Builder dlg = new AlertDialog.Builder(this.ctx); + dlg.setMessage(message); + final EditText input = new EditText(this.ctx); + if (defaultValue != null) { + input.setText(defaultValue); + } + dlg.setView(input); + dlg.setCancelable(false); + dlg.setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + String usertext = input.getText().toString(); + res.confirm(usertext); + } + }); + dlg.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + res.cancel(); + } + }); + dlg.create(); + dlg.show(); + } return true; } - + } /** * WebChromeClient that extends GapClient with additional support for Android 2.X */ - public final class EclairClient extends GapClient { + public class EclairClient extends GapClient { private String TAG = "PhoneGapLog"; private long MAX_QUOTA = 100 * 1024 * 1024; @@ -876,6 +1004,12 @@ public class DroidGap extends PhonegapActivity { } @Override + /** + * Instructs the client to show a prompt to ask the user to set the Geolocation permission state for the specified origin. + * + * @param origin + * @param callback + */ public void onGeolocationPermissionsShowPrompt(String origin, Callback callback) { // TODO Auto-generated method stub super.onGeolocationPermissionsShowPrompt(origin, callback); @@ -982,17 +1116,28 @@ public class DroidGap extends PhonegapActivity { // All else else { - int i = url.lastIndexOf('/'); - String newBaseUrl = url; - if (i > 0) { - newBaseUrl = url.substring(0, i); - } - // 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); + if (this.ctx.loadInWebView || url.startsWith("file://") || url.indexOf(this.ctx.baseUrl) == 0) { + try { + // Init parameters to new DroidGap activity and propagate existing parameters + HashMap params = new HashMap(); + params.put("loadingDialog", ""); + if (this.ctx.loadInWebView) { + params.put("loadInWebView", true); + } + params.put("keepRunning", this.ctx.keepRunning); + params.put("loadUrlTimeoutValue", this.ctx.loadUrlTimeoutValue); + String errorUrl = this.ctx.getStringProperty("errorUrl", null); + if (errorUrl != null) { + params.put("errorUrl", errorUrl); + } + + this.ctx.showWebPage(url, true, false, params); + } catch (android.content.ActivityNotFoundException e) { + System.out.println("Error loading url into DroidGap - "+url+":"+ e.toString()); + } } // If not our application, let default viewer handle @@ -1025,22 +1170,28 @@ public class DroidGap extends PhonegapActivity { // 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;}"); + if (!url.equals("about:blank")) { + 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); - } + this.ctx.spinnerStop(); // 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(); } + + // Shutdown if blank loaded + if (url.equals("about:blank")) { + if (this.ctx.callbackServer != null) { + this.ctx.callbackServer.destroy(); + } + } } /** @@ -1060,11 +1211,32 @@ public class DroidGap extends PhonegapActivity { this.ctx.loadUrlTimeout++; // Stop "app loading" spinner if showing - this.ctx.pluginManager.exec("Notification", "activityStop", null, "[]", false); + this.ctx.spinnerStop(); // Handle error this.ctx.onReceivedError(errorCode, description, failingUrl); } + + public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { + + final String packageName = this.ctx.getPackageName(); + final PackageManager pm = this.ctx.getPackageManager(); + ApplicationInfo appInfo; + try { + appInfo = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA); + if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { + // debug = true + handler.proceed(); + return; + } else { + // debug = false + super.onReceivedSslError(view, handler, error); + } + } catch (NameNotFoundException e) { + // When it doubt, lock it out! + super.onReceivedSslError(view, handler, error); + } + } } /** @@ -1075,6 +1247,9 @@ public class DroidGap extends PhonegapActivity { */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { + if (this.appView == null) { + return super.onKeyDown(keyCode, event); + } // If back key if (keyCode == KeyEvent.KEYCODE_BACK) { @@ -1082,6 +1257,7 @@ public class DroidGap extends PhonegapActivity { // If back key is bound, then send event to JavaScript if (this.bound) { this.appView.loadUrl("javascript:PhoneGap.fireEvent('backbutton');"); + return true; } // If not bound @@ -1090,6 +1266,7 @@ public class DroidGap extends PhonegapActivity { // Go to previous page in webview if it is possible to go back if (this.appView.canGoBack()) { this.appView.goBack(); + return true; } // If not, then invoke behavior of super class @@ -1102,11 +1279,13 @@ public class DroidGap extends PhonegapActivity { // If menu key else if (keyCode == KeyEvent.KEYCODE_MENU) { this.appView.loadUrl("javascript:PhoneGap.fireEvent('menubutton');"); + return true; } // If search key else if (keyCode == KeyEvent.KEYCODE_SEARCH) { this.appView.loadUrl("javascript:PhoneGap.fireEvent('searchbutton');"); + return true; } return false; @@ -1126,12 +1305,7 @@ public class DroidGap extends PhonegapActivity { @Override public void startActivityForResult(Intent intent, int requestCode) throws RuntimeException { System.out.println("startActivityForResult(intent,"+requestCode+")"); - if (requestCode == -1) { - super.startActivityForResult(intent, requestCode); - } - else { - throw new RuntimeException("PhoneGap Exception: Call startActivityForResult(Command, Intent) instead."); - } + super.startActivityForResult(intent, requestCode); } /** @@ -1142,7 +1316,7 @@ public class DroidGap extends PhonegapActivity { * @param intent The intent to start * @param requestCode The request code that is passed to callback to identify the activity */ - public void startActivityForResult(Plugin command, Intent intent, int requestCode) { + public void startActivityForResult(IPlugin command, Intent intent, int requestCode) { this.activityResultCallback = command; this.activityResultKeepRunning = this.keepRunning; @@ -1167,12 +1341,17 @@ public class DroidGap extends PhonegapActivity { */ protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); - Plugin callback = this.activityResultCallback; + IPlugin callback = this.activityResultCallback; if (callback != null) { callback.onActivityResult(requestCode, resultCode, intent); } } - + + @Override + public void setActivityResultCallback(IPlugin plugin) { + this.activityResultCallback = plugin; + } + /** * 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. @@ -1233,4 +1412,84 @@ public class DroidGap extends PhonegapActivity { } }); } + + /** + * We are providing this class to detect when the soft keyboard is shown + * and hidden in the web view. + */ + class LinearLayoutSoftKeyboardDetect extends LinearLayout { + + private static final String LOG_TAG = "SoftKeyboardDetect"; + + private int oldHeight = 0; // Need to save the old height as not to send redundant events + private int oldWidth = 0; // Need to save old width for orientation change + private int screenWidth = 0; + private int screenHeight = 0; + + public LinearLayoutSoftKeyboardDetect(Context context, int width, int height) { + super(context); + screenWidth = width; + screenHeight = height; + } + + @Override + /** + * Start listening to new measurement events. Fire events when the height + * gets smaller fire a show keyboard event and when height gets bigger fire + * a hide keyboard event. + * + * Note: We are using callbackServer.sendJavascript() instead of + * this.appView.loadUrl() as changing the URL of the app would cause the + * soft keyboard to go away. + * + * @param widthMeasureSpec + * @param heightMeasureSpec + */ + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + Log.d(LOG_TAG, "We are in our onMeasure method"); + + // Get the current height of the visible part of the screen. + // This height will not included the status bar. + int height = MeasureSpec.getSize(heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + + Log.d(LOG_TAG, "Old Height = " + oldHeight); + Log.d(LOG_TAG, "Height = " + height); + Log.d(LOG_TAG, "Old Width = " + oldWidth); + Log.d(LOG_TAG, "Width = " + width); + + + // If the oldHeight = 0 then this is the first measure event as the app starts up. + // If oldHeight == height then we got a measurement change that doesn't affect us. + if (oldHeight == 0 || oldHeight == height) { + Log.d(LOG_TAG, "Ignore this event"); + } + // Account for orientation change and ignore this event/Fire orientation change + else if(screenHeight == width) + { + int tmp_var = screenHeight; + screenHeight = screenWidth; + screenWidth = tmp_var; + Log.d(LOG_TAG, "Orientation Change"); + } + // If the height as gotten bigger then we will assume the soft keyboard has + // gone away. + else if (height > oldHeight) { + Log.d(LOG_TAG, "Throw hide keyboard event"); + callbackServer.sendJavascript("PhoneGap.fireEvent('hidekeyboard');"); + } + // If the height as gotten smaller then we will assume the soft keyboard has + // been displayed. + else if (height < oldHeight) { + Log.d(LOG_TAG, "Throw show keyboard event"); + callbackServer.sendJavascript("PhoneGap.fireEvent('showkeyboard');"); + } + + // Update the old height for the next event + oldHeight = height; + oldWidth = width; + } + } } diff --git a/framework/src/com/phonegap/FileTransfer.java b/framework/src/com/phonegap/FileTransfer.java index 24b181fb..b73961c7 100644 --- a/framework/src/com/phonegap/FileTransfer.java +++ b/framework/src/com/phonegap/FileTransfer.java @@ -272,7 +272,7 @@ public class FileTransfer extends Plugin { 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("Content-Disposition: form-data; name=\"" + key.toString() + "\";"); dos.writeBytes(LINE_END + LINE_END); dos.writeBytes(params.getString(key.toString())); dos.writeBytes(LINE_END); @@ -315,7 +315,13 @@ public class FileTransfer extends Plugin { //------------------ read the SERVER RESPONSE StringBuffer responseString = new StringBuffer(""); - DataInputStream inStream = new DataInputStream ( conn.getInputStream() ); + DataInputStream inStream; + try { + inStream = new DataInputStream ( conn.getInputStream() ); + } catch(FileNotFoundException e) { + throw new IOException("Received error from server"); + } + String line; while (( line = inStream.readLine()) != null) { responseString.append(line); diff --git a/framework/src/com/phonegap/FileUtils.java b/framework/src/com/phonegap/FileUtils.java index b88fc962..240a507e 100755 --- a/framework/src/com/phonegap/FileUtils.java +++ b/framework/src/com/phonegap/FileUtils.java @@ -3,13 +3,14 @@ * 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 java.io.*; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLDecoder; import java.nio.channels.FileChannel; import org.apache.commons.codec.binary.Base64; @@ -17,8 +18,10 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import android.database.Cursor; import android.net.Uri; import android.os.Environment; +import android.provider.MediaStore; import android.util.Log; import android.webkit.MimeTypeMap; @@ -97,48 +100,20 @@ public class FileUtils extends Plugin { 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); - } + String s = this.readAsText(args.getString(0), args.getString(1)); + return new PluginResult(status, s); } 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); - } + String s = this.readAsDataURL(args.getString(0)); + return new PluginResult(status, s); } 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); - } + long fileSize = this.write(args.getString(0), args.getString(1), args.getInt(2)); + return new PluginResult(status, fileSize); } 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); - } + long fileSize = this.truncateFile(args.getString(0), args.getLong(1)); + return new PluginResult(status, fileSize); } else if (action.equals("requestFileSystem")) { long size = args.optLong(1); @@ -254,22 +229,35 @@ public class FileUtils extends Plugin { * @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(); - } + String decoded = URLDecoder.decode(url, "UTF-8"); + + File fp = null; + + // Handle the special case where you get an Android content:// uri. + if (decoded.startsWith("content:")) { + Cursor cursor = this.ctx.managedQuery(Uri.parse(decoded), new String[] { MediaStore.Images.Media.DATA }, null, null, null); + // Note: MediaStore.Images/Audio/Video.Media.DATA is always "_data" + int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + cursor.moveToFirst(); + fp = new File(cursor.getString(column_index)); + } else { + // Test to see if this is a valid URL first + @SuppressWarnings("unused") + URL testUrl = new URL(decoded); + + if (decoded.startsWith("file://")) { + fp = new File(decoded.substring(7, decoded.length())); + } else { + fp = new File(decoded); + } + } + + if (!fp.exists()) { + throw new FileNotFoundException(); + } + if (!fp.canRead()) { + throw new IOException(); + } return getEntry(fp); } @@ -436,7 +424,7 @@ public class FileUtils extends Plugin { } // Check to make sure we are not copying the directory into itself - if (destinationDir.getAbsolutePath().startsWith(srcDir.getAbsolutePath())) { + if (isCopyOnItself(srcDir.getAbsolutePath(), destinationDir.getAbsolutePath())) { throw new InvalidModificationException("Can't copy itself into itself"); } @@ -460,6 +448,26 @@ public class FileUtils extends Plugin { return getEntry(destinationDir); } + /** + * Check to see if the user attempted to copy an entry into its parent without changing its name, + * or attempted to copy a directory into a directory that it contains directly or indirectly. + * + * @param srcDir + * @param destinationDir + * @return + */ + private boolean isCopyOnItself(String src, String dest) { + + // This weird test is to determine if we are copying or moving a directory into itself. + // Copy /sdcard/myDir to /sdcard/myDir-backup is okay but + // Copy /sdcard/myDir to /sdcard/myDir/backup should thow an INVALID_MODIFICATION_ERR + if (dest.startsWith(src) && dest.indexOf(File.separator, src.length()-1) != -1) { + return true; + } + + return false; + } + /** * Move a file * @@ -505,8 +513,8 @@ public class FileUtils extends Plugin { } // Check to make sure we are not copying the directory into itself - if (destinationDir.getAbsolutePath().startsWith(srcDir.getAbsolutePath())) { - throw new InvalidModificationException("Can't copy itself into itself"); + if (isCopyOnItself(srcDir.getAbsolutePath(), destinationDir.getAbsolutePath())) { + throw new InvalidModificationException("Can't move itself into itself"); } // If the destination directory already exists and is empty then delete it. This is according to spec. @@ -836,49 +844,19 @@ public class FileUtils extends Plugin { * @return T=returns value */ public boolean isSynch(String action) { - if (action.equals("readAsText")) { - return false; - } - else if (action.equals("readAsDataURL")) { - return false; - } - 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; + if (action.equals("testSaveLocationExists")) { + return true; + } + else if (action.equals("getFreeDiskSpace")) { + return true; + } + else if (action.equals("testFileExists")) { + return true; + } + else if (action.equals("testDirectoryExists")) { + return true; + } + return false; } //-------------------------------------------------------------------------- @@ -942,7 +920,7 @@ public class FileUtils extends Plugin { * @param filename * @return a mime type */ - private String getMimeType(String filename) { + public static String getMimeType(String filename) { MimeTypeMap map = MimeTypeMap.getSingleton(); return map.getMimeTypeFromExtension(map.getFileExtensionFromUrl(filename)); } @@ -952,39 +930,29 @@ public class FileUtils extends Plugin { * * @param filename The name of the file. * @param data The contents of the file. - * @param append T=append, F=overwrite + * @param offset The position to begin writing the file. * @throws FileNotFoundException, IOException */ - public void writeAsText(String filename, String data, boolean append) throws FileNotFoundException, IOException { - String FilePath= filename; + /**/ + public long write(String filename, String data, int offset) throws FileNotFoundException, IOException { + boolean append = false; + if (offset > 0) { + this.truncateFile(filename, offset); + append = true; + } + byte [] rawData = data.getBytes(); ByteArrayInputStream in = new ByteArrayInputStream(rawData); - FileOutputStream out= new FileOutputStream(FilePath, append); + FileOutputStream out = new FileOutputStream(filename, append); byte buff[] = new byte[rawData.length]; in.read(buff, 0, buff.length); out.write(buff, 0, rawData.length); out.flush(); 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 * diff --git a/framework/src/com/phonegap/NetworkManager.java b/framework/src/com/phonegap/NetworkManager.java index d7dfa6dc..19a8a5c6 100755 --- a/framework/src/com/phonegap/NetworkManager.java +++ b/framework/src/com/phonegap/NetworkManager.java @@ -3,34 +3,65 @@ * 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 org.apache.http.client.methods.HttpGet; -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; +import android.content.BroadcastReceiver; import android.content.Context; -import android.net.*; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; public class NetworkManager extends Plugin { - public static int NOT_REACHABLE = 0; + public static int NOT_REACHABLE = 0; public static int REACHABLE_VIA_CARRIER_DATA_NETWORK = 1; public static int REACHABLE_VIA_WIFI_NETWORK = 2; + + public static final String WIFI = "wifi"; + public static final String WIMAX = "wimax"; + // mobile + public static final String MOBILE = "mobile"; + // 2G network types + public static final String GSM = "gsm"; + public static final String GPRS = "gprs"; + public static final String EDGE = "edge"; + // 3G network types + public static final String CDMA = "cdma"; + public static final String UMTS = "umts"; + // 4G network types + public static final String LTE = "lte"; + public static final String UMB = "umb"; + // return types + public static final String TYPE_UNKNOWN = "unknown"; + public static final String TYPE_ETHERNET = "ethernet"; + public static final String TYPE_WIFI = "wifi"; + public static final String TYPE_2G = "2g"; + public static final String TYPE_3G = "3g"; + public static final String TYPE_4G = "4g"; + public static final String TYPE_NONE = "none"; - ConnectivityManager sockMan; + private static final String LOG_TAG = "NetworkManager"; + + private String connectionCallbackId; + + ConnectivityManager sockMan; + BroadcastReceiver receiver; /** * Constructor. */ public NetworkManager() { + this.receiver = null; } /** @@ -41,9 +72,24 @@ public class NetworkManager extends Plugin { */ public void setContext(PhonegapActivity ctx) { super.setContext(ctx); - this.sockMan = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); - } + this.sockMan = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); + this.connectionCallbackId = null; + + // We need to listen to connectivity events to update navigator.connection + IntentFilter intentFilter = new IntentFilter() ; + intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + if (this.receiver == null) { + this.receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateConnectionInfo((NetworkInfo) intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO)); + } + }; + ctx.registerReceiver(this.receiver, intentFilter); + } + } + /** * Executes the request and returns PluginResult. * @@ -53,25 +99,18 @@ public class NetworkManager extends Plugin { * @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("isAvailable")) { - boolean b = this.isAvailable(); - return new PluginResult(status, b); - } - else if (action.equals("isWifiActive")) { - boolean b = this.isWifiActive(); - return new PluginResult(status, b); - } - else if (action.equals("isReachable")) { - int i = this.isReachable(args.getString(0), args.getBoolean(1)); - return new PluginResult(status, i); - } - return new PluginResult(status, result); - } catch (JSONException e) { - return new PluginResult(PluginResult.Status.JSON_EXCEPTION); + PluginResult.Status status = PluginResult.Status.INVALID_ACTION; + String result = "Unsupported Operation: " + action; + + if (action.equals("getConnectionInfo")) { + this.connectionCallbackId = callbackId; + NetworkInfo info = sockMan.getActiveNetworkInfo(); + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, this.getConnectionInfo(info)); + pluginResult.setKeepCallback(true); + return pluginResult; } + + return new PluginResult(status, result); } /** @@ -84,70 +123,100 @@ public class NetworkManager extends Plugin { // All methods take a while, so always use async return false; } + + /** + * Stop network receiver. + */ + public void onDestroy() { + if (this.receiver != null) { + try { + this.ctx.unregisterReceiver(this.receiver); + } catch (Exception e) { + Log.e(LOG_TAG, "Error unregistering network receiver: " + e.getMessage(), e); + } + } + } //-------------------------------------------------------------------------- // LOCAL METHODS //-------------------------------------------------------------------------- - /** - * Determine if a network connection exists. - * - * @return - */ - public boolean isAvailable() { - NetworkInfo info = sockMan.getActiveNetworkInfo(); - boolean conn = false; - if (info != null) { - conn = info.isConnected(); - } - return conn; - } - + /** - * Determine if a WIFI connection exists. + * Updates the JavaScript side whenever the connection changes * + * @param info the current active network info * @return */ - public boolean isWifiActive() { - NetworkInfo info = sockMan.getActiveNetworkInfo(); - if (info != null) { - String type = info.getTypeName(); - return type.equals("WIFI"); - } - return false; + private void updateConnectionInfo(NetworkInfo info) { + // send update to javascript "navigator.network.connection" + sendUpdate(this.getConnectionInfo(info)); } - - /** - * Determine if a URI is reachable over the network. + + /** + * Get the latest network connection information * - * @param uri - * @param isIpAddress - * @return + * @param info the current active network info + * @return a JSONObject that represents the network info */ - public int isReachable(String uri, boolean isIpAddress) { - int reachable = NOT_REACHABLE; - - if (uri.indexOf("http://") == -1) { - uri = "http://" + uri; - } - - if (this.isAvailable()) { - try { - DefaultHttpClient httpclient = new DefaultHttpClient(); - HttpGet httpget = new HttpGet(uri); - httpclient.execute(httpget); - - if (this.isWifiActive()) { - reachable = REACHABLE_VIA_WIFI_NETWORK; - } - else { - reachable = REACHABLE_VIA_CARRIER_DATA_NETWORK; - } - } catch (Exception e) { - reachable = NOT_REACHABLE; + private String getConnectionInfo(NetworkInfo info) { + String type = TYPE_NONE; + if (info != null) { + // If we are not connected to any network set type to none + if (!info.isConnected()) { + type = TYPE_NONE; + } + else { + type = getType(info); } } - - return reachable; + return type; + } + + /** + * Create a new plugin result and send it back to JavaScript + * + * @param connection the network info to set as navigator.connection + */ + private void sendUpdate(String type) { + PluginResult result = new PluginResult(PluginResult.Status.OK, type); + result.setKeepCallback(true); + this.success(result, this.connectionCallbackId); + } + + /** + * Determine the type of connection + * + * @param info the network info so we can determine connection type. + * @return the type of mobile network we are on + */ + private String getType(NetworkInfo info) { + if (info != null) { + String type = info.getTypeName(); + + if (type.toLowerCase().equals(WIFI)) { + return TYPE_WIFI; + } + else if (type.toLowerCase().equals(MOBILE)) { + type = info.getSubtypeName(); + if (type.toLowerCase().equals(GSM) || + type.toLowerCase().equals(GPRS) || + type.toLowerCase().equals(EDGE)) { + return TYPE_2G; + } + else if (type.toLowerCase().equals(CDMA) || + type.toLowerCase().equals(UMTS)) { + return TYPE_3G; + } + else if (type.toLowerCase().equals(LTE) || + type.toLowerCase().equals(UMB)) { + return TYPE_4G; + } + } + } + else { + return TYPE_NONE; + } + return TYPE_UNKNOWN; } } diff --git a/framework/src/com/phonegap/Storage.java b/framework/src/com/phonegap/Storage.java index de30afb2..56bf7893 100755 --- a/framework/src/com/phonegap/Storage.java +++ b/framework/src/com/phonegap/Storage.java @@ -16,15 +16,21 @@ import android.database.Cursor; import android.database.sqlite.*; /** - * This class implements the HTML5 database support for Android 1.X devices. - * It is not used for Android 2.X, since HTML5 database is built in to the browser. + * This class implements the HTML5 database support for Android 1.X devices. It + * is not used for Android 2.X, since HTML5 database is built in to the browser. */ public class Storage extends Plugin { + + // Data Definition Language + private static final String ALTER = "alter"; + private static final String CREATE = "create"; + private static final String DROP = "drop"; + private static final String TRUNCATE = "truncate"; - SQLiteDatabase myDb = null; // Database object - String path = null; // Database path - String dbName = null; // Database name - + SQLiteDatabase myDb = null; // Database object + String path = null; // Database path + String dbName = null; // Database name + /** * Constructor. */ @@ -34,29 +40,37 @@ public class Storage 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. + * @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 = ""; - + String result = ""; + try { - // TODO: Do we want to allow a user to do this, since they could get to other app databases? + // TODO: Do we want to allow a user to do this, since they could get + // to other app databases? if (action.equals("setStorage")) { this.setStorage(args.getString(0)); - } - else if (action.equals("openDatabase")) { - this.openDatabase(args.getString(0), args.getString(1), args.getString(2), args.getLong(3)); - } - else if (action.equals("executeSql")) { - JSONArray a = args.getJSONArray(1); - int len = a.length(); - String[] s = new String[len]; - for (int i=0; i plugins = new HashMap(); + private HashMap plugins = new HashMap(); private HashMap services = new HashMap(); private final PhonegapActivity ctx; @@ -38,6 +42,35 @@ public final class PluginManager { public PluginManager(WebView app, PhonegapActivity ctx) { this.ctx = ctx; this.app = app; + this.loadPlugins(); + } + + /** + * Load plugins from res/xml/plugins.xml + */ + public void loadPlugins() { + int id = ctx.getResources().getIdentifier("plugins", "xml", ctx.getPackageName()); + if (id == 0) { pluginConfigurationMissing(); } + XmlResourceParser xml = ctx.getResources().getXml(id); + int eventType = -1; + while (eventType != XmlResourceParser.END_DOCUMENT) { + if (eventType == XmlResourceParser.START_TAG) { + String strNode = xml.getName(); + if (strNode.equals("plugin")) { + String name = xml.getAttributeValue(null, "name"); + String value = xml.getAttributeValue(null, "value"); + //System.out.println("Plugin: "+name+" => "+value); + this.addService(name, value); + } + } + try { + eventType = xml.next(); + } catch (XmlPullParserException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } } /** @@ -74,7 +107,7 @@ public final class PluginManager { c = getClassByName(clazz); } if (isPhoneGapPlugin(c)) { - final Plugin plugin = this.addPlugin(clazz, c); + final IPlugin plugin = this.addPlugin(clazz, c); final PhonegapActivity ctx = this.ctx; runAsync = async && !plugin.isSynch(action); if (runAsync) { @@ -100,7 +133,7 @@ public final class PluginManager { ctx.sendJavascript(cr.toErrorCallbackString(callbackId)); } } catch (Exception e) { - PluginResult cr = new PluginResult(PluginResult.Status.ERROR); + PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage()); ctx.sendJavascript(cr.toErrorCallbackString(callbackId)); } } @@ -159,24 +192,7 @@ public final class PluginManager { } return false; } - - /** - * Add plugin to be loaded and cached. This creates an instance of the plugin. - * If plugin is already created, then just return it. - * - * @param className The class to load - * @return The plugin - */ - public Plugin addPlugin(String className) { - try { - return this.addPlugin(className, this.getClassByName(className)); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - System.out.println("Error adding plugin "+className+"."); - } - return null; - } - + /** * Add plugin to be loaded and cached. This creates an instance of the plugin. * If plugin is already created, then just return it. @@ -187,12 +203,12 @@ public final class PluginManager { * @return The plugin */ @SuppressWarnings("unchecked") - private Plugin addPlugin(String className, Class clazz) { + private IPlugin addPlugin(String className, Class clazz) { if (this.plugins.containsKey(className)) { return this.getPlugin(className); } try { - Plugin plugin = (Plugin)clazz.newInstance(); + IPlugin plugin = (IPlugin)clazz.newInstance(); this.plugins.put(className, plugin); plugin.setContext(this.ctx); plugin.setView(this.app); @@ -211,8 +227,8 @@ public final class PluginManager { * @param className The class of the loaded plugin. * @return */ - private Plugin getPlugin(String className) { - Plugin plugin = this.plugins.get(className); + private IPlugin getPlugin(String className) { + IPlugin plugin = this.plugins.get(className); return plugin; } @@ -229,27 +245,31 @@ public final class PluginManager { /** * Called when the system is about to start resuming a previous activity. + * + * @param multitasking Flag indicating if multitasking is turned on for app */ - public void onPause() { - java.util.Set> s = this.plugins.entrySet(); - java.util.Iterator> it = s.iterator(); + public void onPause(boolean multitasking) { + java.util.Set> s = this.plugins.entrySet(); + java.util.Iterator> it = s.iterator(); while(it.hasNext()) { - Entry entry = it.next(); - Plugin plugin = entry.getValue(); - plugin.onPause(); + Entry entry = it.next(); + IPlugin plugin = entry.getValue(); + plugin.onPause(multitasking); } } /** * Called when the activity will start interacting with the user. + * + * @param multitasking Flag indicating if multitasking is turned on for app */ - public void onResume() { - java.util.Set> s = this.plugins.entrySet(); - java.util.Iterator> it = s.iterator(); + public void onResume(boolean multitasking) { + java.util.Set> s = this.plugins.entrySet(); + java.util.Iterator> it = s.iterator(); while(it.hasNext()) { - Entry entry = it.next(); - Plugin plugin = entry.getValue(); - plugin.onResume(); + Entry entry = it.next(); + IPlugin plugin = entry.getValue(); + plugin.onResume(multitasking); } } @@ -257,12 +277,32 @@ public final class PluginManager { * The final call you receive before your activity is destroyed. */ public void onDestroy() { - java.util.Set> s = this.plugins.entrySet(); - java.util.Iterator> it = s.iterator(); + java.util.Set> s = this.plugins.entrySet(); + java.util.Iterator> it = s.iterator(); while(it.hasNext()) { - Entry entry = it.next(); - Plugin plugin = entry.getValue(); + Entry entry = it.next(); + IPlugin plugin = entry.getValue(); plugin.onDestroy(); } } + + /** + * Called when the activity receives a new intent. + */ + public void onNewIntent(Intent intent) { + java.util.Set> s = this.plugins.entrySet(); + java.util.Iterator> it = s.iterator(); + while(it.hasNext()) { + Entry entry = it.next(); + IPlugin plugin = entry.getValue(); + plugin.onNewIntent(intent); + } + } + + private void pluginConfigurationMissing() { + System.err.println("====================================================================================="); + System.err.println("ERROR: plugin.xml is missing. Add res/xml/plugins.xml to your project."); + System.err.println("https://raw.github.com/phonegap/phonegap-android/master/framework/res/xml/plugins.xml"); + System.err.println("====================================================================================="); + } } \ No newline at end of file diff --git a/lib/classic.rb b/lib/classic.rb index cf9ab692..9e2b2dfc 100755 --- a/lib/classic.rb +++ b/lib/classic.rb @@ -5,7 +5,7 @@ class Classic @android_sdk_path, @name, @pkg, @www, @path = a build end - + def build setup clobber @@ -16,8 +16,8 @@ class Classic copy_libs add_name_to_strings write_java - end - + end + def setup @android_dir = File.expand_path(File.dirname(__FILE__).gsub(/lib$/,'')) @framework_dir = File.join(@android_dir, "framework") @@ -31,14 +31,14 @@ class Classic @app_js_dir = '' @content = 'index.html' end - + # replaces @path with new android project def clobber FileUtils.rm_r(@path) if File.exists? @path FileUtils.mkdir_p @path end - - # removes local.properties and recreates based on android_sdk_path + + # removes local.properties and recreates based on android_sdk_path # then generates framework/phonegap.jar def build_jar %w(local.properties phonegap.js phonegap.jar).each do |f| @@ -46,7 +46,7 @@ class Classic end open(File.join(@framework_dir, "local.properties"), 'w') do |f| f.puts "sdk.dir=#{ @android_sdk_path }" - end + end Dir.chdir(@framework_dir) `ant jar` Dir.chdir(@android_dir) @@ -55,9 +55,9 @@ class Classic # runs android create project # TODO need to allow more flexible SDK targetting via config.xml def create_android - IO.popen("android list targets") { |f| + IO.popen("android list targets") { |f| targets = f.readlines(nil)[0].scan(/id\:.*$/) - if (targets.length > 0) + if (targets.length > 0) target_id = targets.last.match(/\d+/).to_a.first `android create project -t #{ target_id } -k #{ @pkg } -a #{ @name } -n #{ @name } -p #{ @path }` else @@ -66,7 +66,7 @@ class Classic end } end - + # copies the project/www folder into tmp/android/www def include_www FileUtils.mkdir_p File.join(@path, "assets", "www") @@ -88,15 +88,18 @@ class Classic # copies stuff from src directory into the android project directory (@path) def copy_libs - version = IO.read(File.join(@framework_dir, '../VERSION')) + version = IO.read(File.join(@framework_dir, '../VERSION')).lstrip.rstrip 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.#{ version }.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") + # copies in plugins.xml + FileUtils.mkdir_p File.join(app_res_dir, "xml") + FileUtils.cp File.join(framework_res_dir, "xml","plugins.xml"), File.join(app_res_dir, "xml", "plugins.xml") # drops in the layout files: main.xml and preview.xml FileUtils.mkdir_p File.join(app_res_dir, "layout") %w(main.xml).each do |f| @@ -125,9 +128,9 @@ 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.#{ version }.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 def add_name_to_strings x = " @@ -138,8 +141,8 @@ class Classic " open(File.join(@path, "res", "values", "strings.xml"), 'w') do |f| f.puts x.gsub(' ','') - end - end + end + end # create java source file def write_java @@ -164,7 +167,7 @@ class Classic FileUtils.mkdir_p(code_dir) open(File.join(code_dir, "#{ @name }.java"),'w') { |f| f.puts j } end - + # friendly output for now def msg puts "Created #{ @path }" diff --git a/lib/create.rb b/lib/create.rb index aef3dd49..ec4ebd6e 100644 --- a/lib/create.rb +++ b/lib/create.rb @@ -1,5 +1,5 @@ # Create -# +# # Generates an Android project from a valid WWW directory and puts it in ../[PROJECT NAME]_android # class Create < Classic @@ -8,35 +8,35 @@ class Create < Classic read_config build end - + def guess_paths(path) # if no path is supplied uses current directory for project path = FileUtils.pwd if path.nil? - + # if a www is found use it for the project path = File.join(path, 'www') if File.exists? File.join(path, 'www') - + # defaults @name = path.split("/").last.gsub('-','').gsub(' ','') # no dashses nor spaces @path = File.join(path, '..', "#{ @name }_android") - @www = path - @pkg = "com.phonegap.#{ @name }" + @www = path + @pkg = "com.phonegap.#{ @name }" @android_sdk_path = Dir.getwd[0,1] != "/" ? `android-sdk-path.bat android.bat`.gsub('\\tools','').gsub('\\', '\\\\\\\\') : `which android`.gsub(/\/tools\/android$/,'').chomp @android_dir = File.expand_path(File.dirname(__FILE__).gsub('lib','')) @framework_dir = File.join(@android_dir, "framework") @icon = File.join(@www, 'icon.png') @app_js_dir = '' @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 # reads in a config.xml file def read_config config_file = File.join(@www, 'config.xml') - + if File.exists?(config_file) require 'rexml/document' f = File.new config_file @@ -47,9 +47,9 @@ class Create < Classic @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[:content] = n.attributes["src"] if n.name == 'content' + @config[:name] = n.text.gsub('-','').gsub(' ','') if n.name == 'name' + @config[:description] = n.text if n.name == 'description' + @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"] @@ -74,12 +74,12 @@ class Create < Classic end end - + if n.name == "preference" && n.attributes["name"] == 'javascript_folder' @config[:js_dir] = n.attributes["value"] - end - end - + end + end + # extract android specific stuff @config[:versionCode] = doc.elements["//android:versionCode"] ? doc.elements["//android:versionCode"].text : 3 @config[:minSdkVersion] = doc.elements["//android:minSdkVersion"] ? doc.elements["//android:minSdkVersion"].text : 1 @@ -92,6 +92,6 @@ class Create < Classic @app_js_dir = @config[:js_dir] ? @config[:js_dir] : '' # sets the start page @content = @config[:content] ? @config[:content] : 'index.html' - end - end + end + end end diff --git a/lib/update.rb b/lib/update.rb index aa1459a1..fa960add 100755 --- a/lib/update.rb +++ b/lib/update.rb @@ -33,12 +33,19 @@ class Update # TODO need to allow for www import inc icon def copy_libs 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") + version = IO.read(File.join(@framework_dir, '../VERSION')) - FileUtils.mkdir_p File.join(@path, "assets", "www") - FileUtils.cp File.join(@framework_dir, "assets", "www", "phonegap.js"), File.join(@path, "assets", "www") + FileUtils.cp File.join(@framework_dir, "phonegap-#{ version }.jar"), File.join(@path, "libs") + + # 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") + 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-#{ version }.js"), 'w') {|f| f.write(phonegapjs) } end # end \ No newline at end of file diff --git a/util/yuicompressor/LICENSE b/util/yuicompressor/LICENSE deleted file mode 100755 index c364b9da..00000000 --- a/util/yuicompressor/LICENSE +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100755 index 1604846c..00000000 --- a/util/yuicompressor/README +++ /dev/null @@ -1,140 +0,0 @@ -============================================================================== -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 deleted file mode 100755 index c29470bd..00000000 Binary files a/util/yuicompressor/yuicompressor-2.4.2.jar and /dev/null differ