From f5271431fbcda70188f485d9e91541bb596551fe Mon Sep 17 00:00:00 2001 From: riknoll Date: Tue, 10 Nov 2015 15:01:13 -0800 Subject: [PATCH] CB-8917: New Plugin API for passing results on resume after Activity destruction --- cordova-js-src/platform.js | 16 +++- .../org/apache/cordova/CallbackContext.java | 2 +- .../apache/cordova/CordovaInterfaceImpl.java | 34 +++++++-- .../src/org/apache/cordova/CordovaPlugin.java | 34 +++++++-- .../apache/cordova/CordovaWebViewImpl.java | 5 +- .../src/org/apache/cordova/CoreAndroid.java | 42 ++++++++-- .../src/org/apache/cordova/PluginManager.java | 13 ++++ .../org/apache/cordova/ResumeCallback.java | 76 +++++++++++++++++++ 8 files changed, 201 insertions(+), 21 deletions(-) create mode 100644 framework/src/org/apache/cordova/ResumeCallback.java diff --git a/cordova-js-src/platform.js b/cordova-js-src/platform.js index bffc6751..0706a348 100644 --- a/cordova-js-src/platform.js +++ b/cordova-js-src/platform.js @@ -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); } diff --git a/framework/src/org/apache/cordova/CallbackContext.java b/framework/src/org/apache/cordova/CallbackContext.java index 446c37d9..4c0d7b98 100644 --- a/framework/src/org/apache/cordova/CallbackContext.java +++ b/framework/src/org/apache/cordova/CallbackContext.java @@ -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) { diff --git a/framework/src/org/apache/cordova/CordovaInterfaceImpl.java b/framework/src/org/apache/cordova/CordovaInterfaceImpl.java index 3f5e69d0..65e2a90d 100644 --- a/framework/src/org/apache/cordova/CordovaInterfaceImpl.java +++ b/framework/src/org/apache/cordova/CordovaInterfaceImpl.java @@ -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; } } - - } diff --git a/framework/src/org/apache/cordova/CordovaPlugin.java b/framework/src/org/apache/cordova/CordovaPlugin.java index 4627ebbe..41af1db7 100644 --- a/framework/src/org/apache/cordova/CordovaPlugin.java +++ b/framework/src/org/apache/cordova/CordovaPlugin.java @@ -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; @@ -77,7 +78,7 @@ public class CordovaPlugin { public String getServiceName() { return serviceName; } - + /** * Executes the request. * @@ -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. * @@ -323,7 +347,7 @@ public class CordovaPlugin { */ public void onReset() { } - + /** * Called when the system received an HTTP authentication request. Plugin can use * the supplied HttpAuthHandler to process this auth challenge. @@ -332,14 +356,14 @@ public class CordovaPlugin { * @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. diff --git a/framework/src/org/apache/cordova/CordovaWebViewImpl.java b/framework/src/org/apache/cordova/CordovaWebViewImpl.java index 06da55e1..59a0de7a 100644 --- a/framework/src/org/apache/cordova/CordovaWebViewImpl.java +++ b/framework/src/org/apache/cordova/CordovaWebViewImpl.java @@ -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"); } diff --git a/framework/src/org/apache/cordova/CoreAndroid.java b/framework/src/org/apache/cordova/CoreAndroid.java index 000717a2..90d079ea 100755 --- a/framework/src/org/apache/cordova/CoreAndroid.java +++ b/framework/src/org/apache/cordova/CoreAndroid.java @@ -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")) { - messageChannel = callbackContext; + 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; + } + } + } } diff --git a/framework/src/org/apache/cordova/PluginManager.java b/framework/src/org/apache/cordova/PluginManager.java index 3afbc18d..64147da0 100755 --- a/framework/src/org/apache/cordova/PluginManager.java +++ b/framework/src/org/apache/cordova/PluginManager.java @@ -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; + } } diff --git a/framework/src/org/apache/cordova/ResumeCallback.java b/framework/src/org/apache/cordova/ResumeCallback.java new file mode 100644 index 00000000..49a43b5d --- /dev/null +++ b/framework/src/org/apache/cordova/ResumeCallback.java @@ -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 result = new ArrayList(); + result.add(eventResult); + result.add(pluginResult); + + CoreAndroid appPlugin = (CoreAndroid) pluginManager.getPlugin(CoreAndroid.PLUGIN_NAME); + appPlugin.sendResumeEvent(new PluginResult(PluginResult.Status.OK, result)); + } +}