This commit is contained in:
Anis Kadri 2012-09-18 14:45:20 -07:00
commit f71e664952
28 changed files with 1106 additions and 486 deletions

View File

@ -33,7 +33,7 @@ To create your cordova.jar, copy the commons codec:
then run in the framework directory: then run in the framework directory:
android update project -p . -t android-15 android update project -p . -t android-16
ant jar ant jar
@ -61,7 +61,7 @@ Project Commands
These commands live in a generated Cordova Android project. These commands live in a generated Cordova Android project.
./cordovap/debug [path] ..................... install to first device ./cordova/debug [path] ..................... install to first device
./cordova/emulate .......................... start avd (emulator) named default ./cordova/emulate .......................... start avd (emulator) named default
./cordova/log .............................. starts logcat ./cordova/log .............................. starts logcat

View File

@ -66,7 +66,7 @@ function createAppInfoJar {
} }
function on_error { function on_error {
echo "An error occured. Deleting project..." echo "An error occurred. Deleting project..."
[ -d "$PROJECT_PATH" ] && rm -rf "$PROJECT_PATH" [ -d "$PROJECT_PATH" ] && rm -rf "$PROJECT_PATH"
} }

View File

@ -73,7 +73,7 @@ function emulate {
function clean { function clean {
ant clean ant clean
} }
# has to be used independently and not in conjuction with other commands # has to be used independently and not in conjunction with other commands
function log { function log {
adb logcat adb logcat
} }

View File

@ -47,8 +47,10 @@
<application android:icon="@drawable/icon" android:label="@string/app_name" <application android:icon="@drawable/icon" android:label="@string/app_name"
android:hardwareAccelerated="true"
android:debuggable="true"> android:debuggable="true">
<activity android:name="__ACTIVITY__" android:label="@string/app_name" <activity android:name="__ACTIVITY__" android:label="@string/app_name"
android:theme="@android:style/Theme.Black.NoTitleBar"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale"> android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -31,7 +31,7 @@ var app = {
// deviceready Event Handler // deviceready Event Handler
// //
// The scope of 'this' is the event. In order to call the 'receivedEvent' // The scope of 'this' is the event. In order to call the 'receivedEvent'
// function, we must explicity call 'app.receivedEvent(...);' // function, we must explicitly call 'app.receivedEvent(...);'
onDeviceReady: function() { onDeviceReady: function() {
app.receivedEvent('deviceready'); app.receivedEvent('deviceready');
}, },

View File

@ -75,7 +75,7 @@ create_project.on('exit', function(code) {
// make sure main Activity was added // make sure main Activity was added
path.exists(util.format('%s/src/%s/%s.java', project_path, package_as_path, project_name), function(exists) { path.exists(util.format('%s/src/%s/%s.java', project_path, package_as_path, project_name), function(exists) {
assert(exists, 'Activity did not get created'); assert(exists, 'Activity did not get created');
// TODO check that package name and activity name were substitued properly // TODO check that package name and activity name were substituted properly
}); });
// make sure plugins.xml was added // make sure plugins.xml was added

View File

@ -85,7 +85,7 @@ create_project.on('exit', function(code) {
// make sure main Activity was added // make sure main Activity was added
path.exists(util.format('%s/src/%s/%s.java', project_path, package_as_path, project_name), function(exists) { path.exists(util.format('%s/src/%s/%s.java', project_path, package_as_path, project_name), function(exists) {
assert(exists, 'Activity did not get created'); assert(exists, 'Activity did not get created');
// TODO check that package name and activity name were substitued properly // TODO check that package name and activity name were substituted properly
}); });
// make sure plugins.xml was added // make sure plugins.xml was added

File diff suppressed because it is too large Load Diff

View File

@ -235,7 +235,7 @@ public class AudioHandler extends Plugin {
/** /**
* Seek to a location. * Seek to a location.
* @param id The id of the audio player * @param id The id of the audio player
* @param miliseconds int: number of milliseconds to skip 1000 = 1 second * @param milliseconds int: number of milliseconds to skip 1000 = 1 second
*/ */
public void seekToAudio(String id, int milliseconds) { public void seekToAudio(String id, int milliseconds) {
AudioPlayer audio = this.players.get(id); AudioPlayer audio = this.players.get(id);

View File

@ -410,7 +410,7 @@ public class AudioPlayer implements OnCompletionListener, OnPreparedListener, On
*/ */
private void setMode(MODE mode) { private void setMode(MODE mode) {
if (this.mode != mode) { if (this.mode != mode) {
//mode is not part of the expected behaviour, so no notification //mode is not part of the expected behavior, so no notification
//this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + mode + ");"); //this.handler.sendJavascript("cordova.require('cordova/plugin/Media').onStatus('" + this.id + "', " + MEDIA_STATE + ", " + mode + ");");
} }
this.mode = mode; this.mode = mode;

View File

@ -207,19 +207,18 @@ public class CallbackServer implements Runnable {
// Must have security token // Must have security token
if ((requestParts.length == 3) && (requestParts[1].substring(1).equals(this.token))) { if ((requestParts.length == 3) && (requestParts[1].substring(1).equals(this.token))) {
//Log.d(LOG_TAG, "CallbackServer -- Processing GET request"); //Log.d(LOG_TAG, "CallbackServer -- Processing GET request");
String js = null; String payload = null;
// Wait until there is some data to send, or send empty data every 10 sec // Wait until there is some data to send, or send empty data every 10 sec
// to prevent XHR timeout on the client // to prevent XHR timeout on the client
synchronized (this) { while (this.active) {
while (this.active) { if (jsMessageQueue != null) {
if (jsMessageQueue != null) { payload = jsMessageQueue.popAndEncode();
// TODO(agrieve): Should this use popAll() instead? if (payload != null) {
js = jsMessageQueue.pop(); break;
if (js != null) { }
break; }
} synchronized (this) {
}
try { try {
this.wait(10000); // prevent timeout from happening this.wait(10000); // prevent timeout from happening
//Log.d(LOG_TAG, "CallbackServer>>> break <<<"); //Log.d(LOG_TAG, "CallbackServer>>> break <<<");
@ -233,14 +232,14 @@ public class CallbackServer implements Runnable {
if (this.active) { if (this.active) {
// If no data, then send 404 back to client before it times out // If no data, then send 404 back to client before it times out
if (js == null) { if (payload == null) {
//Log.d(LOG_TAG, "CallbackServer -- sending data 0"); //Log.d(LOG_TAG, "CallbackServer -- sending data 0");
response = "HTTP/1.1 404 NO DATA\r\n\r\n "; // need to send content otherwise some Android devices fail, so send space response = "HTTP/1.1 404 NO DATA\r\n\r\n "; // need to send content otherwise some Android devices fail, so send space
} }
else { else {
//Log.d(LOG_TAG, "CallbackServer -- sending item"); //Log.d(LOG_TAG, "CallbackServer -- sending item");
response = "HTTP/1.1 200 OK\r\n\r\n"; response = "HTTP/1.1 200 OK\r\n\r\n";
response += encode(js, "UTF-8"); response += encode(payload, "UTF-8");
} }
} }
else { else {

View File

@ -143,7 +143,7 @@ public class CameraLauncher extends Plugin implements MediaScannerConnectionClie
this.saveToPhotoAlbum = args.getBoolean(9); this.saveToPhotoAlbum = args.getBoolean(9);
// If the user specifies a 0 or smaller width/height // If the user specifies a 0 or smaller width/height
// make it -1 so later comparrisions succeed // make it -1 so later comparisons succeed
if (this.targetWidth < 1) { if (this.targetWidth < 1) {
this.targetWidth = -1; this.targetWidth = -1;
} }
@ -392,7 +392,7 @@ public class CameraLauncher extends Plugin implements MediaScannerConnectionClie
// If we don't have a valid image so quit. // If we don't have a valid image so quit.
if (imagePath == null) { if (imagePath == null) {
Log.d(LOG_TAG, "I either have a null image path or bitmap"); Log.d(LOG_TAG, "I either have a null image path or bitmap");
this.failPicture("Unable to retreive path to picture!"); this.failPicture("Unable to retrieve path to picture!");
return; return;
} }
Bitmap bitmap = getScaledBitmap(imagePath); Bitmap bitmap = getScaledBitmap(imagePath);
@ -762,7 +762,7 @@ public class CameraLauncher extends Plugin implements MediaScannerConnectionClie
try{ try{
this.conn.scanFile(this.scanMe.toString(), "image/*"); this.conn.scanFile(this.scanMe.toString(), "image/*");
} catch (java.lang.IllegalStateException e){ } catch (java.lang.IllegalStateException e){
LOG.e(LOG_TAG, "Can't scan file in MediaScanner aftering taking picture"); LOG.e(LOG_TAG, "Can't scan file in MediaScanner after taking picture");
} }
} }

View File

@ -28,7 +28,6 @@ import android.content.ContentValues;
import android.content.OperationApplicationException; import android.content.OperationApplicationException;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Debug;
import android.os.RemoteException; import android.os.RemoteException;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.util.Log; import android.util.Log;
@ -331,8 +330,6 @@ public class ContactAccessorSdk5 extends ContactAccessor {
JSONArray websites = new JSONArray(); JSONArray websites = new JSONArray();
JSONArray photos = new JSONArray(); JSONArray photos = new JSONArray();
ArrayList<String> names = new ArrayList<String>();
// Column indices // Column indices
int colContactId = c.getColumnIndex(ContactsContract.Data.CONTACT_ID); int colContactId = c.getColumnIndex(ContactsContract.Data.CONTACT_ID);
int colRawContactId = c.getColumnIndex(ContactsContract.Data.RAW_CONTACT_ID); int colRawContactId = c.getColumnIndex(ContactsContract.Data.RAW_CONTACT_ID);
@ -795,10 +792,10 @@ public class ContactAccessorSdk5 extends ContactAccessor {
formatted.append(middleName + " "); formatted.append(middleName + " ");
} }
if (familyName != null) { if (familyName != null) {
formatted.append(familyName + " "); formatted.append(familyName);
} }
if (honorificSuffix != null) { if (honorificSuffix != null) {
formatted.append(honorificSuffix + " "); formatted.append(" " + honorificSuffix);
} }
contactName.put("familyName", familyName); contactName.put("familyName", familyName);
@ -862,7 +859,8 @@ public class ContactAccessorSdk5 extends ContactAccessor {
im.put("id", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im._ID))); im.put("id", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im._ID)));
im.put("pref", false); // Android does not store pref attribute im.put("pref", false); // Android does not store pref attribute
im.put("value", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA))); im.put("value", cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
im.put("type", getContactType(cursor.getInt(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.TYPE)))); String type = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.PROTOCOL));
im.put("type", getImType(new Integer(type).intValue()));
} catch (JSONException e) { } catch (JSONException e) {
Log.e(LOG_TAG, e.getMessage(), e); Log.e(LOG_TAG, e.getMessage(), e);
} }
@ -1248,7 +1246,7 @@ public class ContactAccessorSdk5 extends ContactAccessor {
contentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId); contentValues.put(ContactsContract.Data.RAW_CONTACT_ID, rawId);
contentValues.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE); contentValues.put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE);
contentValues.put(ContactsContract.CommonDataKinds.Im.DATA, getJsonString(im, "value")); contentValues.put(ContactsContract.CommonDataKinds.Im.DATA, getJsonString(im, "value"));
contentValues.put(ContactsContract.CommonDataKinds.Im.TYPE, getContactType(getJsonString(im, "type"))); contentValues.put(ContactsContract.CommonDataKinds.Im.TYPE, getImType(getJsonString(im, "type")));
ops.add(ContentProviderOperation.newInsert( ops.add(ContentProviderOperation.newInsert(
ContactsContract.Data.CONTENT_URI).withValues(contentValues).build()); ContactsContract.Data.CONTENT_URI).withValues(contentValues).build());
@ -1412,7 +1410,7 @@ public class ContactAccessorSdk5 extends ContactAccessor {
retVal = false; retVal = false;
} }
// if the save was a succes return the contact ID // if the save was a success return the contact ID
if (retVal) { if (retVal) {
return id; return id;
} else { } else {
@ -1447,7 +1445,7 @@ public class ContactAccessorSdk5 extends ContactAccessor {
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Im.DATA, getJsonString(im, "value")) .withValue(ContactsContract.CommonDataKinds.Im.DATA, getJsonString(im, "value"))
.withValue(ContactsContract.CommonDataKinds.Im.TYPE, getContactType(getJsonString(im, "type"))) .withValue(ContactsContract.CommonDataKinds.Im.TYPE, getImType(getJsonString(im, "type")))
.build()); .build());
} }
@ -2091,5 +2089,86 @@ public class ContactAccessorSdk5 extends ContactAccessor {
} }
return stringType; return stringType;
} }
/**
* Converts a string from the W3C Contact API to it's Android int value.
* @param string
* @return Android int value
*/
private int getImType(String string) {
int type = ContactsContract.CommonDataKinds.Im.PROTOCOL_CUSTOM;
if (string != null) {
if ("aim".equals(string.toLowerCase())) {
return ContactsContract.CommonDataKinds.Im.PROTOCOL_AIM;
}
else if ("google talk".equals(string.toLowerCase())) {
return ContactsContract.CommonDataKinds.Im.PROTOCOL_GOOGLE_TALK;
}
else if ("icq".equals(string.toLowerCase())) {
return ContactsContract.CommonDataKinds.Im.PROTOCOL_ICQ;
}
else if ("jabber".equals(string.toLowerCase())) {
return ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER;
}
else if ("msn".equals(string.toLowerCase())) {
return ContactsContract.CommonDataKinds.Im.PROTOCOL_MSN;
}
else if ("netmeeting".equals(string.toLowerCase())) {
return ContactsContract.CommonDataKinds.Im.PROTOCOL_NETMEETING;
}
else if ("qq".equals(string.toLowerCase())) {
return ContactsContract.CommonDataKinds.Im.PROTOCOL_QQ;
}
else if ("skype".equals(string.toLowerCase())) {
return ContactsContract.CommonDataKinds.Im.PROTOCOL_SKYPE;
}
else if ("yahoo".equals(string.toLowerCase())) {
return ContactsContract.CommonDataKinds.Im.PROTOCOL_YAHOO;
}
}
return type;
}
/**
* getPhoneType converts an Android phone type into a string
* @param type
* @return phone type as string.
*/
private String getImType(int type) {
String stringType;
switch (type) {
case ContactsContract.CommonDataKinds.Im.PROTOCOL_AIM:
stringType = "AIM";
break;
case ContactsContract.CommonDataKinds.Im.PROTOCOL_GOOGLE_TALK:
stringType = "Google Talk";
break;
case ContactsContract.CommonDataKinds.Im.PROTOCOL_ICQ:
stringType = "ICQ";
break;
case ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER:
stringType = "Jabber";
break;
case ContactsContract.CommonDataKinds.Im.PROTOCOL_MSN:
stringType = "MSN";
break;
case ContactsContract.CommonDataKinds.Im.PROTOCOL_NETMEETING:
stringType = "NetMeeting";
break;
case ContactsContract.CommonDataKinds.Im.PROTOCOL_QQ:
stringType = "QQ";
break;
case ContactsContract.CommonDataKinds.Im.PROTOCOL_SKYPE:
stringType = "Skype";
break;
case ContactsContract.CommonDataKinds.Im.PROTOCOL_YAHOO:
stringType = "Yahoo";
break;
default:
stringType = "custom";
break;
}
return stringType;
}
} }

View File

@ -202,9 +202,8 @@ public class CordovaChromeClient extends WebChromeClient {
String service = array.getString(0); String service = array.getString(0);
String action = array.getString(1); String action = array.getString(1);
String callbackId = array.getString(2); String callbackId = array.getString(2);
boolean async = array.getBoolean(3); String r = this.appView.exposedJsApi.exec(service, action, callbackId, message);
PluginResult r = this.appView.pluginManager.exec(service, action, callbackId, message, async); result.confirm(r == null ? "" : r);
result.confirm(r == null ? "" : r.getJSONString());
} catch (JSONException e) { } catch (JSONException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -212,15 +211,14 @@ public class CordovaChromeClient extends WebChromeClient {
// Sets the native->JS bridge mode. // Sets the native->JS bridge mode.
else if (reqOk && defaultValue != null && defaultValue.equals("gap_bridge_mode:")) { else if (reqOk && defaultValue != null && defaultValue.equals("gap_bridge_mode:")) {
this.appView.jsMessageQueue.setBridgeMode(Integer.parseInt(message)); this.appView.exposedJsApi.setNativeToJsBridgeMode(Integer.parseInt(message));
result.confirm(""); result.confirm("");
} }
// Polling for JavaScript messages // Polling for JavaScript messages
else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) { else if (reqOk && defaultValue != null && defaultValue.equals("gap_poll:")) {
// TODO(agrieve): Use popAll() here. String r = this.appView.exposedJsApi.retrieveJsMessages();
String r = this.appView.jsMessageQueue.pop(); result.confirm(r == null ? "" : r);
result.confirm(r);
} }
// Do NO-OP so older code doesn't display dialog // Do NO-OP so older code doesn't display dialog

View File

@ -55,6 +55,11 @@ public class CordovaLocationListener implements LocationListener {
{ {
this.owner.fail(code, message, callbackId); this.owner.fail(code, message, callbackId);
} }
if(this.owner.isGlobalListener(this))
{
Log.d(TAG, "Stopping global listener");
this.stop();
}
this.callbacks.clear(); this.callbacks.clear();
Iterator it = this.watches.entrySet().iterator(); Iterator it = this.watches.entrySet().iterator();
@ -69,6 +74,11 @@ public class CordovaLocationListener implements LocationListener {
{ {
this.owner.win(loc, callbackId); this.owner.win(loc, callbackId);
} }
if(this.owner.isGlobalListener(this))
{
Log.d(TAG, "Stopping global listener");
this.stop();
}
this.callbacks.clear(); this.callbacks.clear();
Iterator it = this.watches.entrySet().iterator(); Iterator it = this.watches.entrySet().iterator();

View File

@ -36,9 +36,13 @@ import org.xmlpull.v1.XmlPullParserException;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.XmlResourceParser; import android.content.res.XmlResourceParser;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -63,15 +67,17 @@ public class CordovaWebView extends WebView {
public PluginManager pluginManager; public PluginManager pluginManager;
public CallbackServer callbackServer; public CallbackServer callbackServer;
private boolean paused; private boolean paused;
private BroadcastReceiver receiver;
/** Actvities and other important classes **/ /** Activities and other important classes **/
private CordovaInterface cordova; private CordovaInterface cordova;
CordovaWebViewClient viewClient; CordovaWebViewClient viewClient;
@SuppressWarnings("unused") @SuppressWarnings("unused")
private CordovaChromeClient chromeClient; private CordovaChromeClient chromeClient;
//This is for the polyfil history //This is for the polyfill history
private String url; private String url;
String baseUrl; String baseUrl;
private Stack<String> urls = new Stack<String>(); private Stack<String> urls = new Stack<String>();
@ -90,6 +96,7 @@ public class CordovaWebView extends WebView {
private boolean handleButton = false; private boolean handleButton = false;
NativeToJsMessageQueue jsMessageQueue; NativeToJsMessageQueue jsMessageQueue;
ExposedJsApi exposedJsApi;
/** /**
* Constructor. * Constructor.
@ -199,8 +206,6 @@ public class CordovaWebView extends WebView {
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@SuppressLint("NewApi") @SuppressLint("NewApi")
private void setup() { private void setup() {
jsMessageQueue = new NativeToJsMessageQueue(this, cordova);
this.setInitialScale(0); this.setInitialScale(0);
this.setVerticalScrollBarEnabled(false); this.setVerticalScrollBarEnabled(false);
this.requestFocusFromTouch(); this.requestFocusFromTouch();
@ -229,16 +234,33 @@ public class CordovaWebView extends WebView {
// Enable built-in geolocation // Enable built-in geolocation
settings.setGeolocationEnabled(true); settings.setGeolocationEnabled(true);
//Start up the plugin manager // Fix for CB-1405
try { // Google issue 4641
this.pluginManager = new PluginManager(this, this.cordova); this.updateUserAgentString();
} catch (Exception e) {
// TODO Auto-generated catch block IntentFilter intentFilter = new IntentFilter();
e.printStackTrace(); intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
if (this.receiver == null) {
this.receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateUserAgentString();
}
};
this.cordova.getActivity().registerReceiver(this.receiver, intentFilter);
} }
// end CB-1405
pluginManager = new PluginManager(this, this.cordova);
jsMessageQueue = new NativeToJsMessageQueue(this, cordova);
exposedJsApi = new ExposedJsApi(pluginManager, jsMessageQueue);
exposeJsInterface(); exposeJsInterface();
} }
private void updateUserAgentString() {
this.getSettings().getUserAgentString();
}
private void exposeJsInterface() { private void exposeJsInterface() {
// addJavascriptInterface crashes on the 2.3 emulator. // addJavascriptInterface crashes on the 2.3 emulator.
@ -246,13 +268,7 @@ public class CordovaWebView extends WebView {
Log.i(TAG, "Disabled addJavascriptInterface() bridge callback due to a bug on the 2.3 emulator"); Log.i(TAG, "Disabled addJavascriptInterface() bridge callback due to a bug on the 2.3 emulator");
return; return;
} }
this.addJavascriptInterface(new Object() { this.addJavascriptInterface(exposedJsApi, "_cordovaNative");
@SuppressWarnings("unused")
public String exec(String service, String action, String callbackId, String arguments) throws JSONException {
PluginResult r = pluginManager.exec(service, action, callbackId, arguments, true /* async */);
return r == null ? "" : r.getJSONString();
}
}, "_cordovaExec");
} }
/** /**
@ -461,7 +477,9 @@ public class CordovaWebView extends WebView {
* @param url * @param url
*/ */
void loadUrlNow(String url) { void loadUrlNow(String url) {
LOG.d(TAG, ">>> loadUrlNow()"); if (LOG.isLoggable(LOG.DEBUG) && !url.startsWith("javascript:")) {
LOG.d(TAG, ">>> loadUrlNow()");
}
super.loadUrl(url); super.loadUrl(url);
} }
@ -499,7 +517,17 @@ public class CordovaWebView extends WebView {
* @param message * @param message
*/ */
public void sendJavascript(String statement) { public void sendJavascript(String statement) {
this.jsMessageQueue.add(statement); this.jsMessageQueue.addJavaScript(statement);
}
/**
* Send a plugin result back to JavaScript.
* (This is a convenience method)
*
* @param message
*/
public void sendPluginResult(PluginResult result, String callbackId) {
this.jsMessageQueue.addPluginResult(result, callbackId);
} }
/** /**
@ -731,7 +759,7 @@ public class CordovaWebView extends WebView {
if(keyDownCodes.contains(keyCode)) if(keyDownCodes.contains(keyCode))
{ {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
// only override default behaviour is event bound // only override default behavior is event bound
LOG.d(TAG, "Down Key Hit"); LOG.d(TAG, "Down Key Hit");
this.loadUrl("javascript:cordova.fireDocumentEvent('volumedownbutton');"); this.loadUrl("javascript:cordova.fireDocumentEvent('volumedownbutton');");
return true; return true;
@ -766,7 +794,7 @@ public class CordovaWebView extends WebView {
if (this.backHistory()) { if (this.backHistory()) {
return true; return true;
} }
// If not, then invoke default behaviour // If not, then invoke default behavior
else { else {
//this.activityState = ACTIVITY_EXITING; //this.activityState = ACTIVITY_EXITING;
return false; return false;
@ -789,7 +817,7 @@ public class CordovaWebView extends WebView {
return super.onKeyUp(keyCode, event); return super.onKeyUp(keyCode, event);
} }
//Does webkit change this behaviour? //Does webkit change this behavior?
return super.onKeyUp(keyCode, event); return super.onKeyUp(keyCode, event);
} }
@ -873,6 +901,15 @@ public class CordovaWebView extends WebView {
if (this.pluginManager != null) { if (this.pluginManager != null) {
this.pluginManager.onDestroy(); this.pluginManager.onDestroy();
} }
// unregister the receiver
if (this.receiver != null) {
try {
this.cordova.getActivity().unregisterReceiver(this.receiver);
} catch (Exception e) {
Log.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e);
}
}
} }
public void onNewIntent(Intent intent) public void onNewIntent(Intent intent)

View File

@ -54,9 +54,6 @@ import android.webkit.WebViewClient;
public class CordovaWebViewClient extends WebViewClient { public class CordovaWebViewClient extends WebViewClient {
private static final String TAG = "Cordova"; private static final String TAG = "Cordova";
// Disable URL-based exec() bridge by default since it's a bit of a
// security concern.
private static boolean ENABLE_LOCATION_CHANGE_EXEC_MODE = false;
private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/"; private static final String CORDOVA_EXEC_URL_PREFIX = "http://cdv_exec/";
CordovaInterface cordova; CordovaInterface cordova;
CordovaWebView appView; CordovaWebView appView;
@ -110,11 +107,7 @@ public class CordovaWebViewClient extends WebViewClient {
String action = url.substring(idx2 + 1, idx3); String action = url.substring(idx2 + 1, idx3);
String callbackId = url.substring(idx3 + 1, idx4); String callbackId = url.substring(idx3 + 1, idx4);
String jsonArgs = url.substring(idx4 + 1); String jsonArgs = url.substring(idx4 + 1);
PluginResult r = appView.pluginManager.exec(service, action, callbackId, jsonArgs, true /* async */); appView.pluginManager.exec(service, action, callbackId, jsonArgs, true /* async */);
String callbackString = r.toCallbackString(callbackId);
if (r != null) {
appView.sendJavascript(callbackString);
}
} }
/** /**
@ -128,7 +121,7 @@ public class CordovaWebViewClient extends WebViewClient {
@Override @Override
public boolean shouldOverrideUrlLoading(WebView view, String url) { public boolean shouldOverrideUrlLoading(WebView view, String url) {
// Check if it's an exec() bridge command message. // Check if it's an exec() bridge command message.
if (ENABLE_LOCATION_CHANGE_EXEC_MODE && url.startsWith(CORDOVA_EXEC_URL_PREFIX)) { if (NativeToJsMessageQueue.ENABLE_LOCATION_CHANGE_EXEC_MODE && url.startsWith(CORDOVA_EXEC_URL_PREFIX)) {
handleExecUrl(url); handleExecUrl(url);
} }

View File

@ -406,6 +406,8 @@ public class DroidGap extends Activity implements CordovaInterface {
} }
this.splashscreenTime = time; this.splashscreenTime = time;
this.splashscreen = this.getIntegerProperty("splashscreen", 0);
this.showSplashScreen(this.splashscreenTime);
this.appView.loadUrl(url, time); this.appView.loadUrl(url, time);
} }
@ -716,7 +718,7 @@ public class DroidGap extends Activity implements CordovaInterface {
*/ */
public void sendJavascript(String statement) { public void sendJavascript(String statement) {
if (this.appView != null) { if (this.appView != null) {
this.appView.jsMessageQueue.add(statement); this.appView.jsMessageQueue.addJavaScript(statement);
} }
} }
@ -996,7 +998,6 @@ public class DroidGap extends Activity implements CordovaInterface {
this.runOnUiThread(runnable); this.runOnUiThread(runnable);
} }
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) public boolean onKeyUp(int keyCode, KeyEvent event)
{ {
@ -1024,8 +1025,11 @@ public class DroidGap extends Activity implements CordovaInterface {
this.removeSplashScreen(); this.removeSplashScreen();
} }
else { else {
this.splashscreen = this.getIntegerProperty("splashscreen", 0); // If the splash dialog is showing don't try to show it again
this.showSplashScreen(this.splashscreenTime); if (this.splashDialog != null && !this.splashDialog.isShowing()) {
this.splashscreen = this.getIntegerProperty("splashscreen", 0);
this.showSplashScreen(this.splashscreenTime);
}
} }
} }
else if ("spinner".equals(id)) { else if ("spinner".equals(id)) {

View File

@ -23,7 +23,7 @@ import java.io.IOException;
import android.media.ExifInterface; import android.media.ExifInterface;
public class ExifHelper { public class ExifHelper {
private String aperature = null; private String aperture = null;
private String datetime = null; private String datetime = null;
private String exposureTime = null; private String exposureTime = null;
private String flash = null; private String flash = null;
@ -70,7 +70,7 @@ public class ExifHelper {
* Reads all the EXIF data from the input file. * Reads all the EXIF data from the input file.
*/ */
public void readExifData() { public void readExifData() {
this.aperature = inFile.getAttribute(ExifInterface.TAG_APERTURE); this.aperture = inFile.getAttribute(ExifInterface.TAG_APERTURE);
this.datetime = inFile.getAttribute(ExifInterface.TAG_DATETIME); this.datetime = inFile.getAttribute(ExifInterface.TAG_DATETIME);
this.exposureTime = inFile.getAttribute(ExifInterface.TAG_EXPOSURE_TIME); this.exposureTime = inFile.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
this.flash = inFile.getAttribute(ExifInterface.TAG_FLASH); this.flash = inFile.getAttribute(ExifInterface.TAG_FLASH);
@ -102,8 +102,8 @@ public class ExifHelper {
return; return;
} }
if (this.aperature != null) { if (this.aperture != null) {
this.outFile.setAttribute(ExifInterface.TAG_APERTURE, this.aperature); this.outFile.setAttribute(ExifInterface.TAG_APERTURE, this.aperture);
} }
if (this.datetime != null) { if (this.datetime != null) {
this.outFile.setAttribute(ExifInterface.TAG_DATETIME, this.datetime); this.outFile.setAttribute(ExifInterface.TAG_DATETIME, this.datetime);

View File

@ -0,0 +1,61 @@
/*
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.
*/
package org.apache.cordova;
import org.apache.cordova.api.PluginManager;
import org.apache.cordova.api.PluginResult;
import org.json.JSONException;
/**
* Contains APIs that the JS can call. All functions in here should also have
* an equivalent entry in CordovaChromeClient.java, and be added to
* cordova-js/lib/android/plugin/android/promptbasednativeapi.js
*/
/* package */ class ExposedJsApi {
private PluginManager pluginManager;
private NativeToJsMessageQueue jsMessageQueue;
public ExposedJsApi(PluginManager pluginManager, NativeToJsMessageQueue jsMessageQueue) {
this.pluginManager = pluginManager;
this.jsMessageQueue = jsMessageQueue;
}
public String exec(String service, String action, String callbackId, String arguments) throws JSONException {
jsMessageQueue.setPaused(true);
try {
boolean wasSync = pluginManager.exec(service, action, callbackId, arguments, true /* async */);
String ret = "";
if (!NativeToJsMessageQueue.DISABLE_EXEC_CHAINING || wasSync) {
ret = jsMessageQueue.popAndEncode();
}
return ret;
} finally {
jsMessageQueue.setPaused(false);
}
}
public void setNativeToJsBridgeMode(int value) {
jsMessageQueue.setBridgeMode(value);
}
public String retrieveJsMessages() {
return jsMessageQueue.popAndEncode();
}
}

View File

@ -85,7 +85,7 @@ public class FileTransfer extends Plugin {
if (action.equals("upload")) { if (action.equals("upload")) {
return upload(URLDecoder.decode(source), target, args); return upload(URLDecoder.decode(source), target, args);
} else if (action.equals("download")) { } else if (action.equals("download")) {
return download(source, target); return download(source, target, args.optBoolean(2));
} else { } else {
return new PluginResult(PluginResult.Status.INVALID_ACTION); return new PluginResult(PluginResult.Status.INVALID_ACTION);
} }
@ -287,7 +287,7 @@ public class FileTransfer extends Plugin {
bytesRead = fileInputStream.read(buffer, 0, bufferSize); bytesRead = fileInputStream.read(buffer, 0, bufferSize);
} }
// send multipart form data necesssary after file data... // send multipart form data necessary after file data...
dos.writeBytes(tailParams); dos.writeBytes(tailParams);
// close streams // close streams
@ -459,7 +459,7 @@ public class FileTransfer extends Plugin {
* @param target Full path of the file on the file system * @param target Full path of the file on the file system
* @return JSONObject the downloaded file * @return JSONObject the downloaded file
*/ */
private PluginResult download(String source, String target) { private PluginResult download(String source, String target, boolean trustEveryone) {
Log.d(LOG_TAG, "download " + source + " to " + target); Log.d(LOG_TAG, "download " + source + " to " + target);
HttpURLConnection connection = null; HttpURLConnection connection = null;
@ -473,7 +473,30 @@ public class FileTransfer extends Plugin {
if (webView.isUrlWhiteListed(source)) if (webView.isUrlWhiteListed(source))
{ {
URL url = new URL(source); URL url = new URL(source);
connection = (HttpURLConnection) url.openConnection(); boolean useHttps = url.getProtocol().toLowerCase().equals("https");
// Open a HTTP connection to the URL based on protocol
if (useHttps) {
// Using standard HTTPS connection. Will not allow self signed certificate
if (!trustEveryone) {
connection = (HttpsURLConnection) url.openConnection();
}
// Use our HTTPS connection that blindly trusts everyone.
// This should only be used in debug environments
else {
// Setup the HTTPS connection class to trust everyone
trustAllHosts();
HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
// Save the current hostnameVerifier
defaultHostnameVerifier = https.getHostnameVerifier();
// Setup the connection not to verify hostnames
https.setHostnameVerifier(DO_NOT_VERIFY);
connection = https;
}
}
// Return a standard HTTP connection
else {
connection = (HttpURLConnection) url.openConnection();
}
connection.setRequestMethod("GET"); connection.setRequestMethod("GET");
//Add cookie support //Add cookie support
@ -516,6 +539,12 @@ public class FileTransfer extends Plugin {
FileUtils fileUtil = new FileUtils(); FileUtils fileUtil = new FileUtils();
JSONObject fileEntry = fileUtil.getEntry(file); JSONObject fileEntry = fileUtil.getEntry(file);
// Revert back to the proper verifier and socket factories
if (trustEveryone && url.getProtocol().toLowerCase().equals("https")) {
((HttpsURLConnection) connection).setHostnameVerifier(defaultHostnameVerifier);
HttpsURLConnection.setDefaultSSLSocketFactory(defaultSSLSocketFactory);
}
return new PluginResult(PluginResult.Status.OK, fileEntry); return new PluginResult(PluginResult.Status.OK, fileEntry);
} }
else else

View File

@ -409,19 +409,19 @@ public class FileUtils extends Plugin {
throw new InvalidModificationException("Can't rename a file to a directory"); throw new InvalidModificationException("Can't rename a file to a directory");
} }
FileChannel input = new FileInputStream(srcFile).getChannel(); FileInputStream istream = new FileInputStream(srcFile);
FileChannel output = new FileOutputStream(destFile).getChannel(); FileOutputStream ostream = new FileOutputStream(destFile);
FileChannel input = istream.getChannel();
FileChannel output = ostream.getChannel();
input.transferTo(0, input.size(), output); try {
input.transferTo(0, input.size(), output);
input.close(); } finally {
output.close(); istream.close();
ostream.close();
/* input.close();
if (srcFile.length() != destFile.length()) { output.close();
return false;
} }
*/
return getEntry(destFile); return getEntry(destFile);
} }
@ -480,7 +480,7 @@ public class FileUtils extends Plugin {
// This weird test is to determine if we are copying or moving a directory into itself. // 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 is okay but
// Copy /sdcard/myDir to /sdcard/myDir/backup should thow an INVALID_MODIFICATION_ERR // Copy /sdcard/myDir to /sdcard/myDir/backup should throw an INVALID_MODIFICATION_ERR
if (dest.startsWith(src) && dest.indexOf(File.separator, src.length() - 1) != -1) { if (dest.startsWith(src) && dest.indexOf(File.separator, src.length() - 1) != -1) {
return true; return true;
} }
@ -1008,14 +1008,17 @@ public class FileUtils extends Plugin {
filename = stripFileProtocol(filename); filename = stripFileProtocol(filename);
RandomAccessFile raf = new RandomAccessFile(filename, "rw"); RandomAccessFile raf = new RandomAccessFile(filename, "rw");
try {
if (raf.length() >= size) { if (raf.length() >= size) {
FileChannel channel = raf.getChannel(); FileChannel channel = raf.getChannel();
channel.truncate(size); channel.truncate(size);
return size; return size;
}
return raf.length();
} finally {
raf.close();
} }
return raf.length();
} }
/** /**
@ -1040,7 +1043,7 @@ public class FileUtils extends Plugin {
* Queries the media store to find out what the file path is for the Uri we supply * 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 * @param contentUri the Uri of the audio/image/video
* @param cordova) the current applicaiton context * @param cordova the current application context
* @return the full path to the file * @return the full path to the file
*/ */
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")

View File

@ -191,4 +191,14 @@ public class GeoBroker extends Plugin {
this.error(result, callbackId); this.error(result, callbackId);
} }
public boolean isGlobalListener(CordovaLocationListener listener)
{
if (gpsListener != null && networkListener != null)
{
return gpsListener.equals(listener) || networkListener.equals(listener);
}
else
return false;
}
} }

View File

@ -19,12 +19,11 @@
package org.apache.cordova; package org.apache.cordova;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedList; import java.util.LinkedList;
import org.apache.cordova.api.CordovaInterface; import org.apache.cordova.api.CordovaInterface;
import org.apache.cordova.api.PluginResult;
import android.os.Message; import android.os.Message;
import android.util.Log; import android.util.Log;
@ -39,15 +38,36 @@ public class NativeToJsMessageQueue {
// This must match the default value in incubator-cordova-js/lib/android/exec.js // This must match the default value in incubator-cordova-js/lib/android/exec.js
private static final int DEFAULT_BRIDGE_MODE = 1; private static final int DEFAULT_BRIDGE_MODE = 1;
// Set this to true to force plugin results to be encoding as
// JS instead of the custom format (useful for benchmarking).
private static final boolean FORCE_ENCODE_USING_EVAL = false;
// Disable URL-based exec() bridge by default since it's a bit of a
// security concern.
static final boolean ENABLE_LOCATION_CHANGE_EXEC_MODE = false;
// Disable sending back native->JS messages during an exec() when the active
// exec() is asynchronous. Set this to true when running bridge benchmarks.
static final boolean DISABLE_EXEC_CHAINING = false;
// Arbitrarily chosen upper limit for how much data to send to JS in one shot.
private static final int MAX_PAYLOAD_SIZE = 50 * 1024;
/** /**
* The index into registeredListeners to treat as active. * The index into registeredListeners to treat as active.
*/ */
private int activeListenerIndex; private int activeListenerIndex;
/**
* When true, the active listener is not fired upon enqueue. When set to false,
* the active listener will be fired if the queue is non-empty.
*/
private boolean paused;
/** /**
* The list of JavaScript statements to be sent to JavaScript. * The list of JavaScript statements to be sent to JavaScript.
*/ */
private final LinkedList<String> queue = new LinkedList<String>(); private final LinkedList<JsMessage> queue = new LinkedList<JsMessage>();
/** /**
* The array of listeners that can be used to send messages to JS. * The array of listeners that can be used to send messages to JS.
@ -56,7 +76,7 @@ public class NativeToJsMessageQueue {
private final CordovaInterface cordova; private final CordovaInterface cordova;
private final CordovaWebView webView; private final CordovaWebView webView;
public NativeToJsMessageQueue(CordovaWebView webView, CordovaInterface cordova) { public NativeToJsMessageQueue(CordovaWebView webView, CordovaInterface cordova) {
this.cordova = cordova; this.cordova = cordova;
this.webView = webView; this.webView = webView;
@ -81,7 +101,7 @@ public class NativeToJsMessageQueue {
synchronized (this) { synchronized (this) {
activeListenerIndex = value; activeListenerIndex = value;
BridgeMode activeListener = registeredListeners[value]; BridgeMode activeListener = registeredListeners[value];
if (!queue.isEmpty() && activeListener != null) { if (!paused && !queue.isEmpty() && activeListener != null) {
activeListener.onNativeToJsMessageAvailable(); activeListener.onNativeToJsMessageAvailable();
} }
} }
@ -99,60 +119,151 @@ public class NativeToJsMessageQueue {
} }
} }
private int calculatePackedMessageLength(JsMessage message) {
int messageLen = message.calculateEncodedLength();
String messageLenStr = String.valueOf(messageLen);
return messageLenStr.length() + messageLen + 1;
}
private void packMessage(JsMessage message, StringBuilder sb) {
sb.append(message.calculateEncodedLength())
.append(' ');
message.encodeAsMessage(sb);
}
/** /**
* Removes and returns the last statement in the queue. * Combines and returns queued messages combined into a single string.
* Combines as many messages as possible, while staying under MAX_PAYLOAD_SIZE.
* Returns null if the queue is empty. * Returns null if the queue is empty.
*/ */
public String pop() { public String popAndEncode() {
synchronized (this) { synchronized (this) {
if (queue.isEmpty()) { if (queue.isEmpty()) {
return null; return null;
} }
return queue.remove(0); int totalPayloadLen = 0;
int numMessagesToSend = 0;
for (JsMessage message : queue) {
int messageSize = calculatePackedMessageLength(message);
if (numMessagesToSend > 0 && totalPayloadLen + messageSize > MAX_PAYLOAD_SIZE) {
break;
}
totalPayloadLen += messageSize;
numMessagesToSend += 1;
}
StringBuilder sb = new StringBuilder(totalPayloadLen);
for (int i = 0; i < numMessagesToSend; ++i) {
JsMessage message = queue.removeFirst();
packMessage(message, sb);
}
if (!queue.isEmpty()) {
// Attach a char to indicate that there are more messages pending.
sb.append('*');
}
return sb.toString();
} }
} }
/** /**
* Combines and returns all statements. Clears the queue. * Same as popAndEncode(), except encodes in a form that can be executed as JS.
* Returns null if the queue is empty.
*/ */
public String popAll() { private String popAndEncodeAsJs() {
synchronized (this) { synchronized (this) {
int length = queue.size(); int length = queue.size();
if (length == 0) { if (length == 0) {
return null; return null;
} }
StringBuffer sb = new StringBuffer(); int totalPayloadLen = 0;
int numMessagesToSend = 0;
for (JsMessage message : queue) {
int messageSize = message.calculateEncodedLength() + 50; // overestimate.
if (numMessagesToSend > 0 && totalPayloadLen + messageSize > MAX_PAYLOAD_SIZE) {
break;
}
totalPayloadLen += messageSize;
numMessagesToSend += 1;
}
boolean willSendAllMessages = numMessagesToSend == queue.size();
StringBuilder sb = new StringBuilder(totalPayloadLen + (willSendAllMessages ? 0 : 100));
// Wrap each statement in a try/finally so that if one throws it does // Wrap each statement in a try/finally so that if one throws it does
// not affect the next. // not affect the next.
int i = 0; for (int i = 0; i < numMessagesToSend; ++i) {
for (String message : queue) { JsMessage message = queue.removeFirst();
if (++i == length) { if (willSendAllMessages && (i + 1 == numMessagesToSend)) {
sb.append(message); message.encodeAsJsMessage(sb);
} else { } else {
sb.append("try{") sb.append("try{");
.append(message) message.encodeAsJsMessage(sb);
.append("}finally{"); sb.append("}finally{");
} }
} }
for ( i = 1; i < length; ++i) { if (!willSendAllMessages) {
sb.append("window.setTimeout(function(){cordova.require('cordova/plugin/android/polling').pollOnce();},0);");
}
for (int i = willSendAllMessages ? 1 : 0; i < numMessagesToSend; ++i) {
sb.append('}'); sb.append('}');
} }
queue.clear();
return sb.toString(); return sb.toString();
} }
} }
/** /**
* Add a JavaScript statement to the list. * Add a JavaScript statement to the list.
*/ */
public void add(String statement) { public void addJavaScript(String statement) {
enqueueMessage(new JsMessage(statement));
}
/**
* Add a JavaScript statement to the list.
*/
public void addPluginResult(PluginResult result, String callbackId) {
// Don't send anything if there is no result and there is no need to
// clear the callbacks.
boolean noResult = result.getStatus() == PluginResult.Status.NO_RESULT.ordinal();
boolean keepCallback = result.getKeepCallback();
if (noResult && keepCallback) {
return;
}
JsMessage message = new JsMessage(result, callbackId);
if (FORCE_ENCODE_USING_EVAL) {
StringBuilder sb = new StringBuilder(message.calculateEncodedLength() + 50);
message.encodeAsJsMessage(sb);
message = new JsMessage(sb.toString());
}
enqueueMessage(message);
}
private void enqueueMessage(JsMessage message) {
synchronized (this) { synchronized (this) {
queue.add(statement); queue.add(message);
if (registeredListeners[activeListenerIndex] != null) { if (!paused && registeredListeners[activeListenerIndex] != null) {
registeredListeners[activeListenerIndex].onNativeToJsMessageAvailable(); registeredListeners[activeListenerIndex].onNativeToJsMessageAvailable();
} }
}
}
public void setPaused(boolean value) {
if (paused && value) {
// This should never happen. If a use-case for it comes up, we should
// change pause to be a counter.
Log.e(LOG_TAG, "nested call to setPaused detected.", new Throwable());
} }
paused = value;
if (!value) {
synchronized (this) {
if (!queue.isEmpty() && registeredListeners[activeListenerIndex] != null) {
registeredListeners[activeListenerIndex].onNativeToJsMessageAvailable();
}
}
}
}
public boolean getPaused() {
return paused;
} }
private interface BridgeMode { private interface BridgeMode {
@ -170,8 +281,17 @@ public class NativeToJsMessageQueue {
/** Uses webView.loadUrl("javascript:") to execute messages. */ /** Uses webView.loadUrl("javascript:") to execute messages. */
private class LoadUrlBridgeMode implements BridgeMode { private class LoadUrlBridgeMode implements BridgeMode {
final Runnable runnable = new Runnable() {
public void run() {
String js = popAndEncodeAsJs();
if (js != null) {
webView.loadUrlNow("javascript:" + js);
}
}
};
public void onNativeToJsMessageAvailable() { public void onNativeToJsMessageAvailable() {
webView.loadUrlNow("javascript:" + popAll()); cordova.getActivity().runOnUiThread(runnable);
} }
} }
@ -187,7 +307,9 @@ public class NativeToJsMessageQueue {
} }
} }
}; };
OnlineEventsBridgeMode() {
webView.setNetworkAvailable(true);
}
public void onNativeToJsMessageAvailable() { public void onNativeToJsMessageAvailable() {
cordova.getActivity().runOnUiThread(runnable); cordova.getActivity().runOnUiThread(runnable);
} }
@ -241,7 +363,7 @@ public class NativeToJsMessageQueue {
} }
// webViewCore is lazily initialized, and so may not be available right away. // webViewCore is lazily initialized, and so may not be available right away.
if (sendMessageMethod != null) { if (sendMessageMethod != null) {
String js = popAll(); String js = popAndEncodeAsJs();
Message execJsMessage = Message.obtain(null, EXECUTE_JS, js); Message execJsMessage = Message.obtain(null, EXECUTE_JS, js);
try { try {
sendMessageMethod.invoke(webViewCore, execJsMessage); sendMessageMethod.invoke(webViewCore, execJsMessage);
@ -251,4 +373,94 @@ public class NativeToJsMessageQueue {
} }
} }
} }
private static class JsMessage {
final String jsPayloadOrCallbackId;
final PluginResult pluginResult;
JsMessage(String js) {
jsPayloadOrCallbackId = js;
pluginResult = null;
}
JsMessage(PluginResult pluginResult, String callbackId) {
jsPayloadOrCallbackId = callbackId;
this.pluginResult = pluginResult;
}
int calculateEncodedLength() {
if (pluginResult == null) {
return jsPayloadOrCallbackId.length() + 1;
}
int statusLen = String.valueOf(pluginResult.getStatus()).length();
int ret = 2 + statusLen + 1 + jsPayloadOrCallbackId.length() + 1;
switch (pluginResult.getMessageType()) {
case PluginResult.MESSAGE_TYPE_BOOLEAN:
ret += 1;
break;
case PluginResult.MESSAGE_TYPE_NUMBER: // n
ret += 1 + pluginResult.getMessage().length();
break;
case PluginResult.MESSAGE_TYPE_STRING: // s
ret += 1 + pluginResult.getStrMessage().length();
break;
case PluginResult.MESSAGE_TYPE_JSON:
default:
ret += pluginResult.getMessage().length();
}
return ret;
}
void encodeAsMessage(StringBuilder sb) {
if (pluginResult == null) {
sb.append('J')
.append(jsPayloadOrCallbackId);
return;
}
int status = pluginResult.getStatus();
boolean noResult = status == PluginResult.Status.NO_RESULT.ordinal();
boolean resultOk = status == PluginResult.Status.OK.ordinal();
boolean keepCallback = pluginResult.getKeepCallback();
sb.append((noResult || resultOk) ? 'S' : 'F')
.append(keepCallback ? '1' : '0')
.append(status)
.append(' ')
.append(jsPayloadOrCallbackId)
.append(' ');
switch (pluginResult.getMessageType()) {
case PluginResult.MESSAGE_TYPE_BOOLEAN:
sb.append(pluginResult.getMessage().charAt(0)); // t or f.
break;
case PluginResult.MESSAGE_TYPE_NUMBER: // n
sb.append('n')
.append(pluginResult.getMessage());
break;
case PluginResult.MESSAGE_TYPE_STRING: // s
sb.append('s')
.append(pluginResult.getStrMessage());
break;
case PluginResult.MESSAGE_TYPE_JSON:
default:
sb.append(pluginResult.getMessage()); // [ or {
}
}
void encodeAsJsMessage(StringBuilder sb) {
if (pluginResult == null) {
sb.append(jsPayloadOrCallbackId);
} else {
int status = pluginResult.getStatus();
boolean success = (status == PluginResult.Status.OK.ordinal()) || (status == PluginResult.Status.NO_RESULT.ordinal());
sb.append("cordova.callbackFromNative('")
.append(jsPayloadOrCallbackId)
.append("',")
.append(success)
.append(",")
.append(status)
.append(",")
.append(pluginResult.getMessage())
.append(",")
.append(pluginResult.getKeepCallback())
.append(");");
}
}
}
} }

View File

@ -180,7 +180,7 @@ public class Storage extends Plugin {
} }
/** /**
* Checks to see the the query is a Data Definintion command * Checks to see the the query is a Data Definition command
* *
* @param query to be executed * @param query to be executed
* @return true if it is a DDL command, false otherwise * @return true if it is a DDL command, false otherwise

View File

@ -139,14 +139,19 @@ public abstract class Plugin implements IPlugin {
/** /**
* Send generic JavaScript statement back to JavaScript. * Send generic JavaScript statement back to JavaScript.
* success(...) and error(...) should be used instead where possible. * sendPluginResult() should be used instead where possible.
*
* @param statement
*/ */
public void sendJavascript(String statement) { public void sendJavascript(String statement) {
this.webView.sendJavascript(statement); this.webView.sendJavascript(statement);
} }
/**
* Send generic JavaScript statement back to JavaScript.
*/
public void sendPluginResult(PluginResult pluginResult, String callbackId) {
this.webView.sendPluginResult(pluginResult, callbackId);
}
/** /**
* Call the JavaScript success callback for this plugin. * Call the JavaScript success callback for this plugin.
* *
@ -158,7 +163,7 @@ public abstract class Plugin implements IPlugin {
* @param callbackId The callback id used when calling back into JavaScript. * @param callbackId The callback id used when calling back into JavaScript.
*/ */
public void success(PluginResult pluginResult, String callbackId) { public void success(PluginResult pluginResult, String callbackId) {
this.webView.sendJavascript(pluginResult.toSuccessCallbackString(callbackId)); this.webView.sendPluginResult(pluginResult, callbackId);
} }
/** /**
@ -168,7 +173,7 @@ public abstract class Plugin implements IPlugin {
* @param callbackId The callback id used when calling back into JavaScript. * @param callbackId The callback id used when calling back into JavaScript.
*/ */
public void success(JSONObject message, String callbackId) { public void success(JSONObject message, String callbackId) {
this.webView.sendJavascript(new PluginResult(PluginResult.Status.OK, message).toSuccessCallbackString(callbackId)); this.webView.sendPluginResult(new PluginResult(PluginResult.Status.OK, message), callbackId);
} }
/** /**
@ -178,7 +183,7 @@ public abstract class Plugin implements IPlugin {
* @param callbackId The callback id used when calling back into JavaScript. * @param callbackId The callback id used when calling back into JavaScript.
*/ */
public void success(String message, String callbackId) { public void success(String message, String callbackId) {
this.webView.sendJavascript(new PluginResult(PluginResult.Status.OK, message).toSuccessCallbackString(callbackId)); this.webView.sendPluginResult(new PluginResult(PluginResult.Status.OK, message), callbackId);
} }
/** /**
@ -188,7 +193,7 @@ public abstract class Plugin implements IPlugin {
* @param callbackId The callback id used when calling back into JavaScript. * @param callbackId The callback id used when calling back into JavaScript.
*/ */
public void error(PluginResult pluginResult, String callbackId) { public void error(PluginResult pluginResult, String callbackId) {
this.webView.sendJavascript(pluginResult.toErrorCallbackString(callbackId)); this.webView.sendPluginResult(pluginResult, callbackId);
} }
/** /**
@ -198,7 +203,7 @@ public abstract class Plugin implements IPlugin {
* @param callbackId The callback id used when calling back into JavaScript. * @param callbackId The callback id used when calling back into JavaScript.
*/ */
public void error(JSONObject message, String callbackId) { public void error(JSONObject message, String callbackId) {
this.webView.sendJavascript(new PluginResult(PluginResult.Status.ERROR, message).toErrorCallbackString(callbackId)); this.webView.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, message), callbackId);
} }
/** /**
@ -208,6 +213,6 @@ public abstract class Plugin implements IPlugin {
* @param callbackId The callback id used when calling back into JavaScript. * @param callbackId The callback id used when calling back into JavaScript.
*/ */
public void error(String message, String callbackId) { public void error(String message, String callbackId) {
this.webView.sendJavascript(new PluginResult(PluginResult.Status.ERROR, message).toErrorCallbackString(callbackId)); this.webView.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, message), callbackId);
} }
} }

View File

@ -22,6 +22,8 @@ import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.cordova.CordovaWebView; import org.apache.cordova.CordovaWebView;
import org.json.JSONArray; import org.json.JSONArray;
@ -45,6 +47,7 @@ public class PluginManager {
private final CordovaInterface ctx; private final CordovaInterface ctx;
private final CordovaWebView app; private final CordovaWebView app;
private final ExecutorService execThreadPool = Executors.newCachedThreadPool();
// Flag to track first time through // Flag to track first time through
private boolean firstRun; private boolean firstRun;
@ -200,7 +203,7 @@ public class PluginManager {
* or execute the class denoted by the clazz argument. * or execute the class denoted by the clazz argument.
* *
* @param service String containing the service to run * @param service String containing the service to run
* @param action String containt the action that the class is supposed to perform. This is * @param action String containing the action that the class is supposed to perform. This is
* passed to the plugin execute method and it is up to the plugin developer * passed to the plugin execute method and it is up to the plugin developer
* how to deal with it. * how to deal with it.
* @param callbackId String containing the id of the callback that is execute in JavaScript if * @param callbackId String containing the id of the callback that is execute in JavaScript if
@ -210,10 +213,9 @@ public class PluginManager {
* @param async Boolean indicating whether the calling JavaScript code is expecting an * @param async Boolean indicating whether the calling JavaScript code is expecting an
* immediate return value. If true, either Cordova.callbackSuccess(...) or * immediate return value. If true, either Cordova.callbackSuccess(...) or
* Cordova.callbackError(...) is called once the plugin code has executed. * Cordova.callbackError(...) is called once the plugin code has executed.
* * @return Whether the task completed synchronously.
* @return PluginResult to send to the page, or null if no response is ready yet.
*/ */
public PluginResult exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) { public boolean exec(final String service, final String action, final String callbackId, final String jsonArgs, final boolean async) {
PluginResult cr = null; PluginResult cr = null;
boolean runAsync = async; boolean runAsync = async;
try { try {
@ -224,30 +226,26 @@ public class PluginManager {
runAsync = async && !plugin.isSynch(action); runAsync = async && !plugin.isSynch(action);
if (runAsync) { if (runAsync) {
// Run this on a different thread so that this one can return back to JS // Run this on a different thread so that this one can return back to JS
Thread thread = new Thread(new Runnable() { execThreadPool.execute(new Runnable() {
public void run() { public void run() {
try { try {
// Call execute on the plugin so that it can do it's thing // Call execute on the plugin so that it can do it's thing
PluginResult cr = plugin.execute(action, args, callbackId); PluginResult cr = plugin.execute(action, args, callbackId);
String callbackString = cr.toCallbackString(callbackId); app.sendPluginResult(cr, callbackId);
if (callbackString != null) {
app.sendJavascript(callbackString);
}
} catch (Exception e) { } catch (Exception e) {
PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage()); PluginResult cr = new PluginResult(PluginResult.Status.ERROR, e.getMessage());
app.sendJavascript(cr.toErrorCallbackString(callbackId)); app.sendPluginResult(cr, callbackId);
} }
} }
}); });
thread.start(); return false;
return null;
} else { } else {
// Call execute on the plugin so that it can do it's thing // Call execute on the plugin so that it can do it's thing
cr = plugin.execute(action, args, callbackId); cr = plugin.execute(action, args, callbackId);
// If no result to be sent and keeping callback, then no need to sent back to JavaScript // If no result to be sent and keeping callback, then no need to sent back to JavaScript
if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) { if ((cr.getStatus() == PluginResult.Status.NO_RESULT.ordinal()) && cr.getKeepCallback()) {
return null; return true;
} }
} }
} }
@ -260,12 +258,13 @@ public class PluginManager {
if (cr == null) { if (cr == null) {
cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION); cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION);
} }
app.sendJavascript(cr.toErrorCallbackString(callbackId)); app.sendPluginResult(cr, callbackId);
} }
if (cr == null) { if (cr == null) {
cr = new PluginResult(PluginResult.Status.NO_RESULT); cr = new PluginResult(PluginResult.Status.NO_RESULT);
} }
return cr; app.sendPluginResult(cr, callbackId);
return true;
} }
/** /**

View File

@ -23,43 +23,49 @@ import org.json.JSONObject;
public class PluginResult { public class PluginResult {
private final int status; private final int status;
private final String message; private final int messageType;
private boolean keepCallback = false; private boolean keepCallback = false;
private String strMessage;
private String encodedMessage;
public PluginResult(Status status) { public PluginResult(Status status) {
this.status = status.ordinal(); this(status, PluginResult.StatusMessages[status.ordinal()]);
this.message = "\"" + PluginResult.StatusMessages[this.status] + "\"";
} }
public PluginResult(Status status, String message) { public PluginResult(Status status, String message) {
this.status = status.ordinal(); this.status = status.ordinal();
this.message = JSONObject.quote(message); this.messageType = MESSAGE_TYPE_STRING;
this.strMessage = message;
} }
public PluginResult(Status status, JSONArray message) { public PluginResult(Status status, JSONArray message) {
this.status = status.ordinal(); this.status = status.ordinal();
this.message = message.toString(); this.messageType = MESSAGE_TYPE_JSON;
encodedMessage = message.toString();
} }
public PluginResult(Status status, JSONObject message) { public PluginResult(Status status, JSONObject message) {
this.status = status.ordinal(); this.status = status.ordinal();
this.message = message.toString(); this.messageType = MESSAGE_TYPE_JSON;
encodedMessage = message.toString();
} }
public PluginResult(Status status, int i) { public PluginResult(Status status, int i) {
this.status = status.ordinal(); this.status = status.ordinal();
this.message = ""+i; this.messageType = MESSAGE_TYPE_NUMBER;
this.encodedMessage = ""+i;
} }
public PluginResult(Status status, float f) { public PluginResult(Status status, float f) {
this.status = status.ordinal(); this.status = status.ordinal();
this.message = ""+f; this.messageType = MESSAGE_TYPE_NUMBER;
this.encodedMessage = ""+f;
} }
public PluginResult(Status status, boolean b) { public PluginResult(Status status, boolean b) {
this.status = status.ordinal(); this.status = status.ordinal();
this.message = ""+b; this.messageType = MESSAGE_TYPE_BOOLEAN;
this.encodedMessage = Boolean.toString(b);
} }
public void setKeepCallback(boolean b) { public void setKeepCallback(boolean b) {
@ -70,18 +76,35 @@ public class PluginResult {
return status; return status;
} }
public int getMessageType() {
return messageType;
}
public String getMessage() { public String getMessage() {
return message; if (encodedMessage == null) {
encodedMessage = JSONObject.quote(strMessage);
}
return encodedMessage;
}
/**
* If messageType == MESSAGE_TYPE_STRING, then returns the message string.
* Otherwise, returns null.
*/
public String getStrMessage() {
return strMessage;
} }
public boolean getKeepCallback() { public boolean getKeepCallback() {
return this.keepCallback; return this.keepCallback;
} }
@Deprecated // Use sendPluginResult instead of sendJavascript.
public String getJSONString() { public String getJSONString() {
return "{\"status\":" + this.status + ",\"message\":" + this.message + ",\"keepCallback\":" + this.keepCallback + "}"; return "{\"status\":" + this.status + ",\"message\":" + this.getMessage() + ",\"keepCallback\":" + this.keepCallback + "}";
} }
@Deprecated // Use sendPluginResult instead of sendJavascript.
public String toCallbackString(String callbackId) { public String toCallbackString(String callbackId) {
// If no result to be sent and keeping callback, then no need to sent back to JavaScript // If no result to be sent and keeping callback, then no need to sent back to JavaScript
if ((status == PluginResult.Status.NO_RESULT.ordinal()) && keepCallback) { if ((status == PluginResult.Status.NO_RESULT.ordinal()) && keepCallback) {
@ -95,14 +118,22 @@ public class PluginResult {
return toErrorCallbackString(callbackId); return toErrorCallbackString(callbackId);
} }
@Deprecated // Use sendPluginResult instead of sendJavascript.
public String toSuccessCallbackString(String callbackId) { public String toSuccessCallbackString(String callbackId) {
return "cordova.callbackSuccess('"+callbackId+"',"+this.getJSONString()+");"; return "cordova.callbackSuccess('"+callbackId+"',"+this.getJSONString()+");";
} }
@Deprecated // Use sendPluginResult instead of sendJavascript.
public String toErrorCallbackString(String callbackId) { public String toErrorCallbackString(String callbackId) {
return "cordova.callbackError('"+callbackId+"', " + this.getJSONString()+ ");"; return "cordova.callbackError('"+callbackId+"', " + this.getJSONString()+ ");";
} }
public static final int MESSAGE_TYPE_STRING = 1;
public static final int MESSAGE_TYPE_JSON = 2;
public static final int MESSAGE_TYPE_NUMBER = 3;
public static final int MESSAGE_TYPE_BOOLEAN = 4;
public static String[] StatusMessages = new String[] { public static String[] StatusMessages = new String[] {
"No result", "No result",
"OK", "OK",