CB-8917: New Plugin API for passing results on resume after Activity destruction

This commit is contained in:
riknoll 2015-11-10 15:01:13 -08:00
parent c30eeee5d8
commit f5271431fb
8 changed files with 201 additions and 21 deletions

View File

@ -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);
}

View File

@ -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) {

View File

@ -19,7 +19,6 @@
package org.apache.cordova;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
@ -28,6 +27,7 @@ import android.os.Bundle;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -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());
@ -95,12 +97,28 @@ public class CordovaInterfaceImpl implements CordovaInterface {
}
/**
* 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));
}
}
}
@ -115,6 +133,10 @@ public class CordovaInterfaceImpl implements CordovaInterface {
savedResult = new ActivityResultHolder(requestCode, resultCode, intent);
if (pluginManager != null) {
callback = pluginManager.getPlugin(initCallbackService);
if(callback != null) {
callback.onRestoreStateForActivityResult(savedPluginState.getBundle(callback.getServiceName()),
new ResumeCallback(callback.getServiceName(), pluginManager));
}
}
}
activityResultCallback = null;
@ -126,7 +148,7 @@ public class CordovaInterfaceImpl implements CordovaInterface {
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;
}
@ -147,6 +169,8 @@ public class CordovaInterfaceImpl implements CordovaInterface {
String serviceName = activityResultCallback.getServiceName();
outState.putString("callbackService", serviceName);
}
outState.putBundle("plugin", pluginManager.onSaveInstanceState());
}
/**
@ -154,6 +178,8 @@ public class CordovaInterfaceImpl implements CordovaInterface {
*/
public void restoreInstanceState(Bundle savedInstanceState) {
initCallbackService = savedInstanceState.getString("callbackService");
savedPluginState = savedInstanceState.getBundle("plugin");
activityWasDestroyed = true;
}
private static class ActivityResultHolder {
@ -209,6 +235,4 @@ public class CordovaInterfaceImpl implements CordovaInterface {
return true;
}
}
}

View File

@ -30,6 +30,7 @@ import android.content.pm.PackageManager;
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;
@ -174,6 +175,29 @@ public class CordovaPlugin {
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.
*

View File

@ -445,7 +445,10 @@ public class CordovaWebViewImpl implements CordovaWebView {
// 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");
}

View File

@ -19,10 +19,6 @@
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;
@ -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.
@ -112,7 +110,13 @@ class CoreAndroid extends CordovaPlugin {
this.exitApp();
}
else if (action.equals("messageChannel")) {
synchronized(messageChannelLock) {
messageChannel = callbackContext;
if (pendingResume != null) {
sendEventMessage(pendingResume);
pendingResume = null;
}
}
return true;
}
@ -313,10 +317,13 @@ class CoreAndroid extends CordovaPlugin {
} 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);
}
}
@ -328,4 +335,23 @@ class CoreAndroid extends CordovaPlugin {
{
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;
}
}
}
}

View File

@ -26,6 +26,7 @@ import org.json.JSONException;
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;
@ -511,4 +512,16 @@ public class PluginManager {
}
}
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;
}
}

View File

@ -0,0 +1,76 @@
/*
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.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.List;
public class ResumeCallback extends CallbackContext {
private final String TAG = "CordovaResumeCallback";
private String serviceName;
private PluginManager pluginManager;
public ResumeCallback(String serviceName, PluginManager pluginManager) {
super("resumecallback", null);
this.serviceName = serviceName;
this.pluginManager = pluginManager;
}
@Override
public void sendPluginResult(PluginResult pluginResult) {
synchronized (this) {
if (finished) {
LOG.w(TAG, serviceName + " attempted to send a second callback to ResumeCallback\nResult was: " + pluginResult.getMessage());
return;
} else {
finished = true;
}
}
JSONObject event = new JSONObject();
JSONObject pluginResultObject = new JSONObject();
try {
pluginResultObject.put("pluginServiceName", this.serviceName);
pluginResultObject.put("pluginStatus", PluginResult.StatusMessages[pluginResult.getStatus()]);
event.put("action", "resume");
event.put("pendingResult", pluginResultObject);
} catch (JSONException e) {
LOG.e(TAG, "Unable to create resume object for Activity Result");
}
PluginResult eventResult = new PluginResult(PluginResult.Status.OK, event);
// We send a list of results to the js so that we don't have to decode
// the PluginResult passed to this CallbackContext into JSON twice.
// The results are combined into an event payload before the event is
// fired on the js side of things (see platform.js)
List<PluginResult> result = new ArrayList<PluginResult>();
result.add(eventResult);
result.add(pluginResult);
CoreAndroid appPlugin = (CoreAndroid) pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME);
appPlugin.sendResumeEvent(new PluginResult(PluginResult.Status.OK, result));
}
}