From 9d3306ccc5eb83032d693a3d5271e000077bde03 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Fri, 17 Sep 2010 15:37:34 -0500 Subject: [PATCH 01/13] Add comments to directory manager source file. --- .../src/com/phonegap/DirectoryManager.java | 147 ++++++++++++++---- 1 file changed, 118 insertions(+), 29 deletions(-) diff --git a/framework/src/com/phonegap/DirectoryManager.java b/framework/src/com/phonegap/DirectoryManager.java index 2b411a6a..6ee0d46e 100644 --- a/framework/src/com/phonegap/DirectoryManager.java +++ b/framework/src/com/phonegap/DirectoryManager.java @@ -6,26 +6,47 @@ import android.os.Environment; import android.os.StatFs; import android.util.Log; +/** + * This class provides file directory utilities. + * All file operations are performed on the SD card. + * + * It is used by the FileUtils class. + */ public class DirectoryManager { - protected static boolean testFileExists (String name){ + /** + * Determine if a file or directory exists. + * + * @param name The name of the file to check. + * @return T=exists, F=not found + */ + protected static boolean testFileExists(String name) { boolean status; - if ((testSaveLocationExists())&&(!name.equals(""))){ + + // If SD card exists + if ((testSaveLocationExists()) && (!name.equals(""))) { File path = Environment.getExternalStorageDirectory(); File newPath = constructFilePaths(path.toString(), name); status = newPath.exists(); - }else{ + } + + // If no SD card + else{ status = false; } return status; } - protected static long getFreeDiskSpace(){ - /* - * gets the available SD card free space or returns -1 if the SD card is not mounted. - */ + /** + * Get the free disk space on the SD card + * + * @return Size in KB or -1 if not available + */ + protected static long getFreeDiskSpace() { String status = Environment.getExternalStorageState(); long freeSpace = 0; + + // If SD card exists if (status.equals(Environment.MEDIA_MOUNTED)) { try { File path = Environment.getExternalStorageDirectory(); @@ -34,44 +55,82 @@ public class DirectoryManager { long availableBlocks = stat.getAvailableBlocks(); freeSpace = availableBlocks*blockSize/1024; } catch (Exception e) {e.printStackTrace(); } - } else { return -1; } + } + + // If no SD card, then return -1 + else { + return -1; + } + return (freeSpace); } - protected static boolean createDirectory(String directoryName){ + /** + * Create directory on SD card. + * + * @param directoryName The name of the directory to create. + * @return T=successful, F=failed + */ + protected static boolean createDirectory(String directoryName) { boolean status; - if ((testSaveLocationExists())&&(!directoryName.equals(""))){ + + // Make sure SD card exists + if ((testSaveLocationExists()) && (!directoryName.equals(""))) { File path = Environment.getExternalStorageDirectory(); File newPath = constructFilePaths(path.toString(), directoryName); status = newPath.mkdir(); status = true; - }else + } + + // If no SD card or invalid dir name + else { status = false; + } return status; } - protected static boolean testSaveLocationExists(){ + /** + * Determine if SD card exists. + * + * @return T=exists, F=not found + */ + protected static boolean testSaveLocationExists() { String sDCardStatus = Environment.getExternalStorageState(); boolean status; - if (sDCardStatus.equals(Environment.MEDIA_MOUNTED)){ + + // If SD card is mounted + if (sDCardStatus.equals(Environment.MEDIA_MOUNTED)) { status = true; - }else + } + + // If no SD card + else { status = false; + } return status; } - protected static boolean deleteDirectory(String fileName){ + /** + * Delete directory. + * + * @param fileName The name of the directory to delete + * @return T=deleted, F=could not delete + */ + protected static boolean deleteDirectory(String fileName) { boolean status; SecurityManager checker = new SecurityManager(); - if ((testSaveLocationExists())&&(!fileName.equals(""))){ - + // Make sure SD card exists + if ((testSaveLocationExists()) && (!fileName.equals(""))) { File path = Environment.getExternalStorageDirectory(); File newPath = constructFilePaths(path.toString(), fileName); checker.checkDelete(newPath.toString()); - if(newPath.isDirectory()){ + + // If dir to delete is really a directory + if (newPath.isDirectory()) { String[] listfile = newPath.list(); - // delete all files within the specified directory and then delete the directory + + // Delete all files within the specified directory and then delete the directory try{ for (int i=0; i < listfile.length; i++){ File deletedFile = new File (newPath.toString()+"/"+listfile[i].toString()); @@ -80,27 +139,43 @@ public class DirectoryManager { newPath.delete(); Log.i("DirectoryManager deleteDirectory", fileName); status = true; - }catch (Exception e){ + } + catch (Exception e){ e.printStackTrace(); status = false; } - - }else + } + + // If dir not a directory, then error + else { status = false; - }else + } + } + + // If no SD card + else { status = false; + } return status; } - protected static boolean deleteFile(String fileName){ + /** + * Delete file. + * + * @param fileName The name of the file to delete + * @return T=deleted, F=not deleted + */ + protected static boolean deleteFile(String fileName) { boolean status; SecurityManager checker = new SecurityManager(); - if ((testSaveLocationExists())&&(!fileName.equals(""))){ - + // Make sure SD card exists + if ((testSaveLocationExists()) && (!fileName.equals(""))) { File path = Environment.getExternalStorageDirectory(); File newPath = constructFilePaths(path.toString(), fileName); checker.checkDelete(newPath.toString()); + + // If file to delete is really a file if (newPath.isFile()){ try { Log.i("DirectoryManager deleteFile", fileName); @@ -110,14 +185,28 @@ public class DirectoryManager { se.printStackTrace(); status = false; } - }else + } + // If not a file, then error + else { status = false; - }else + } + } + + // If no SD card + else { status = false; + } return status; } - private static File constructFilePaths (String file1, String file2){ + /** + * Create a new file object from two file paths. + * + * @param file1 Base file path + * @param file2 Remaining file path + * @return File object + */ + private static File constructFilePaths (String file1, String file2) { File newPath; newPath = new File(file1+"/"+file2); return newPath; From b95ad44c18a42c173e5bf24a6bf601d865e29131 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Fri, 17 Sep 2010 15:38:18 -0500 Subject: [PATCH 02/13] Add general ERROR to plugin result status. --- framework/src/com/phonegap/api/PluginResult.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/framework/src/com/phonegap/api/PluginResult.java b/framework/src/com/phonegap/api/PluginResult.java index fbfb281f..777f2605 100755 --- a/framework/src/com/phonegap/api/PluginResult.java +++ b/framework/src/com/phonegap/api/PluginResult.java @@ -63,7 +63,8 @@ public class PluginResult { "Malformed url", "IO error", "Invalid action", - "JSON error" + "JSON error", + "Error" }; public enum Status { @@ -74,6 +75,7 @@ public class PluginResult { MALFORMED_URL_EXCEPTION, IO_EXCEPTION, INVALID_ACTION, - JSON_EXCEPTION + JSON_EXCEPTION, + ERROR } } From 00dc18a488189b7c56da206402702d7b0814c000 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Fri, 17 Sep 2010 15:41:48 -0500 Subject: [PATCH 03/13] Convert FileUtils to plugin architecture. --- framework/src/com/phonegap/DroidGap.java | 4 +- framework/src/com/phonegap/FileUtils.java | 336 +++++++++++++++------- 2 files changed, 238 insertions(+), 102 deletions(-) diff --git a/framework/src/com/phonegap/DroidGap.java b/framework/src/com/phonegap/DroidGap.java index 4786d357..56eeb0e7 100755 --- a/framework/src/com/phonegap/DroidGap.java +++ b/framework/src/com/phonegap/DroidGap.java @@ -82,7 +82,6 @@ public class DroidGap extends Activity { protected Boolean loadInWebView = false; private LinearLayout root; - private FileUtils fs; private BrowserKey mKey; public CallbackServer callbackServer; private PluginManager pluginManager; @@ -266,13 +265,11 @@ public class DroidGap extends Activity { private void bindBrowser(WebView appView) { this.callbackServer = new CallbackServer(); this.pluginManager = new PluginManager(appView, this); - this.fs = new FileUtils(appView, this); this.mKey = new BrowserKey(appView, this); // This creates the new javascript interfaces for PhoneGap appView.addJavascriptInterface(this.pluginManager, "PluginManager"); - appView.addJavascriptInterface(this.fs, "FileUtil"); appView.addJavascriptInterface(this.mKey, "BackButton"); appView.addJavascriptInterface(this.callbackServer, "CallbackServer"); @@ -296,6 +293,7 @@ public class DroidGap extends Activity { 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"); this.addService("Network Status", "com.phonegap.NetworkManager"); this.addService("Storage", "com.phonegap.Storage"); diff --git a/framework/src/com/phonegap/FileUtils.java b/framework/src/com/phonegap/FileUtils.java index 3f54f9b6..1e538080 100644 --- a/framework/src/com/phonegap/FileUtils.java +++ b/framework/src/com/phonegap/FileUtils.java @@ -2,123 +2,261 @@ package com.phonegap; import java.io.*; +import org.apache.commons.codec.binary.Base64; +import org.json.JSONArray; +import org.json.JSONException; + +import com.phonegap.api.Plugin; +import com.phonegap.api.PluginResult; + +import android.content.Intent; import android.webkit.WebView; -public class FileUtils { +/** + * This class provides SD card file and directory services to JavaScript. + * Only files on the SD card can be accessed. + */ +public class FileUtils implements Plugin { + + public static int NOT_FOUND_ERR = 8; + public static int SECURITY_ERR = 18; + public static int ABORT_ERR = 20; + + public static int NOT_READABLE_ERR = 24; + public static int ENCODING_ERR = 26; - WebView mView; + WebView webView; // WebView object + DroidGap ctx; // DroidGap object + FileReader f_in; FileWriter f_out; - public FileUtils(WebView view, DroidGap gap) - { - mView = view; + /** + * Constructor. + */ + public FileUtils() { + System.out.println("FileUtils()"); } - - public int testSaveLocationExists(){ - if (DirectoryManager.testSaveLocationExists()) - return 0; - else - return 1; - } - - public long getFreeDiskSpace(){ - long freeDiskSpace=DirectoryManager.getFreeDiskSpace(); - return freeDiskSpace; - } - public int testFileExists(String file){ - if (DirectoryManager.testFileExists(file)) - return 0; - else - return 1; - } - - public int testDirectoryExists(String file){ - if (DirectoryManager.testFileExists(file)) - return 0; - else - return 1; - } - - /** - * Delete a specific directory. - * Everyting in side the directory would be gone. - * TODO: JavaScript Call backs for success and error handling + /** + * Sets the context of the Command. This can then be used to do things like + * get file paths associated with the Activity. + * + * @param ctx The context of the main Activity. */ - public int deleteDirectory (String dir){ - if (DirectoryManager.deleteDirectory(dir)) - return 0; - else - return 1; - } - + public void setContext(DroidGap ctx) { + this.ctx = ctx; + } - /** - * Delete a specific file. - * TODO: JavaScript Call backs for success and error handling + /** + * Sets the main View of the application, this is the WebView within which + * a PhoneGap app runs. + * + * @param webView The PhoneGap WebView */ - public int deleteFile (String file){ - if (DirectoryManager.deleteFile(file)) - return 0; - else - return 1; - } - + public void setView(WebView webView) { + this.webView = webView; + } - /** - * Create a new directory. - * TODO: JavaScript Call backs for success and error handling + /** + * Executes the request and returns CommandResult. + * + * @param action The command to execute. + * @param args JSONArry of arguments for the command. + * @return A CommandResult object with a status and message. */ - public int createDirectory(String dir){ - if (DirectoryManager.createDirectory(dir)) - return 0; - else - return 1; - } - - public String read(String filename) - { - String data = ""; - String output = ""; - try { - FileInputStream fstream = new FileInputStream(filename); - DataInputStream in = new DataInputStream(fstream); - while (in.available() !=0) - { - data += in.readLine(); - } - - } catch (FileNotFoundException e) { - data = "FAIL: File not found"; - } catch (IOException e) { - data = "FAIL: IO ERROR"; - } + public PluginResult execute(String action, JSONArray args) { + PluginResult.Status status = PluginResult.Status.OK; + String result = ""; + //System.out.println("FileUtils.execute("+action+")"); - //mView.loadUrl("javascript:navigator.FileReader.hasRead('" + data + "')"); + try { + if (action.equals("testSaveLocationExists")) { + boolean b = DirectoryManager.testSaveLocationExists(); + return new PluginResult(status, b); + } + else if (action.equals("getFreeDiskSpace")) { + long l = DirectoryManager.getFreeDiskSpace(); + return new PluginResult(status, l); + } + else if (action.equals("testFileExists")) { + boolean b = DirectoryManager.testFileExists(args.getString(0)); + return new PluginResult(status, b); + } + else if (action.equals("testDirectoryExists")) { + boolean b = DirectoryManager.testFileExists(args.getString(0)); + return new PluginResult(status, b); + } + else if (action.equals("deleteDirectory")) { + boolean b = DirectoryManager.deleteDirectory(args.getString(0)); + return new PluginResult(status, b); + } + else if (action.equals("deleteFile")) { + boolean b = DirectoryManager.deleteFile(args.getString(0)); + return new PluginResult(status, b); + } + else if (action.equals("createDirectory")) { + boolean b = DirectoryManager.createDirectory(args.getString(0)); + return new PluginResult(status, b); + } + else if (action.equals("readAsText")) { + try { + String s = this.readAsText(args.getString(0), args.getString(1)); + return new PluginResult(status, s); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_FOUND_ERR); + } catch (IOException e) { + e.printStackTrace(); + return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR); + } + } + else if (action.equals("readAsDataURL")) { + try { + String s = this.readAsDataURL(args.getString(0)); + return new PluginResult(status, s); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_FOUND_ERR); + } catch (IOException e) { + e.printStackTrace(); + return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR); + } + } + else if (action.equals("writeAsText")) { + try { + this.writeAsText(args.getString(0), args.getString(1), args.getBoolean(2)); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_FOUND_ERR); + } catch (IOException e) { + e.printStackTrace(); + return new PluginResult(PluginResult.Status.ERROR, FileUtils.NOT_READABLE_ERR); + } + } + return new PluginResult(status, result); + } catch (JSONException e) { + e.printStackTrace(); + return new PluginResult(PluginResult.Status.JSON_EXCEPTION); + } + } + + /** + * Identifies if action to be executed returns a value and should be run synchronously. + * + * @param action The action to execute + * @return T=returns value + */ + public boolean isSynch(String action) { + if (action.equals("read")) { + return false; + } + else if (action.equals("write")) { + return false; + } + return true; + } + + /** + * Called when the system is about to start resuming a previous activity. + */ + public void onPause() { + } + + /** + * Called when the activity will start interacting with the user. + */ + public void onResume() { + } + + /** + * Called by AccelBroker when listener is to be shut down. + * Stop listener. + */ + public void onDestroy() { + } + + /** + * Called when an activity you launched exits, giving you the requestCode you started it with, + * the resultCode it returned, and any additional data from it. + * + * @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 data An Intent, which can return result data to the caller (various data can be attached to Intent "extras"). + */ + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + } + + //-------------------------------------------------------------------------- + // LOCAL METHODS + //-------------------------------------------------------------------------- + + /** + * Read content of text file. + * + * @param filename The name of the file. + * @param encoding The encoding to return contents as. Typical value is UTF-8. + * (see http://www.iana.org/assignments/character-sets) + * @return Contents of file. + * @throws FileNotFoundException, IOException + */ + public String readAsText(String filename, String encoding) throws FileNotFoundException, IOException { + System.out.println("FileUtils.readAsText("+filename+", "+encoding+")"); + StringBuilder data = new StringBuilder(); + FileInputStream fis = new FileInputStream(filename); + BufferedReader reader = new BufferedReader(new InputStreamReader(fis, encoding), 1024); + String line; + while ((line = reader.readLine()) != null) { + data.append(line); + } + return data.toString(); + } + + /** + * Read content of text file and return as base64 encoded data url. + * + * @param filename The name of the file. + * @return Contents of file = data:;base64, + * @throws FileNotFoundException, IOException + */ + public String readAsDataURL(String filename) throws FileNotFoundException, IOException { + byte[] bytes = new byte[1000]; + BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filename), 1024); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int numRead = 0; + while ((numRead = bis.read(bytes, 0, 1000)) >= 0) { + bos.write(bytes, 0, numRead); + } + + // Determine content type from file name + // TODO + String contentType = ""; + + byte[] base64 = Base64.encodeBase64(bos.toByteArray()); + String data = "data:" + contentType + ";base64," + new String(base64); return data; } - public int write(String filename, String data, boolean append) - { - String FilePath= filename; - try { - byte [] rawData = data.getBytes(); - ByteArrayInputStream in = new ByteArrayInputStream(rawData); - FileOutputStream out= new FileOutputStream(FilePath, append); - byte buff[] = new byte[rawData.length]; - in.read(buff, 0, buff.length); - out.write(buff, 0, rawData.length); - out.flush(); - out.close(); - //mView.loadUrl("javascript:navigator.FileReader.onsuccess('File written')"); - } catch (Exception e) { - //mView.loadUrl("javascript:navigator.FileReader.onerror('Fail')"); - // So, do we just return -1 at this point! - return -1; - } - return 0; + /** + * Write contents of file. + * + * @param filename The name of the file. + * @param data The contents of the file. + * @param append T=append, F=overwrite + * @throws FileNotFoundException, IOException + */ + public void writeAsText(String filename, String data, boolean append) throws FileNotFoundException, IOException { + String FilePath= filename; + byte [] rawData = data.getBytes(); + ByteArrayInputStream in = new ByteArrayInputStream(rawData); + FileOutputStream out= new FileOutputStream(FilePath, append); + byte buff[] = new byte[rawData.length]; + in.read(buff, 0, buff.length); + out.write(buff, 0, rawData.length); + out.flush(); + out.close(); } From 2d4a321cc167f9cbae8512f19081f0e20cb0c6c9 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Fri, 17 Sep 2010 15:43:20 -0500 Subject: [PATCH 04/13] Update files.js to be closer to iPhone impl and W3C File API working draft at http://www.w3.org/TR/FileAPI/. --- framework/assets/js/file.js | 601 +++++++++++++++++++++++------------- 1 file changed, 393 insertions(+), 208 deletions(-) mode change 100644 => 100755 framework/assets/js/file.js diff --git a/framework/assets/js/file.js b/framework/assets/js/file.js old mode 100644 new mode 100755 index e32bbfd7..29c88980 --- a/framework/assets/js/file.js +++ b/framework/assets/js/file.js @@ -1,226 +1,411 @@ +/** + * This class provides generic read and write access to the mobile device file system. + * They are not used to read files from a server. + */ +/** + * List of files + */ +function FileList() { + this.files = {}; +}; +/** + * Describes a single file in a FileList + */ +function File() { + this.name = null; + this.type = null; + this.urn = null; +}; -PhoneGap.addConstructor(function() { if (typeof navigator.fileMgr == "undefined") navigator.fileMgr = new FileMgr();}); +/** + * Create an event object since we can't set target on DOM event. + * + * @param type + * @param target + * + */ +File._createEvent = function(type, target) { + // Can't create event object, since we can't set target (its readonly) + //var evt = document.createEvent('Events'); + //evt.initEvent("onload", false, false); + var evt = {"type": type}; + evt.target = target; + return evt; +}; + +function FileError() { + // File error codes + // Found in DOMException + this.NOT_FOUND_ERR = 8; + this.SECURITY_ERR = 18; + this.ABORT_ERR = 20; + + // Added by this specification + this.NOT_READABLE_ERR = 24; + this.ENCODING_ERR = 26; + + this.code = null; +}; + +//----------------------------------------------------------------------------- +// File manager +//----------------------------------------------------------------------------- + +function FileMgr() { +}; + +FileMgr.prototype.getFileBasePaths = function() { +}; + +FileMgr.prototype.testSaveLocationExists = function(successCallback, errorCallback) { + PhoneGap.execAsync(successCallback, errorCallback, "File", "testSaveLocationExists", []); +}; + +FileMgr.prototype.testFileExists = function(fileName, successCallback, errorCallback) { + PhoneGap.execAsync(successCallback, errorCallback, "File", "testFileExists", [fileName]); +}; + +FileMgr.prototype.testDirectoryExists = function(dirName, successCallback, errorCallback) { + PhoneGap.execAsync(successCallback, errorCallback, "File", "testDirectoryExists", [dirName]); +}; + +FileMgr.prototype.createDirectory = function(dirName, successCallback, errorCallback) { + PhoneGap.execAsync(successCallback, errorCallback, "File", "createDirectory", [dirName]); +}; + +FileMgr.prototype.deleteDirectory = function(dirName, successCallback, errorCallback) { + PhoneGap.execAsync(successCallback, errorCallback, "File", "deleteDirectory", [dirName]); +}; + +FileMgr.prototype.deleteFile = function(fileName, successCallback, errorCallback) { + PhoneGap.execAsync(successCallback, errorCallback, "File", "deleteFile", [fileName]); +}; + +FileMgr.prototype.getFreeDiskSpace = function(successCallback, errorCallback) { + PhoneGap.execAsync(successCallback, errorCallback, "File", "getFreeDiskSpace", []); +}; + +FileMgr.prototype.writeAsText = function(fileName, data, append, successCallback, errorCallback) { + PhoneGap.execAsync(successCallback, errorCallback, "File", "writeAsText", [fileName, data, append]); +}; + +FileMgr.prototype.readAsText = function(fileName, encoding, successCallback, errorCallback) { + PhoneGap.execAsync(successCallback, errorCallback, "File", "readAsText", [fileName, encoding]); +}; + +FileMgr.prototype.readAsDataURL = function(fileName, successCallback, errorCallback) { + PhoneGap.execAsync(successCallback, errorCallback, "File", "readAsDataURL", [fileName]); +}; + +PhoneGap.addConstructor(function() { + if (typeof navigator.fileMgr == "undefined") navigator.fileMgr = new FileMgr(); +}); + +//----------------------------------------------------------------------------- +// File Reader +//----------------------------------------------------------------------------- +// TODO: All other FileMgr function operate on the SD card as root. However, +// for FileReader & FileWriter the root is not SD card. Should this be changed? + +/** + * This class reads the mobile device file system. + * + * For Android: + * The root directory is the root of the file system. + * To read from the SD card, the file name is "sdcard/my_file.txt" + */ +function FileReader() { + this.fileName = ""; + + this.readyState = 0; + + // File data + this.result = null; + + // Error + this.error = null; + + // Event handlers + this.onloadstart = null; // When the read starts. + this.onprogress = null; // While reading (and decoding) file or fileBlob data, and reporting partial file data (progess.loaded/progress.total) + this.onload = null; // When the read has successfully completed. + this.onerror = null; // When the read has failed (see errors). + this.onloadend = null; // When the request has completed (either in success or failure). + this.onabort = null; // When the read has been aborted. For instance, by invoking the abort() method. +}; + +// States +FileReader.EMPTY = 0; +FileReader.LOADING = 1; +FileReader.DONE = 2; + +/** + * Abort reading file. + */ +FileReader.prototype.abort = function() { + this.readyState = FileReader.DONE; + + // If abort callback + if (typeof this.onabort == "function") { + var evt = File._createEvent("abort", this); + this.onabort(evt); + } + + // TODO: Anything else to do? Maybe sent to native? +}; + +/** + * Read text file. + * + * @param file The name of the file + * @param encoding [Optional] (see http://www.iana.org/assignments/character-sets) + */ +FileReader.prototype.readAsText = function(file, encoding) { + this.fileName = file; + + // LOADING state + this.readyState = FileReader.LOADING; + + // If loadstart callback + if (typeof this.onloadstart == "function") { + var evt = File._createEvent("loadstart", this); + this.onloadstart(evt); + } + + // Default encoding is UTF-8 + var enc = encoding ? encoding : "UTF-8"; + + var me = this; + + // Read file + navigator.fileMgr.readAsText(file, enc, + + // Success callback + function(r) { + + // If DONE (cancelled), then don't do anything + if (me.readyState == FileReader.DONE) { + return; + } + + // Save result + me.result = r; + + // DONE state + me.readyState = FileReader.DONE; + + // If onload callback + if (typeof me.onload == "function") { + var evt = File._createEvent("load", me); + me.onload(evt); + } + + // If onloadend callback + if (typeof me.onloadend == "function") { + var evt = File._createEvent("loadend", me); + me.onloadend(evt); + } + }, + + // Error callback + function(e) { + + // If DONE (cancelled), then don't do anything + if (me.readyState == FileReader.DONE) { + return; + } + + // Save error + me.error = e; + + // DONE state + me.readyState = FileReader.DONE; + + // If onerror callback + if (typeof me.onerror == "function") { + var evt = File._createEvent("error", me); + me.onerror(evt); + } + + // If onloadend callback + if (typeof me.onloadend == "function") { + var evt = File._createEvent("loadend", me); + me.onloadend(evt); + } + } + ); +}; /** - * This class provides iPhone read and write access to the mobile device file system. - * Based loosely on http://www.w3.org/TR/2009/WD-FileAPI-20091117/#dfn-empty - */ -function FileMgr() -{ - this.fileWriters = {}; // empty maps - this.fileReaders = {}; - - this.docsFolderPath = "../../Documents"; - this.tempFolderPath = "../../tmp"; - this.freeDiskSpace = -1; - this.getFileBasePaths(); -} - -// private, called from Native Code -FileMgr.prototype._setPaths = function(docs,temp) -{ - this.docsFolderPath = docs; - this.tempFolderPath = temp; -} - -// private, called from Native Code -FileMgr.prototype._setFreeDiskSpace = function(val) -{ - this.freeDiskSpace = val; -} - - -// FileWriters add/remove -// called internally by writers -FileMgr.prototype.addFileWriter = function(filePath,fileWriter) -{ - this.fileWriters[filePath] = fileWriter; -} - -FileMgr.prototype.removeFileWriter = function(filePath) -{ - this.fileWriters[filePath] = null; -} - -// File readers add/remove -// called internally by readers -FileMgr.prototype.addFileReader = function(filePath,fileReader) -{ - this.fileReaders[filePath] = fileReader; -} - -FileMgr.prototype.removeFileReader = function(filePath) -{ - this.fileReaders[filePath] = null; -} - -/******************************************* + * Read file and return data as a base64 encoded data url. + * A data url is of the form: + * data:[][;base64], * - * private reader callback delegation - * called from native code + * @param file The name of the file */ -FileMgr.prototype.reader_onloadstart = function(filePath,result) -{ - this.fileReaders[filePath].onloadstart(result); -} +FileReader.prototype.readAsDataURL = function(file) { + this.fileName = file; -FileMgr.prototype.reader_onprogress = function(filePath,result) -{ - this.fileReaders[filePath].onprogress(result); -} + // LOADING state + this.readyState = FileReader.LOADING; -FileMgr.prototype.reader_onload = function(filePath,result) -{ - this.fileReaders[filePath].result = unescape(result); - this.fileReaders[filePath].onload(this.fileReaders[filePath].result); -} + // If loadstart callback + if (typeof this.onloadstart == "function") { + var evt = File._createEvent("loadstart", this); + this.onloadstart(evt); + } -FileMgr.prototype.reader_onerror = function(filePath,err) -{ - this.fileReaders[filePath].result = err; - this.fileReaders[filePath].onerror(err); -} + var me = this; -FileMgr.prototype.reader_onloadend = function(filePath,result) -{ - this.fileReaders[filePath].onloadend(result); -} + // Read file + navigator.fileMgr.readAsDataURL(file, -/******************************************* + // Success callback + function(r) { + + // If DONE (cancelled), then don't do anything + if (me.readyState == FileReader.DONE) { + return; + } + + // Save result + me.result = r; + + // DONE state + me.readyState = FileReader.DONE; + + // If onload callback + if (typeof me.onload == "function") { + var evt = File._createEvent("load", me); + me.onload(evt); + } + + // If onloadend callback + if (typeof me.onloadend == "function") { + var evt = File._createEvent("loadend", me); + me.onloadend(evt); + } + }, + + // Error callback + function(e) { + + // If DONE (cancelled), then don't do anything + if (me.readyState == FileReader.DONE) { + return; + } + + // Save error + me.error = e; + + // DONE state + me.readyState = FileReader.DONE; + + // If onerror callback + if (typeof me.onerror == "function") { + var evt = File._createEvent("error", me); + me.onerror(evt); + } + + // If onloadend callback + if (typeof me.onloadend == "function") { + var evt = File._createEvent("loadend", me); + me.onloadend(evt); + } + } + ); +}; + +/** + * Read file and return data as a binary data. * - * private writer callback delegation - * called from native code -*/ -FileMgr.prototype.writer_onerror = function(filePath,err) -{ - this.fileWriters[filePath].onerror(err); -} - -FileMgr.prototype.writer_oncomplete = function(filePath,result) -{ - this.fileWriters[filePath].oncomplete(result); // result contains bytes written -} - - -FileMgr.prototype.getFileBasePaths = function() -{ - //PhoneGap.exec("File.getFileBasePaths"); -} - -FileMgr.prototype.testFileExists = function(fileName, successCallback, errorCallback) -{ - var test = FileUtil.testFileExists(fileName); - test ? successCallback() : errorCallback(); -} - -FileMgr.prototype.testDirectoryExists = function(dirName, successCallback, errorCallback) -{ - this.successCallback = successCallback; - this.errorCallback = errorCallback; - var test = FileUtil.testDirectoryExists(dirName); - test ? successCallback() : errorCallback(); -} - -FileMgr.prototype.createDirectory = function(dirName, successCallback, errorCallback) -{ - this.successCallback = successCallback; - this.errorCallback = errorCallback; - var test = FileUtil.createDirectory(dirName); - test ? successCallback() : errorCallback(); -} - -FileMgr.prototype.deleteDirectory = function(dirName, successCallback, errorCallback) -{ - this.successCallback = successCallback; - this.errorCallback = errorCallback; - var test = FileUtil.deleteDirectory(dirName); - test ? successCallback() : errorCallback(); -} - -FileMgr.prototype.deleteFile = function(fileName, successCallback, errorCallback) -{ - this.successCallback = successCallback; - this.errorCallback = errorCallback; - FileUtil.deleteFile(fileName); - test ? successCallback() : errorCallback(); -} - -FileMgr.prototype.getFreeDiskSpace = function(successCallback, errorCallback) -{ - if(this.freeDiskSpace > 0) - { - return this.freeDiskSpace; - } - else - { - this.successCallback = successCallback; - this.errorCallback = errorCallback; - this.freeDiskSpace = FileUtil.getFreeDiskSpace(); - (this.freeDiskSpace > 0) ? successCallback() : errorCallback(); - } -} - - -// File Reader - - -function FileReader() -{ - this.fileName = ""; - this.result = null; - this.onloadstart = null; - this.onprogress = null; - this.onload = null; - this.onerror = null; - this.onloadend = null; -} - - -FileReader.prototype.abort = function() -{ - // Not Implemented -} - -FileReader.prototype.readAsText = function(file) -{ - if(this.fileName && this.fileName.length > 0) - { - navigator.fileMgr.removeFileReader(this.fileName,this); - } - this.fileName = file; - navigator.fileMgr.addFileReader(this.fileName,this); - - return FileUtil.read(this.fileName); -} + * @param file The name of the file + */ +FileReader.prototype.readAsBinaryString = function(file) { + // TODO - Can't return binary data to browser. + this.fileName = file; +}; +//----------------------------------------------------------------------------- // File Writer +//----------------------------------------------------------------------------- -function FileWriter() -{ - this.fileName = ""; - this.result = null; - this.readyState = 0; // EMPTY - this.result = null; - this.onerror = null; - this.oncomplete = null; -} +/** + * This class writes to the mobile device file system. + * + * For Android: + * The root directory is the root of the file system. + * To write to the SD card, the file name is "sdcard/my_file.txt" + */ +function FileWriter() { + this.fileName = ""; + this.result = null; + this.readyState = 0; // EMPTY + this.result = null; + this.onerror = null; + this.oncomplete = null; +}; + +FileWriter.prototype.writeAsText = function(file, text, bAppend) { + if (bAppend != true) { + bAppend = false; // for null values + } + + this.fileName = file; + + // LOADING state + this.readyState = FileReader.LOADING; + + var me = this; + + // Read file + navigator.fileMgr.writeAsText(file, text, bAppend, + + // Success callback + function(r) { + + // If DONE (cancelled), then don't do anything + if (me.readyState == FileReader.DONE) { + return; + } + + // Save result + me.result = r; + + // DONE state + me.readyState = FileReader.DONE; + + // If oncomplete callback + if (typeof me.oncomplete == "function") { + var evt = File._createEvent("complete", me); + me.oncomplete(evt); + } + }, + + // Error callback + function(e) { + + // If DONE (cancelled), then don't do anything + if (me.readyState == FileReader.DONE) { + return; + } + + // Save error + me.error = e; + + // DONE state + me.readyState = FileReader.DONE; + + // If onerror callback + if (typeof me.onerror == "function") { + var evt = File._createEvent("error", me); + me.onerror(evt); + } + } + ); + +}; -FileWriter.prototype.writeAsText = function(file,text,bAppend) -{ - if(this.fileName && this.fileName.length > 0) - { - navigator.fileMgr.removeFileWriter(this.fileName,this); - } - this.fileName = file; - if(bAppend != true) - { - bAppend = false; // for null values - } - navigator.fileMgr.addFileWriter(file,this); - this.readyState = 0; // EMPTY - var call = FileUtil.write(file, text, bAppend); - this.result = null; -} From 0ed522481fe61c6f1911f11b26a75d450ee673f8 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Fri, 17 Sep 2010 16:05:05 -0500 Subject: [PATCH 05/13] Read and write operations are async. --- framework/src/com/phonegap/FileUtils.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) mode change 100644 => 100755 framework/src/com/phonegap/FileUtils.java diff --git a/framework/src/com/phonegap/FileUtils.java b/framework/src/com/phonegap/FileUtils.java old mode 100644 new mode 100755 index 1e538080..598c23e9 --- a/framework/src/com/phonegap/FileUtils.java +++ b/framework/src/com/phonegap/FileUtils.java @@ -149,10 +149,13 @@ public class FileUtils implements Plugin { * @return T=returns value */ public boolean isSynch(String action) { - if (action.equals("read")) { + if (action.equals("readAsText")) { return false; } - else if (action.equals("write")) { + else if (action.equals("readAsDataURL")) { + return false; + } + else if (action.equals("writeAsText")) { return false; } return true; From eff7c92dae13660d46a4bc85056b6e281be82f61 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Fri, 17 Sep 2010 16:17:06 -0500 Subject: [PATCH 06/13] FileWriter should use its own states object. --- framework/assets/js/file.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/framework/assets/js/file.js b/framework/assets/js/file.js index 29c88980..b4037a9c 100755 --- a/framework/assets/js/file.js +++ b/framework/assets/js/file.js @@ -349,6 +349,11 @@ function FileWriter() { this.oncomplete = null; }; +// States +FileWriter.EMPTY = 0; +FileWriter.LOADING = 1; +FileWriter.DONE = 2; + FileWriter.prototype.writeAsText = function(file, text, bAppend) { if (bAppend != true) { bAppend = false; // for null values @@ -357,7 +362,7 @@ FileWriter.prototype.writeAsText = function(file, text, bAppend) { this.fileName = file; // LOADING state - this.readyState = FileReader.LOADING; + this.readyState = FileWriter.LOADING; var me = this; @@ -368,7 +373,7 @@ FileWriter.prototype.writeAsText = function(file, text, bAppend) { function(r) { // If DONE (cancelled), then don't do anything - if (me.readyState == FileReader.DONE) { + if (me.readyState == FileWriter.DONE) { return; } @@ -376,7 +381,7 @@ FileWriter.prototype.writeAsText = function(file, text, bAppend) { me.result = r; // DONE state - me.readyState = FileReader.DONE; + me.readyState = FileWriter.DONE; // If oncomplete callback if (typeof me.oncomplete == "function") { @@ -389,7 +394,7 @@ FileWriter.prototype.writeAsText = function(file, text, bAppend) { function(e) { // If DONE (cancelled), then don't do anything - if (me.readyState == FileReader.DONE) { + if (me.readyState == FileWriter.DONE) { return; } @@ -397,7 +402,7 @@ FileWriter.prototype.writeAsText = function(file, text, bAppend) { me.error = e; // DONE state - me.readyState = FileReader.DONE; + me.readyState = FileWriter.DONE; // If onerror callback if (typeof me.onerror == "function") { From a59cad68e25ab26b8d91377dbdb3f6d2d0ae4c22 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Fri, 17 Sep 2010 16:53:52 -0500 Subject: [PATCH 07/13] Device returns string, but for some reason emulator returns object - so convert to string. --- framework/assets/js/phonegap.js.base | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/framework/assets/js/phonegap.js.base b/framework/assets/js/phonegap.js.base index af0fb556..88a790d9 100755 --- a/framework/assets/js/phonegap.js.base +++ b/framework/assets/js/phonegap.js.base @@ -286,10 +286,12 @@ PhoneGap.execAsync = function(success, fail, clazz, action, args) { if (success || fail) { PhoneGap.callbacks[callbackId] = {success:success, fail:fail}; } - var r = PluginManager.exec(clazz, action, callbackId, this.stringify(args), true); + + // Note: Device returns string, but for some reason emulator returs object - so convert to string. + var r = ""+PluginManager.exec(clazz, action, callbackId, this.stringify(args), true); // If a result was returned - if ((typeof r == "string") && (r.length > 0)) { + if (r.length > 0) { eval("var v="+r+";"); // If status is OK, then return value back to caller From ace84227cca01e0894f7d90c9065174a0a15b600 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Mon, 20 Sep 2010 15:39:54 -0500 Subject: [PATCH 08/13] Fix problem with deviceready being called before device properties are guaranteed to be set. --- framework/assets/js/device.js | 30 +++++++++++-- framework/assets/js/phonegap.js.base | 67 +++++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 8 deletions(-) mode change 100644 => 100755 framework/assets/js/device.js diff --git a/framework/assets/js/device.js b/framework/assets/js/device.js old mode 100644 new mode 100755 index 450a0e32..a4f754f2 --- a/framework/assets/js/device.js +++ b/framework/assets/js/device.js @@ -12,22 +12,46 @@ function Device() { this.phonegap = null; var me = this; - PhoneGap.execAsync( + this.getInfo( function(info) { me.available = true; me.platform = info.platform; me.version = info.version; me.uuid = info.uuid; me.phonegap = info.phonegap; + PhoneGap.onPhoneGapInfoReady.fire(); }, function(e) { me.available = false; console.log("Error initializing PhoneGap: " + e); alert("Error initializing PhoneGap: "+e); - }, - "Device", "getDeviceInfo", []); + }); } +/** + * Get device info + * + * @param {Function} successCallback The function to call when the heading data is available + * @param {Function} errorCallback The function to call when there is an error getting the heading data. (OPTIONAL) + */ +Device.prototype.getInfo = function(successCallback, errorCallback) { + + // successCallback required + if (typeof successCallback != "function") { + console.log("Device Error: successCallback is not a function"); + return; + } + + // errorCallback optional + if (errorCallback && (typeof errorCallback != "function")) { + console.log("Device Error: errorCallback is not a function"); + return; + } + + // Get info + PhoneGap.execAsync(successCallback, errorCallback, "Device", "getDeviceInfo", []); +}; + /* * This is only for Android. * diff --git a/framework/assets/js/phonegap.js.base b/framework/assets/js/phonegap.js.base index 88a790d9..ff503b01 100755 --- a/framework/assets/js/phonegap.js.base +++ b/framework/assets/js/phonegap.js.base @@ -1,4 +1,25 @@ +/** + * The order of events during page load and PhoneGap startup is as follows: + * + * onDOMContentLoaded Internal event that is received when the web page is loaded and parsed. + * window.onload Browser's body onload event. + * onNativeReady Internal event that indicates the PhoneGap native side is ready. + * onPhoneGapInit Internal event that kicks off creatation all PhoneGap JavaScript objects (runs constructors) + * onPhoneGapReady Internal event fired when all PhoneGap JavaScript ojbects have been created + * onPhoneGapInfoReady Internal event fired when device properties are available + * onDeviceReady User event fired to indicate that PhoneGap is ready + * onResume User event fired to indicate a start/resume lifecycle event + * + * The only PhoneGap events that user code should register for are: + * onDeviceReady + * onResume + * + * Listeners can be registered as: + * document.addEventListener("deviceready", myDeviceReadyListener, false); + * document.addEventListener("resume", myResumeListener, false); + */ + if (typeof(DeviceInfo) != 'object') DeviceInfo = {}; @@ -124,7 +145,7 @@ PhoneGap.available = DeviceInfo.uuid != undefined; * @param {Function} func The function callback you want run once PhoneGap is initialized */ PhoneGap.addConstructor = function(func) { - PhoneGap.onDeviceReady.subscribeOnce(function() { + PhoneGap.onPhoneGapInit.subscribeOnce(function() { try { func(); } catch(e) { @@ -162,6 +183,23 @@ PhoneGap.onDOMContentLoaded = new PhoneGap.Channel('onDOMContentLoaded'); */ PhoneGap.onNativeReady = new PhoneGap.Channel('onNativeReady'); +/** + * onPhoneGapInit channel is fired when the web page is fully loaded and + * PhoneGap native code has been initialized. + */ +PhoneGap.onPhoneGapInit = new PhoneGap.Channel('onPhoneGapInit'); + +/** + * onPhoneGapReady channel is fired when the JS PhoneGap objects have been created. + */ +PhoneGap.onPhoneGapReady = new PhoneGap.Channel('onPhoneGapReady'); + +/** + * onPhoneGapInfoReady channel is fired when the PhoneGap device properties + * has been set. + */ +PhoneGap.onPhoneGapInfoReady = new PhoneGap.Channel('onPhoneGapInfoReady'); + /** * onResume channel is fired when the PhoneGap native code * resumes. @@ -180,22 +218,41 @@ PhoneGap.onPause = new PhoneGap.Channel('onPause'); if (typeof _nativeReady !== 'undefined') { PhoneGap.onNativeReady.fire(); } /** - * onDeviceReady is fired only after both onDOMContentLoaded and - * onNativeReady have fired. + * onDeviceReady is fired only after all PhoneGap objects are created and + * the device properties are set. */ PhoneGap.onDeviceReady = new PhoneGap.Channel('onDeviceReady'); +/** + * Start listening for XHR callbacks onDeviceReady + */ PhoneGap.onDeviceReady.subscribeOnce(function() { PhoneGap.JSCallback(); }); +/** + * Create all PhoneGap objects once page has fully loaded and native side is ready. + */ +PhoneGap.Channel.join(function() { + + // Run PhoneGap constructors + PhoneGap.onPhoneGapInit.fire(); + + // Fire event to notify that all objects are created + PhoneGap.onPhoneGapReady.fire(); + +}, [ PhoneGap.onDOMContentLoaded, PhoneGap.onNativeReady ]); + +/** + * Fire onDeviceReady event once all constructors have run and PhoneGap info has been + * received from native side. + */ PhoneGap.Channel.join(function() { PhoneGap.onDeviceReady.fire(); // Fire the onresume event, since first one happens before JavaScript is loaded PhoneGap.onResume.fire(); -}, [ PhoneGap.onDOMContentLoaded, PhoneGap.onNativeReady ]); - +}, [ PhoneGap.onPhoneGapReady, PhoneGap.onPhoneGapInfoReady]); // Listen for DOMContentLoaded and notify our channel subscribers document.addEventListener('DOMContentLoaded', function() { From b079a243730f044cdad2ff92bdd13eebf81c3ee0 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Mon, 20 Sep 2010 15:48:37 -0500 Subject: [PATCH 09/13] Need to listen for XHR callbacks before constructors are run, since constructors could call native code that returns data in callback. --- framework/assets/js/phonegap.js.base | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/framework/assets/js/phonegap.js.base b/framework/assets/js/phonegap.js.base index ff503b01..41a7064a 100755 --- a/framework/assets/js/phonegap.js.base +++ b/framework/assets/js/phonegap.js.base @@ -223,18 +223,15 @@ if (typeof _nativeReady !== 'undefined') { PhoneGap.onNativeReady.fire(); } */ PhoneGap.onDeviceReady = new PhoneGap.Channel('onDeviceReady'); -/** - * Start listening for XHR callbacks onDeviceReady - */ -PhoneGap.onDeviceReady.subscribeOnce(function() { - PhoneGap.JSCallback(); -}); /** * Create all PhoneGap objects once page has fully loaded and native side is ready. */ PhoneGap.Channel.join(function() { + // Start listening for XHR callbacks + PhoneGap.JSCallback(); + // Run PhoneGap constructors PhoneGap.onPhoneGapInit.fire(); From 1a9173d2c35ff93d9deb7f0716303595bc86b493 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Mon, 20 Sep 2010 21:09:35 -0500 Subject: [PATCH 10/13] Added comments. --- framework/assets/js/phonegap.js.base | 45 +++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/framework/assets/js/phonegap.js.base b/framework/assets/js/phonegap.js.base index 41a7064a..dbfe0343 100755 --- a/framework/assets/js/phonegap.js.base +++ b/framework/assets/js/phonegap.js.base @@ -3,10 +3,10 @@ * The order of events during page load and PhoneGap startup is as follows: * * onDOMContentLoaded Internal event that is received when the web page is loaded and parsed. - * window.onload Browser's body onload event. + * window.onload Body onload event. * onNativeReady Internal event that indicates the PhoneGap native side is ready. - * onPhoneGapInit Internal event that kicks off creatation all PhoneGap JavaScript objects (runs constructors) - * onPhoneGapReady Internal event fired when all PhoneGap JavaScript ojbects have been created + * onPhoneGapInit Internal event that kicks off creation of all PhoneGap JavaScript objects (runs constructors). + * onPhoneGapReady Internal event fired when all PhoneGap JavaScript objects have been created * onPhoneGapInfoReady Internal event fired when device properties are available * onDeviceReady User event fired to indicate that PhoneGap is ready * onResume User event fired to indicate a start/resume lifecycle event @@ -256,7 +256,6 @@ document.addEventListener('DOMContentLoaded', function() { PhoneGap.onDOMContentLoaded.fire(); }, false); - // Intercept calls to document.addEventListener and watch for deviceready PhoneGap.m_document_addEventListener = document.addEventListener; @@ -313,6 +312,7 @@ PhoneGap.callbacks = {}; * @param {String} command Command to be run in PhoneGap, e.g. "ClassName.method" * @param {String[]} [args] Zero or more arguments to pass to the method */ +// TODO: Not used anymore, should be removed. PhoneGap.exec = function(clazz, action, args) { try { var callbackId = 0; @@ -334,15 +334,29 @@ PhoneGap.exec = function(clazz, action, args) { } }; -PhoneGap.execAsync = function(success, fail, clazz, action, args) { +/** + * 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 "" + * If async, the native side will PhoneGap.callbackSuccess or PhoneGap.callbackError, + * depending upon the result of the action. + * + * @param {Function} success The success callback + * @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 + */ +PhoneGap.execAsync = function(success, fail, service, action, args) { try { - var callbackId = clazz + PhoneGap.callbackId++; + var callbackId = service + PhoneGap.callbackId++; if (success || fail) { PhoneGap.callbacks[callbackId] = {success:success, fail:fail}; } - // Note: Device returns string, but for some reason emulator returs object - so convert to string. - var r = ""+PluginManager.exec(clazz, action, callbackId, this.stringify(args), true); + // Note: Device returns string, but for some reason emulator returns object - so convert to string. + var r = ""+PluginManager.exec(service, action, callbackId, this.stringify(args), true); // If a result was returned if (r.length > 0) { @@ -376,6 +390,12 @@ PhoneGap.execAsync = function(success, fail, clazz, action, args) { } }; +/** + * Called by native code when returning successful result from an action. + * + * @param callbackId + * @param args + */ PhoneGap.callbackSuccess = function(callbackId, args) { if (PhoneGap.callbacks[callbackId]) { try { @@ -390,6 +410,12 @@ PhoneGap.callbackSuccess = function(callbackId, args) { } }; +/** + * Called by native code when returning error result from an action. + * + * @param callbackId + * @param args + */ PhoneGap.callbackError = function(callbackId, args) { if (PhoneGap.callbacks[callbackId]) { try { @@ -412,6 +438,7 @@ PhoneGap.callbackError = function(callbackId, args) { * url, which will be turned into a dictionary on the other end. * @private */ +// TODO: Is this used? PhoneGap.run_command = function() { if (!PhoneGap.available || !PhoneGap.queue.ready) return; @@ -451,6 +478,8 @@ PhoneGap.run_command = function() { }; /** + * This is only for Android. + * * 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. From 063e189bb7b7b84d72e6863f0f43918e70697960 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Mon, 20 Sep 2010 22:25:57 -0500 Subject: [PATCH 11/13] Change isReachable() to return NetworkStatus constant to reachableCallback(reachability) as specified in the API documentation. --- framework/assets/js/network.js | 39 ++----------- .../src/com/phonegap/NetworkManager.java | 56 +++++++++++++++---- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/framework/assets/js/network.js b/framework/assets/js/network.js index 720b8529..b3fb1f40 100755 --- a/framework/assets/js/network.js +++ b/framework/assets/js/network.js @@ -4,8 +4,8 @@ * @constructor */ function NetworkStatus() { - this.code = null; - this.message = ""; + //this.code = null; + //this.message = ""; }; NetworkStatus.NOT_REACHABLE = 0; @@ -42,38 +42,11 @@ Network.prototype.updateReachability = function(reachability) { * @param {Object} options (isIpAddress:boolean) */ Network.prototype.isReachable = function(uri, callback, options) { - - // callback required - if (typeof callback != "function") { - console.log("Network Error: callback is not a function"); - return; + var isIpAddress = false; + if (options && options.isIpAddress) { + isIpAddress = options.isIpAddress; } - - PhoneGap.execAsync( - function(status) { - - // If reachable, the check for wifi vs carrier - if (status) { - PhoneGap.execAsync( - function(wifi) { - var s = new NetworkStatus(); - if (wifi) { - s.code = NetworkStatus.REACHABLE_VIA_WIFI_NETWORK; - } - else { - s.code = NetworkStatus.REACHABLE_VIA_CARRIER_DATA_NETWORK; - } - callback(s); - }, null, "Network Status", "isWifiActive", []); - } - - // If not - else { - var s = new NetworkStatus(); - s.code = NetworkStatus.NOT_REACHABLE; - callback(s); - } - }, null, "Network Status", "isReachable", [uri]); + PhoneGap.execAsync(callback, null, "Network Status", "isReachable", [uri, isIpAddress]); }; PhoneGap.addConstructor(function() { diff --git a/framework/src/com/phonegap/NetworkManager.java b/framework/src/com/phonegap/NetworkManager.java index caf6efba..82c75fe5 100755 --- a/framework/src/com/phonegap/NetworkManager.java +++ b/framework/src/com/phonegap/NetworkManager.java @@ -14,6 +14,11 @@ import android.net.*; import android.webkit.WebView; public class NetworkManager implements Plugin { + + public static int NOT_REACHABLE = 0; + public static int REACHABLE_VIA_CARRIER_DATA_NETWORK = 1; + public static int REACHABLE_VIA_WIFI_NETWORK = 2; + WebView webView; // WebView object DroidGap ctx; // DroidGap object @@ -67,8 +72,8 @@ public class NetworkManager implements Plugin { return new PluginResult(status, b); } else if (action.equals("isReachable")) { - boolean b = this.isReachable(args.getString(0)); - return new PluginResult(status, b); + int i = this.isReachable(args.getString(0), args.getBoolean(1)); + return new PluginResult(status, i); } return new PluginResult(status, result); } catch (JSONException e) { @@ -122,6 +127,11 @@ public class NetworkManager implements Plugin { // LOCAL METHODS //-------------------------------------------------------------------------- + /** + * Determine if a network connection exists. + * + * @return + */ public boolean isAvailable() { NetworkInfo info = sockMan.getActiveNetworkInfo(); boolean conn = false; @@ -131,6 +141,11 @@ public class NetworkManager implements Plugin { return conn; } + /** + * Determine if a WIFI connection exists. + * + * @return + */ public boolean isWifiActive() { NetworkInfo info = sockMan.getActiveNetworkInfo(); if (info != null) { @@ -140,18 +155,37 @@ public class NetworkManager implements Plugin { return false; } - public boolean isReachable(String uri) { + /** + * Determine if a URI is reachable over the network. + * + * @param uri + * @param isIpAddress + * @return + */ + public int isReachable(String uri, boolean isIpAddress) { + int reachable = NOT_REACHABLE; + if (uri.indexOf("http://") == -1) { uri = "http://" + uri; } - boolean reached = isAvailable(); - try { - DefaultHttpClient httpclient = new DefaultHttpClient(); - HttpGet httpget = new HttpGet(uri); - httpclient.execute(httpget); - } catch (Exception e) { - reached = false; + + if (isAvailable()) { + try { + DefaultHttpClient httpclient = new DefaultHttpClient(); + HttpGet httpget = new HttpGet(uri); + httpclient.execute(httpget); + + if (isWifiActive()) { + reachable = REACHABLE_VIA_WIFI_NETWORK; + } + else { + reachable = REACHABLE_VIA_CARRIER_DATA_NETWORK; + } + } catch (Exception e) { + reachable = NOT_REACHABLE; + } } - return reached; + + return reachable; } } From edfa41c9f95cadcc07a920dae761a5751d32d71b Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Wed, 22 Sep 2010 14:47:52 -0500 Subject: [PATCH 12/13] Update geolocation to follow W3C spec, add comments, add error checking. --- framework/assets/js/geolocation.js | 211 ++++++++++++------ framework/src/com/phonegap/DroidGap.java | 2 +- framework/src/com/phonegap/GeoBroker.java | 65 +++++- framework/src/com/phonegap/GeoListener.java | 133 ++++++----- framework/src/com/phonegap/GpsListener.java | 149 +++++++++---- .../src/com/phonegap/NetworkListener.java | 145 ++++++++---- 6 files changed, 474 insertions(+), 231 deletions(-) mode change 100644 => 100755 framework/assets/js/geolocation.js mode change 100644 => 100755 framework/src/com/phonegap/GeoBroker.java mode change 100644 => 100755 framework/src/com/phonegap/GeoListener.java mode change 100644 => 100755 framework/src/com/phonegap/GpsListener.java mode change 100644 => 100755 framework/src/com/phonegap/NetworkListener.java diff --git a/framework/assets/js/geolocation.js b/framework/assets/js/geolocation.js old mode 100644 new mode 100755 index fec7b884..40f06da4 --- a/framework/assets/js/geolocation.js +++ b/framework/assets/js/geolocation.js @@ -3,85 +3,152 @@ * @constructor */ function Geolocation() { - /** - * The last known GPS position. - */ + + // The last known GPS position. this.lastPosition = null; - this.lastError = null; - this.listeners = null; + + // Geolocation listeners + this.listeners = {}; }; -var geoListeners = []; - -Geolocation.prototype.getCurrentPosition = function(successCallback, errorCallback, options) -{ - var position = Geo.getCurrentLocation(); - this.global_success = successCallback; - this.fail = errorCallback; -} - -// Run the global callback -Geolocation.prototype.gotCurrentPosition = function(lat, lng, alt, altacc, head, vel, stamp) -{ - if (lat == "undefined" || lng == "undefined") - { - this.fail(); - } - else - { - coords = new Coordinates(lat, lng, alt, acc, head, vel); - loc = new Position(coords, stamp); - this.lastPosition = loc; - this.global_success(loc); - } -} - -/* -* This turns on the GeoLocator class, which has two listeners. -* The listeners have their own timeouts, and run independently of this process -* In this case, we return the key to the watch hash -*/ - -Geolocation.prototype.watchPosition = function(successCallback, errorCallback, options) -{ - var frequency = (options != undefined)? options.frequency : 10000; - - var key = geoListeners.push( {"success" : successCallback, "fail" : errorCallback }) - 1; - - // TO-DO: Get the names of the method and pass them as strings to the Java. - return Geo.start(frequency, key); -} - -/* - * Retrieve and stop this listener from listening to the GPS +/** + * Position error object * + * @param code + * @param message */ -Geolocation.prototype.success = function(key, lat, lng, alt, altacc, head, vel, stamp) -{ - var coords = new Coordinates(lat, lng, alt, acc, head, vel); - var loc = new Position(coords, stamp); - geoListeners[key].success(loc); +function PositionError(code, message) { + this.code = code; + this.message = message; +}; + +PositionError.PERMISSION_DENIED = 1; +PositionError.POSITION_UNAVAILABLE = 2; +PositionError.TIMEOUT = 3; + +/** + * Asynchronously aquires the current position. + * + * @param {Function} successCallback The function to call when the position data is available + * @param {Function} errorCallback The function to call when there is an error getting the heading position. (OPTIONAL) + * @param {PositionOptions} options The options for getting the position data. (OPTIONAL) + */ +Geolocation.prototype.getCurrentPosition = function(successCallback, errorCallback, options) { + if (navigator._geo.listeners["global"]) { + console.log("Geolocation Error: Still waiting for previous getCurrentPosition() request."); + try { + errorCallback(new PositionError(PositionError.TIMEOUT, "Geolocation Error: Still waiting for previous getCurrentPosition() request.")); + } catch (e) { + } + return; + } + navigator._geo.listeners["global"] = {"success" : successCallback, "fail" : errorCallback }; + PhoneGap.execAsync(null, null, "Geolocation", "getCurrentLocation", []); } -Geolocation.prototype.fail = function(key) -{ - geoListeners[key].fail(); -} - -Geolocation.prototype.clearWatch = function(watchId) -{ - Geo.stop(watchId); -} +/** + * Asynchronously watches the geolocation for changes to geolocation. When a change occurs, + * the successCallback is called with the new location. + * + * @param {Function} successCallback The function to call each time the location data is available + * @param {Function} errorCallback The function to call when there is an error getting the location data. (OPTIONAL) + * @param {PositionOptions} options The options for getting the location data such as frequency. (OPTIONAL) + * @return String The watch id that must be passed to #clearWatch to stop watching. + */ +Geolocation.prototype.watchPosition = function(successCallback, errorCallback, options) { + var frequency = (options != undefined)? options.frequency : 10000; + var id = PhoneGap.createUUID(); + navigator._geo.listeners[id] = {"success" : successCallback, "fail" : errorCallback }; + PhoneGap.execAsync(null, null, "Geolocation", "start", [frequency, id]); + return id; +}; + +/* + * Native callback when watch position has a new position. + * + * @param {String} id + * @param {Number} lat + * @param {Number} lng + * @param {Number} alt + * @param {Number} altacc + * @param {Number} head + * @param {Number} vel + * @param {Number} stamp + */ +Geolocation.prototype.success = function(id, lat, lng, alt, altacc, head, vel, stamp) { + var coords = new Coordinates(lat, lng, alt, altacc, head, vel); + var loc = new Position(coords, stamp); + try { + if (lat == "undefined" || lng == "undefined") { + navigator._geo.listeners[id].fail(new PositionError(PositionError.POSITION_UNAVAILABLE, "Lat/Lng are undefined.")); + } + else { + navigator._geo.lastPosition = loc; + navigator._geo.listeners[id].success(loc); + } + } + catch (e) { + console.log("Geolocation Error: Error calling success callback function."); + } + + if (id == "global") { + delete navigator._geo.listeners["global"]; + } +}; + +/** + * Native callback when watch position has an error. + * + * @param {String} id The ID of the watch + * @param {Number} code The error code + * @param {String} msg The error message + */ +Geolocation.prototype.fail = function(id, code, msg) { + try { + navigator._geo.listeners[id].fail(new PositionError(code, msg)); + } + catch (e) { + console.log("Geolocation Error: Error calling error callback function."); + } +}; + +/** + * Clears the specified heading watch. + * + * @param {String} id The ID of the watch returned from #watchPosition + */ +Geolocation.prototype.clearWatch = function(id) { + PhoneGap.execAsync(null, null, "Geolocation", "stop", [id]); + delete navigator._geo.listeners[id]; +}; + +/** + * Force the PhoneGap geolocation to be used instead of built-in. + */ +Geolocation.usingPhoneGap = false; +Geolocation.usePhoneGap = function() { + if (Geolocation.usingPhoneGap) { + return; + } + Geolocation.usingPhoneGap = true; + + // Set built-in geolocation methods to our own implementations + // (Cannot replace entire geolocation, but can replace individual methods) + navigator.geolocation.setLocation = navigator._geo.setLocation; + navigator.geolocation.getCurrentPosition = navigator._geo.getCurrentPosition; + navigator.geolocation.watchPosition = navigator._geo.watchPosition; + navigator.geolocation.clearWatch = navigator._geo.clearWatch; + navigator.geolocation.start = navigator._geo.start; + navigator.geolocation.stop = navigator._geo.stop; +}; PhoneGap.addConstructor(function() { - // Taken from Jesse's geo fix (similar problem) in PhoneGap iPhone. Go figure, same browser! - function __proxyObj(origObj, proxyObj, funkList) { - for (var v in funkList) { - origObj[funkList[v]] = proxyObj[funkList[v]]; - } - } - // In the case of Android, we can use the Native Geolocation Object if it exists, so only load this on 1.x devices - if (typeof navigator.geolocation == 'undefined') { - navigator.geolocation = new Geolocation(); - } + navigator._geo = new Geolocation(); + + // No native geolocation object for Android 1.x, so use PhoneGap geolocation + if (typeof navigator.geolocation == 'undefined') { + navigator.geolocation = navigator._geo; + Geolocation.usingPhoneGap = true; + } }); + diff --git a/framework/src/com/phonegap/DroidGap.java b/framework/src/com/phonegap/DroidGap.java index 56eeb0e7..70d15328 100755 --- a/framework/src/com/phonegap/DroidGap.java +++ b/framework/src/com/phonegap/DroidGap.java @@ -282,10 +282,10 @@ public class DroidGap extends Activity { Storage cupcakeStorage = (Storage)this.pluginManager.addPlugin("com.phonegap.Storage"); cupcakeStorage.setStorage(appPackage); - this.pluginManager.addPlugin("com.phonegap.GeoBroker"); } + this.addService("Geolocation", "com.phonegap.GeoBroker"); this.addService("Device", "com.phonegap.Device"); this.addService("Accelerometer", "com.phonegap.AccelListener"); this.addService("Compass", "com.phonegap.CompassListener"); diff --git a/framework/src/com/phonegap/GeoBroker.java b/framework/src/com/phonegap/GeoBroker.java old mode 100644 new mode 100755 index 0ea4645a..e799f50c --- a/framework/src/com/phonegap/GeoBroker.java +++ b/framework/src/com/phonegap/GeoBroker.java @@ -1,6 +1,7 @@ package com.phonegap; import java.util.HashMap; +import java.util.Map.Entry; import org.json.JSONArray; import org.json.JSONException; @@ -22,6 +23,7 @@ public class GeoBroker implements Plugin { WebView webView; // WebView object DroidGap ctx; // DroidGap object + // List of gGeolocation listeners private HashMap geoListeners; private GeoListener global; @@ -87,7 +89,8 @@ public class GeoBroker implements Plugin { * @return T=returns value */ public boolean isSynch(String action) { - return false; + // Starting listeners is easier to run on main thread, so don't run async. + return true; } /** @@ -103,10 +106,22 @@ public class GeoBroker implements Plugin { } /** - * Called by AccelBroker when listener is to be shut down. + * Called when the activity is to be shut down. * Stop listener. */ - public void onDestroy() { + public void onDestroy() { + java.util.Set> s = this.geoListeners.entrySet(); + java.util.Iterator> it = s.iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + GeoListener listener = entry.getValue(); + listener.destroy(); + } + this.geoListeners.clear(); + if (this.global != null) { + this.global.destroy(); + } + this.global = null; } /** @@ -125,23 +140,51 @@ public class GeoBroker implements Plugin { // LOCAL METHODS //-------------------------------------------------------------------------- - public void getCurrentLocation() { - //It's supposed to run async! - if (global == null) { - global = new GeoListener("global", this.ctx, 10000, this.webView); + /** + * Get current location. + * The result is returned to JavaScript via a callback. + */ + public void getCurrentLocation() { + + // Create a geolocation listener just for getCurrentLocation and call it "global" + if (this.global == null) { + this.global = new GeoListener("global", this.ctx, 10000, this.webView); } else { - global.start(10000); + this.global.start(10000); } } + /** + * Start geolocation listener and add to listener list. + * + * @param freq Period to retrieve geolocation + * @param key The listener id + * @return + */ public String start(int freq, String key) { - GeoListener listener = new GeoListener(key, this.ctx, freq, this.webView); - geoListeners.put(key, listener); + + // Make sure this listener doesn't already exist + GeoListener listener = geoListeners.get(key); + if (listener == null) { + listener = new GeoListener(key, this.ctx, freq, this.webView); + geoListeners.put(key, listener); + } + + // Start it + listener.start(freq); return key; } + /** + * Stop geolocation listener and remove from listener list. + * + * @param key The listener id + */ public void stop(String key) { - GeoListener geo = geoListeners.get(key); + GeoListener listener = geoListeners.remove(key); + if (listener != null) { + listener.stop(); + } } } diff --git a/framework/src/com/phonegap/GeoListener.java b/framework/src/com/phonegap/GeoListener.java old mode 100644 new mode 100755 index 86d79c8f..b78f6fb1 --- a/framework/src/com/phonegap/GeoListener.java +++ b/framework/src/com/phonegap/GeoListener.java @@ -6,83 +6,112 @@ import android.location.LocationManager; import android.webkit.WebView; public class GeoListener { - String id; - String successCallback; + public static int PERMISSION_DENIED = 1; + public static int POSITION_UNAVAILABLE = 2; + public static int TIMEOUT = 3; + + String id; // Listener ID + String successCallback; // String failCallback; - GpsListener mGps; - NetworkListener mNetwork; - LocationManager mLocMan; - private DroidGap mCtx; - private WebView mAppView; + GpsListener mGps; // GPS listener + NetworkListener mNetwork; // Network listener + LocationManager mLocMan; // Location manager + + private DroidGap ctx; // DroidGap object + @SuppressWarnings("unused") + private WebView mAppView; // Webview object int interval; - GeoListener(String i, DroidGap ctx, int time, WebView appView) { - id = i; - interval = time; - mCtx = ctx; - mGps = null; - mNetwork = null; - mLocMan = (LocationManager) mCtx.getSystemService(Context.LOCATION_SERVICE); + /** + * Constructor. + * + * @param id Listener id + * @param ctx + * @param time Sampling period in msec + * @param appView + */ + GeoListener(String id, DroidGap ctx, int time, WebView appView) { + this.id = id; + this.interval = time; + this.ctx = ctx; + this.mGps = null; + this.mNetwork = null; + this.mLocMan = (LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE); - if (mLocMan.getProvider(LocationManager.GPS_PROVIDER) != null) { - mGps = new GpsListener(mCtx, interval, this); + // If GPS provider, then create and start GPS listener + if (this.mLocMan.getProvider(LocationManager.GPS_PROVIDER) != null) { + this.mGps = new GpsListener(ctx, time, this); } - if (mLocMan.getProvider(LocationManager.NETWORK_PROVIDER) != null) { - mNetwork = new NetworkListener(mCtx, interval, this); + + // If network provider, then create and start network listener + if (this.mLocMan.getProvider(LocationManager.NETWORK_PROVIDER) != null) { + this.mNetwork = new NetworkListener(ctx, time, this); } - mAppView = appView; + this.mAppView = appView; } + /** + * Destroy listener. + */ + public void destroy() { + this.stop(); + } + + /** + * Location found. Send location back to JavaScript. + * + * @param loc + */ void success(Location loc) { - /* - * We only need to figure out what we do when we succeed! - */ - String params; - /* - * Build the giant string to send back to Javascript! - */ - params = loc.getLatitude() + "," + loc.getLongitude() + ", " + loc.getAltitude() + "," + loc.getAccuracy() + "," + loc.getBearing(); - params += "," + loc.getSpeed() + "," + loc.getTime(); - if (id != "global") { - mCtx.sendJavascript("navigator._geo.success(" + id + "," + params + ");"); - } - else { - mCtx.sendJavascript("navigator.geolocation.gotCurrentPosition(" + params + ");"); + String params = loc.getLatitude() + "," + loc.getLongitude() + ", " + loc.getAltitude() + + "," + loc.getAccuracy() + "," + loc.getBearing() + + "," + loc.getSpeed() + "," + loc.getTime(); + + if (id == "global") { this.stop(); } + this.ctx.sendJavascript("navigator._geo.success('" + id + "'," + params + ");"); } - void fail() { - // Do we need to know why? How would we handle this? - if (id != "global") { - mCtx.sendJavascript("navigator._geo.fail(" + id + ");"); - } else { - mCtx.sendJavascript("navigator._geo.fail();"); - } + /** + * Location failed. Send error back to JavaScript. + * + * @param code The error code + * @param msg The error message + */ + void fail(int code, String msg) { + this.ctx.sendJavascript("navigator._geo.fail('" + this.id + "', " + ", " + code + ", '" + msg + "');"); + this.stop(); } + /** + * Start retrieving location. + * + * @param interval + */ void start(int interval) { - if (mGps != null) { - mGps.start(interval); + if (this.mGps != null) { + this.mGps.start(interval); } - if (mNetwork != null) { - mNetwork.start(interval); + if (this.mNetwork != null) { + this.mNetwork.start(interval); } - if (mNetwork == null && mGps == null) { - // Really, how were you going to get the location??? - mCtx.sendJavascript("navigator._geo.fail();"); + if (this.mNetwork == null && this.mGps == null) { + this.fail(POSITION_UNAVAILABLE, "No location providers available."); } } - // This stops the listener + /** + * Stop listening for location. + */ void stop() { - if (mGps != null) { - mGps.stop(); + if (this.mGps != null) { + this.mGps.stop(); } - if (mNetwork != null) { - mNetwork.stop(); + if (this.mNetwork != null) { + this.mNetwork.stop(); } } diff --git a/framework/src/com/phonegap/GpsListener.java b/framework/src/com/phonegap/GpsListener.java old mode 100644 new mode 100755 index a236f563..96326f4e --- a/framework/src/com/phonegap/GpsListener.java +++ b/framework/src/com/phonegap/GpsListener.java @@ -26,81 +26,138 @@ import android.location.Location; import android.location.LocationManager; import android.location.LocationListener; import android.os.Bundle; -import android.util.Log; +/** + * This class handles requests for GPS location services. + * + */ public class GpsListener implements LocationListener { - private Context mCtx; - private Location cLoc; - private LocationManager mLocMan; - private static final String LOG_TAG = "PhoneGap"; - private GeoListener owner; - private boolean hasData = false; + private DroidGap mCtx; // DroidGap object - public GpsListener(Context ctx, int interval, GeoListener m) - { - owner = m; - mCtx = ctx; + private LocationManager mLocMan; // Location manager object + private GeoListener owner; // Geolistener object (parent) + private boolean hasData = false; // Flag indicates if location data is available in cLoc + private Location cLoc; // Last recieved location + private boolean running = false; // Flag indicates if listener is running + + /** + * Constructor. + * Automatically starts listening. + * + * @param ctx + * @param interval + * @param m + */ + public GpsListener(DroidGap ctx, int interval, GeoListener m) { + this.owner = m; + this.mCtx = ctx; + this.mLocMan = (LocationManager) this.mCtx.getSystemService(Context.LOCATION_SERVICE); + this.running = false; this.start(interval); } - public Location getLocation() - { - cLoc = mLocMan.getLastKnownLocation(LocationManager.GPS_PROVIDER); - hasData = true; - return cLoc; + /** + * Get last location. + * + * @return Location object + */ + public Location getLocation() { + this.cLoc = this.mLocMan.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (this.cLoc != null) { + this.hasData = true; + } + return this.cLoc; } + /** + * Called when the provider is disabled by the user. + * + * @param provider + */ public void onProviderDisabled(String provider) { - // TODO Auto-generated method stub - Log.d(LOG_TAG, "The provider " + provider + " is disabled"); - owner.fail(); + this.owner.fail(GeoListener.POSITION_UNAVAILABLE, "GPS provider disabled."); } + /** + * Called when the provider is enabled by the user. + * + * @param provider + */ public void onProviderEnabled(String provider) { - // TODO Auto-generated method stub - Log.d(LOG_TAG, "The provider "+ provider + " is enabled"); + System.out.println("GpsListener: The provider "+ provider + " is enabled"); } - + /** + * Called when the provider status changes. This method is called when a + * provider is unable to fetch a location or if the provider has recently + * become available after a period of unavailability. + * + * @param provider + * @param status + * @param extras + */ public void onStatusChanged(String provider, int status, Bundle extras) { - // TODO Auto-generated method stub - Log.d(LOG_TAG, "The status of the provider " + provider + " has changed"); - if(status == 0) - { - Log.d(LOG_TAG, provider + " is OUT OF SERVICE"); - owner.fail(); + System.out.println("GpsListener: The status of the provider " + provider + " has changed"); + if (status == 0) { + System.out.println("GpsListener: " + provider + " is OUT OF SERVICE"); + this.owner.fail(GeoListener.POSITION_UNAVAILABLE, "GPS out of service."); } - else if(status == 1) - { - Log.d(LOG_TAG, provider + " is TEMPORARILY_UNAVAILABLE"); + else if (status == 1) { + System.out.println("GpsListener: " + provider + " is TEMPORARILY_UNAVAILABLE"); } - else - { - Log.d(LOG_TAG, provider + " is Available"); + else { + System.out.println("GpsListener: " + provider + " is Available"); } } - + /** + * Called when the location has changed. + * + * @param location + */ public void onLocationChanged(Location location) { - Log.d(LOG_TAG, "The location has been updated!"); - owner.success(location); + System.out.println("GpsListener: The location has been updated!"); + this.hasData = true; + this.cLoc = location; + this.owner.success(location); } + /** + * Determine if location data is available. + * + * @return + */ public boolean hasLocation() { - return hasData; + return this.hasData; } - public void start(int interval) - { - mLocMan = (LocationManager) mCtx.getSystemService(Context.LOCATION_SERVICE); - mLocMan.requestLocationUpdates(LocationManager.GPS_PROVIDER, interval, 0, this); - cLoc = mLocMan.getLastKnownLocation(LocationManager.GPS_PROVIDER); + /** + * Start requesting location updates. + * + * @param interval + */ + public void start(int interval) { + if (!this.running) { + this.running = true; + this.mLocMan.requestLocationUpdates(LocationManager.GPS_PROVIDER, interval, 0, this); + this.getLocation(); + + // If GPS provider has data, then send now + if (this.hasData) { + this.owner.success(this.cLoc); + } + } } - public void stop() - { - mLocMan.removeUpdates(this); + /** + * Stop receiving location updates. + */ + public void stop() { + if (this.running) { + this.mLocMan.removeUpdates(this); + } + this.running = false; } } diff --git a/framework/src/com/phonegap/NetworkListener.java b/framework/src/com/phonegap/NetworkListener.java old mode 100644 new mode 100755 index 4b0ecda4..0941f08b --- a/framework/src/com/phonegap/NetworkListener.java +++ b/framework/src/com/phonegap/NetworkListener.java @@ -26,82 +26,129 @@ import android.location.Location; import android.location.LocationManager; import android.location.LocationListener; import android.os.Bundle; -import android.util.Log; public class NetworkListener implements LocationListener { - - private Context mCtx; - private Location cLoc; - private LocationManager mLocMan; - private static final String LOG_TAG = "PhoneGap"; - GeoListener owner; - public NetworkListener(Context ctx, int interval, GeoListener m) - { - owner = m; - mCtx = ctx; + private DroidGap mCtx; // DroidGap object + + private LocationManager mLocMan; // Location manager object + private GeoListener owner; // Geolistener object (parent) + private boolean hasData = false; // Flag indicates if location data is available in cLoc + private Location cLoc; // Last recieved location + private boolean running = false; // Flag indicates if listener is running + + /** + * Constructor. + * Automatically starts listening. + * + * @param ctx + * @param interval + * @param m + */ + public NetworkListener(DroidGap ctx, int interval, GeoListener m) { + this.owner = m; + this.mCtx = ctx; + this.mLocMan = (LocationManager) this.mCtx.getSystemService(Context.LOCATION_SERVICE); + this.running = false; this.start(interval); } - public Location getLocation() - { - cLoc = mLocMan.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); - return cLoc; + /** + * Get last location. + * + * @return Location object + */ + public Location getLocation() { + this.cLoc = this.mLocMan.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + if (this.cLoc != null) { + this.hasData = true; + } + return this.cLoc; } + /** + * Called when the provider is disabled by the user. + * + * @param provider + */ public void onProviderDisabled(String provider) { - // TODO Auto-generated method stub - Log.d(LOG_TAG, "The provider " + provider + " is disabled"); + System.out.println("NetworkListener: The provider " + provider + " is disabled"); } - + /** + * Called when the provider is enabled by the user. + * + * @param provider + */ public void onProviderEnabled(String provider) { - // TODO Auto-generated method stub - Log.d(LOG_TAG, "The provider "+ provider + " is enabled"); + System.out.println("NetworkListener: The provider "+ provider + " is enabled"); } - + /** + * Called when the provider status changes. This method is called when a + * provider is unable to fetch a location or if the provider has recently + * become available after a period of unavailability. + * + * @param provider + * @param status + * @param extras + */ public void onStatusChanged(String provider, int status, Bundle extras) { - // TODO Auto-generated method stub - Log.d(LOG_TAG, "The status of the provider " + provider + " has changed"); - if(status == 0) - { - Log.d(LOG_TAG, provider + " is OUT OF SERVICE"); + System.out.println("NetworkListener: The status of the provider " + provider + " has changed"); + if (status == 0) { + System.out.println("NetworkListener: " + provider + " is OUT OF SERVICE"); } - else if(status == 1) - { - Log.d(LOG_TAG, provider + " is TEMPORARILY_UNAVAILABLE"); + else if (status == 1) { + System.out.println("NetworkListener: " + provider + " is TEMPORARILY_UNAVAILABLE"); } - else - { - Log.d(LOG_TAG, provider + " is Available"); + else { + System.out.println("NetworkListener: " + provider + " is Available"); } } - - /* - * The GPS is the primary form of Geolocation in PhoneGap. Only fire the success variables if the GPS is down - * for some reason + /** + * Called when the location has changed. + * + * @param location */ public void onLocationChanged(Location location) { - Log.d(LOG_TAG, "The location has been updated!"); - if (!owner.mGps.hasLocation()) - { - owner.success(location); + System.out.println("NetworkListener: The location has been updated!"); + this.hasData = true; + this.cLoc = location; + + // The GPS is the primary form of Geolocation in PhoneGap. + // Only fire the success variables if the GPS is down for some reason. + if (!this.owner.mGps.hasLocation()) { + this.owner.success(location); } - cLoc = location; } - public void start(int interval) - { - mLocMan = (LocationManager) mCtx.getSystemService(Context.LOCATION_SERVICE); - mLocMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, interval, 0, this); - cLoc = mLocMan.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); + /** + * Start requesting location updates. + * + * @param interval + */ + public void start(int interval) { + if (!this.running) { + this.running = true; + this.mLocMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, interval, 0, this); + this.getLocation(); + + // If Network provider has data but GPS provider doesn't, then send ours + if (this.hasData && !this.owner.mGps.hasLocation()) { + this.owner.success(this.cLoc); + } + } } - public void stop() - { - mLocMan.removeUpdates(this); + /** + * Stop receiving location updates. + */ + public void stop() { + if (this.running) { + this.mLocMan.removeUpdates(this); + } + this.running = false; } } From 7f7cc1db2a5e15e5fbf2456b1c04114a7eb28cc0 Mon Sep 17 00:00:00 2001 From: Bryce Curtis Date: Thu, 23 Sep 2010 14:34:56 -0500 Subject: [PATCH 13/13] Add geolocation options as defined by W3C spec. --- framework/assets/js/geolocation.js | 38 +++++++++++++++++++++-- framework/src/com/phonegap/GeoBroker.java | 26 ++++++++++------ 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/framework/assets/js/geolocation.js b/framework/assets/js/geolocation.js index 40f06da4..e063f075 100755 --- a/framework/assets/js/geolocation.js +++ b/framework/assets/js/geolocation.js @@ -42,8 +42,22 @@ Geolocation.prototype.getCurrentPosition = function(successCallback, errorCallba } return; } + var maximumAge = 10000; + var enableHighAccuracy = false; + var timeout = 10000; + if (typeof options != "undefined") { + if (typeof options.maximumAge != "undefined") { + maximumAge = options.maximumAge; + } + if (typeof options.enableHighAccuracy != "undefined") { + enableHighAccuracy = options.enableHighAccuracy; + } + if (typeof options.timeout != "undefined") { + timeout = options.timeout; + } + } navigator._geo.listeners["global"] = {"success" : successCallback, "fail" : errorCallback }; - PhoneGap.execAsync(null, null, "Geolocation", "getCurrentLocation", []); + PhoneGap.execAsync(null, null, "Geolocation", "getCurrentLocation", [enableHighAccuracy, timeout, maximumAge]); } /** @@ -56,15 +70,32 @@ Geolocation.prototype.getCurrentPosition = function(successCallback, errorCallba * @return String The watch id that must be passed to #clearWatch to stop watching. */ Geolocation.prototype.watchPosition = function(successCallback, errorCallback, options) { - var frequency = (options != undefined)? options.frequency : 10000; + var maximumAge = 10000; + var enableHighAccuracy = false; + var timeout = 10000; + if (typeof options != "undefined") { + if (typeof options.frequency != "undefined") { + maximumAge = options.frequency; + } + if (typeof options.maximumAge != "undefined") { + maximumAge = options.maximumAge; + } + if (typeof options.enableHighAccuracy != "undefined") { + enableHighAccuracy = options.enableHighAccuracy; + } + if (typeof options.timeout != "undefined") { + timeout = options.timeout; + } + } var id = PhoneGap.createUUID(); navigator._geo.listeners[id] = {"success" : successCallback, "fail" : errorCallback }; - PhoneGap.execAsync(null, null, "Geolocation", "start", [frequency, id]); + PhoneGap.execAsync(null, null, "Geolocation", "start", [id, enableHighAccuracy, timeout, maximumAge]); return id; }; /* * Native callback when watch position has a new position. + * PRIVATE METHOD * * @param {String} id * @param {Number} lat @@ -98,6 +129,7 @@ Geolocation.prototype.success = function(id, lat, lng, alt, altacc, head, vel, s /** * Native callback when watch position has an error. + * PRIVATE METHOD * * @param {String} id The ID of the watch * @param {Number} code The error code diff --git a/framework/src/com/phonegap/GeoBroker.java b/framework/src/com/phonegap/GeoBroker.java index e799f50c..45281d76 100755 --- a/framework/src/com/phonegap/GeoBroker.java +++ b/framework/src/com/phonegap/GeoBroker.java @@ -67,10 +67,10 @@ public class GeoBroker implements Plugin { try { if (action.equals("getCurrentLocation")) { - this.getCurrentLocation(); + this.getCurrentLocation(args.getBoolean(0), args.getInt(1), args.getInt(2)); } else if (action.equals("start")) { - String s = this.start(args.getInt(0), args.getString(1)); + String s = this.start(args.getString(0), args.getBoolean(1), args.getInt(2), args.getInt(3)); return new PluginResult(status, s); } else if (action.equals("stop")) { @@ -143,36 +143,42 @@ public class GeoBroker implements Plugin { /** * Get current location. * The result is returned to JavaScript via a callback. + * + * @param enableHighAccuracy + * @param timeout + * @param maximumAge */ - public void getCurrentLocation() { + public void getCurrentLocation(boolean enableHighAccuracy, int timeout, int maximumAge) { // Create a geolocation listener just for getCurrentLocation and call it "global" if (this.global == null) { - this.global = new GeoListener("global", this.ctx, 10000, this.webView); + this.global = new GeoListener("global", this.ctx, maximumAge, this.webView); } else { - this.global.start(10000); + this.global.start(maximumAge); } } /** * Start geolocation listener and add to listener list. * - * @param freq Period to retrieve geolocation - * @param key The listener id + * @param key The listener id + * @param enableHighAccuracy + * @param timeout + * @param maximumAge * @return */ - public String start(int freq, String key) { + public String start(String key, boolean enableHighAccuracy, int timeout, int maximumAge) { // Make sure this listener doesn't already exist GeoListener listener = geoListeners.get(key); if (listener == null) { - listener = new GeoListener(key, this.ctx, freq, this.webView); + listener = new GeoListener(key, this.ctx, maximumAge, this.webView); geoListeners.put(key, listener); } // Start it - listener.start(freq); + listener.start(maximumAge); return key; }