Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CB-8917: Added pending plugin callbacks to resume event payload #239

Merged
merged 1 commit into from
Dec 2, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 15 additions & 1 deletion cordova-js-src/platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,26 @@ function onMessageFromNative(msg) {
case 'searchbutton':
// App life cycle events
case 'pause':
case 'resume':
// Volume events
case 'volumedownbutton':
case 'volumeupbutton':
cordova.fireDocumentEvent(action);
break;
case 'resume':
if(arguments.length > 1 && msg.pendingResult) {
if(arguments.length === 2) {
msg.pendingResult.result = arguments[1];
} else {
// The plugin returned a multipart message
var res = [];
for(var i = 1; i < arguments.length; i++) {
res.push(arguments[i]);
}
msg.pendingResult.result = res;
}
}
cordova.fireDocumentEvent(action, msg);
break;
default:
throw new Error('Unknown event action ' + action);
}
Expand Down
2 changes: 1 addition & 1 deletion framework/src/org/apache/cordova/CallbackContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public class CallbackContext {

private String callbackId;
private CordovaWebView webView;
private boolean finished;
protected boolean finished;
private int changingThreads;

public CallbackContext(String callbackId, CordovaWebView webView) {
Expand Down
34 changes: 29 additions & 5 deletions framework/src/org/apache/cordova/CordovaInterfaceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Licensed to the Apache Software Foundation (ASF) under one

package org.apache.cordova;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
Expand All @@ -28,6 +27,7 @@ Licensed to the Apache Software Foundation (ASF) under one
import android.util.Log;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Expand All @@ -46,6 +46,8 @@ public class CordovaInterfaceImpl implements CordovaInterface {
protected CordovaPlugin permissionResultCallback;
protected String initCallbackService;
protected int activityResultRequestCode;
protected boolean activityWasDestroyed = false;
protected Bundle savedPluginState;

public CordovaInterfaceImpl(Activity activity) {
this(activity, Executors.newCachedThreadPool());
Expand Down Expand Up @@ -95,12 +97,28 @@ public ExecutorService getThreadPool() {
}

/**
* Dispatches any pending onActivityResult callbacks.
* Dispatches any pending onActivityResult callbacks and sends the resume event if the
* Activity was destroyed by the OS.
*/
public void onCordovaInit(PluginManager pluginManager) {
this.pluginManager = pluginManager;
if (savedResult != null) {
onActivityResult(savedResult.requestCode, savedResult.resultCode, savedResult.intent);
} else if(activityWasDestroyed) {
// If there was no Activity result, we still need to send out the resume event if the
// Activity was destroyed by the OS
activityWasDestroyed = false;

CoreAndroid appPlugin = (CoreAndroid) pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME);
if(appPlugin != null) {
JSONObject obj = new JSONObject();
try {
obj.put("action", "resume");
} catch (JSONException e) {
LOG.e(TAG, "Failed to create event message", e);
}
appPlugin.sendResumeEvent(new PluginResult(PluginResult.Status.OK, obj));
}
}
}

Expand All @@ -115,6 +133,10 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent intent)
savedResult = new ActivityResultHolder(requestCode, resultCode, intent);
if (pluginManager != null) {
callback = pluginManager.getPlugin(initCallbackService);
if(callback != null) {
callback.onRestoreStateForActivityResult(savedPluginState.getBundle(callback.getServiceName()),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure savedPluginState will always be non-null at this point? And that it will have the expected bundle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe so. This will only be called after the Activity was destroyed in which case onSaveInstanceState() will have been called and the savedPluginState bundle saved. I believe Activity results are only delivered after onCreate() is called, so the Bundle will have already been retrieved (but I can double check that). It is the responsibility of the plugin to check if the bundle they receive is null.

new ResumeCallback(callback.getServiceName(), pluginManager));
}
}
}
activityResultCallback = null;
Expand All @@ -126,7 +148,7 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent intent)
callback.onActivityResult(requestCode, resultCode, intent);
return true;
}
Log.w(TAG, "Got an activity result, but no plugin was registered to receive it" + (savedResult != null ? " yet!": "."));
Log.w(TAG, "Got an activity result, but no plugin was registered to receive it" + (savedResult != null ? " yet!" : "."));
return false;
}

Expand All @@ -147,13 +169,17 @@ public void onSaveInstanceState(Bundle outState) {
String serviceName = activityResultCallback.getServiceName();
outState.putString("callbackService", serviceName);
}

outState.putBundle("plugin", pluginManager.onSaveInstanceState());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to check if pluginManager is not null? I ask because there is a check for that on line 116.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pluginManager should be created when the first URL is loaded. In the default Android project, this happens in onCreate() so it shouldn't be an issue.

}

/**
* Call this from onCreate() so that any saved startActivityForResult parameters will be restored.
*/
public void restoreInstanceState(Bundle savedInstanceState) {
initCallbackService = savedInstanceState.getString("callbackService");
savedPluginState = savedInstanceState.getBundle("plugin");
activityWasDestroyed = true;
}

private static class ActivityResultHolder {
Expand Down Expand Up @@ -209,6 +235,4 @@ public boolean hasPermission(String permission)
return true;
}
}


}
34 changes: 29 additions & 5 deletions framework/src/org/apache/cordova/CordovaPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Licensed to the Apache Software Foundation (ASF) under one
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;

import java.io.FileNotFoundException;
import java.io.IOException;
Expand Down Expand Up @@ -77,7 +78,7 @@ protected void pluginInitialize() {
public String getServiceName() {
return serviceName;
}

/**
* Executes the request.
*
Expand Down Expand Up @@ -174,6 +175,29 @@ public void onNewIntent(Intent intent) {
public void onDestroy() {
}

/**
* Called when the Activity is being destroyed (e.g. if a plugin calls out to an external
* Activity and the OS kills the CordovaActivity in the background). The plugin should save its
* state in this method only if it is awaiting the result of an external Activity and needs
* to preserve some information so as to handle that result; onRestoreStateForActivityResult()
* will only be called if the plugin is the recipient of an Activity result
*
* @return Bundle containing the state of the plugin or null if state does not need to be saved
*/
public Bundle onSaveInstanceState() {
return null;
}

/**
* Called when a plugin is the recipient of an Activity result after the CordovaActivity has
* been destroyed. The Bundle will be the same as the one the plugin returned in
* onSaveInstanceState()
*
* @param state Bundle containing the state of the plugin
* @param callbackContext Replacement Context to return the plugin result to
*/
public void onRestoreStateForActivityResult(Bundle state, CallbackContext callbackContext) {}

/**
* Called when a message is sent to plugin.
*
Expand Down Expand Up @@ -323,7 +347,7 @@ protected Uri fromPluginUri(Uri pluginUri) {
*/
public void onReset() {
}

/**
* Called when the system received an HTTP authentication request. Plugin can use
* the supplied HttpAuthHandler to process this auth challenge.
Expand All @@ -332,14 +356,14 @@ public void onReset() {
* @param handler The HttpAuthHandler used to set the WebView's response
* @param host The host requiring authentication
* @param realm The realm for which authentication is required
*
*
* @return Returns True if plugin will resolve this auth challenge, otherwise False
*
*
*/
public boolean onReceivedHttpAuthRequest(CordovaWebView view, ICordovaHttpAuthHandler handler, String host, String realm) {
return false;
}

/**
* Called when he system received an SSL client certificate request. Plugin can use
* the supplied ClientCertRequest to process this certificate challenge.
Expand Down
5 changes: 4 additions & 1 deletion framework/src/org/apache/cordova/CordovaWebViewImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,10 @@ public void handleResume(boolean keepRunning) {
// Resume JavaScript timers. This affects all webviews within the app!
engine.setPaused(false);
this.pluginManager.onResume(keepRunning);
// To be the same as other platforms, fire this event only when resumed after a "pause".

// In order to match the behavior of the other platforms, we only send onResume after an
// onPause has occurred. The resume event might still be sent if the Activity was killed
// while waiting for the result of an external Activity once the result is obtained
if (hasPausedEver) {
sendJavascriptEvent("resume");
}
Expand Down
42 changes: 34 additions & 8 deletions framework/src/org/apache/cordova/CoreAndroid.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ Licensed to the Apache Software Foundation (ASF) under one

package org.apache.cordova;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.LOG;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
Expand All @@ -45,6 +41,8 @@ class CoreAndroid extends CordovaPlugin {
protected static final String TAG = "CordovaApp";
private BroadcastReceiver telephonyReceiver;
private CallbackContext messageChannel;
private PluginResult pendingResume;
private final Object messageChannelLock = new Object();

/**
* Send an event to be fired on the Javascript side.
Expand Down Expand Up @@ -112,7 +110,13 @@ else if (action.equals("exitApp")) {
this.exitApp();
}
else if (action.equals("messageChannel")) {
messageChannel = callbackContext;
synchronized(messageChannelLock) {
messageChannel = callbackContext;
if (pendingResume != null) {
sendEventMessage(pendingResume);
pendingResume = null;
}
}
return true;
}

Expand Down Expand Up @@ -313,10 +317,13 @@ private void sendEventMessage(String action) {
} catch (JSONException e) {
LOG.e(TAG, "Failed to create event message", e);
}
PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, obj);
pluginResult.setKeepCallback(true);
sendEventMessage(new PluginResult(PluginResult.Status.OK, obj));
}

private void sendEventMessage(PluginResult payload) {
payload.setKeepCallback(true);
if (messageChannel != null) {
messageChannel.sendPluginResult(pluginResult);
messageChannel.sendPluginResult(payload);
}
}

Expand All @@ -328,4 +335,23 @@ public void onDestroy()
{
webView.getContext().unregisterReceiver(this.telephonyReceiver);
}

/**
* Used to send the resume event in the case that the Activity is destroyed by the OS
*
* @param resumeEvent PluginResult containing the payload for the resume event to be fired
*/
public void sendResumeEvent(PluginResult resumeEvent) {
// This operation must be synchronized because plugin results that trigger resume
// events can be processed asynchronously
synchronized(messageChannelLock) {
if (messageChannel != null) {
sendEventMessage(resumeEvent);
} else {
// Might get called before the page loads, so we need to store it until the
// messageChannel gets created
this.pendingResume = resumeEvent;
}
}
}
}
13 changes: 13 additions & 0 deletions framework/src/org/apache/cordova/PluginManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Licensed to the Apache Software Foundation (ASF) under one
import android.content.Intent;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.Debug;
import android.util.Log;

Expand Down Expand Up @@ -511,4 +512,16 @@ public void onConfigurationChanged(Configuration newConfig) {
}
}

public Bundle onSaveInstanceState() {
Bundle state = new Bundle();
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Bundle pluginState = plugin.onSaveInstanceState();
if(pluginState != null) {
state.putBundle(plugin.getServiceName(), pluginState);
}
}
}
return state;
}
}