Asynchronous communication between Javascript and Phonegap Plugin

21,047

Solution 1

You are almost there but you need to setKeepCallback to true on your PluginResult. If you don't the subsequent results from the Java side will not have a callback on the JavaScript side. The best example of this type of coding is the Network plugin in Cordova core. Here is a link to the source:

https://git-wip-us.apache.org/repos/asf?p=cordova-plugin-network-information.git;a=blob;f=src/android/NetworkManager.java;h=e2ac500ccc885db641d5df6dab8eae23026a5828;hb=HEAD

So you should update your code to:

public boolean execute(String action, final JSONArray args,
        final CallbackContext callbackId) throws JSONException {
    IntentFilter wifiFilter = new IntentFilter(
            WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION);
    cordova.getActivity().registerReceiver(wifiBroadcastReceiver,
            wifiFilter);
    this.callbackContext = callbackId;
    PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT);
    result.setKeepCallback(true);
    this.callbackContext.sendPluginResult(result);
    return true;
}

public class WifiReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if (action.equals(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION)) {
            PluginResult result;
            if (intent.getBooleanExtra(
                    WifiManager.EXTRA_SUPPLICANT_CONNECTED, false)) {
                Toast.makeText(cordova.getActivity(), "Wifi Connected",
                        Toast.LENGTH_SHORT).show();
                result = new PluginResult(PluginResult.Status.OK,
                        "Wifi Connected");
            } else {
                Toast.makeText(cordova.getActivity(), "Wifi Disconnected",
                        Toast.LENGTH_SHORT).show();
                result = new PluginResult(PluginResult.Status.ERROR,
                        "Wifi Disconnected");
            }

            result.setKeepCallback(false);
            if (callbackContext != null) {
                callbackContext.sendPluginResult(result);
                callbackContext = null;
            }
        }
    }
}

Solution 2

Answer to 'second callback' warning...

The Cordova source-code which triggers this warning can be found on line 57 here:

https://github.com/apache/cordova-android/blob/master/framework/src/org/apache/cordova/CallbackContext.java

Thus - warning is caused because your CallbackContext object has 'finished=true'.

Most likely cause of this is you called: callbackContext.sendPluginResult(pluginResult);

Without first calling: pluginResult.setKeepCallback(true);

If not... most likely you are unintentionally caching the CallbackContext object.

Your execute() function should assign CallbackContext each time it is called. See lines 125-127 in the code Simon linked to:

public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {

if (action.equals("getConnectionInfo")) {`

this.connectionCallbackContext = callbackContext;

...

The proper sequence of events in full:

  1. Make initial call to plugin.

  2. Plugin saves reference to passed in CallbackContext object.

  3. Keep CallbackContext object reference, while returning results with setKeepCallback(true).

  4. When the sequence is finished, return with setKeepCallback(false) (the default)

Then later...

  1. Make another call to plugin.

  2. Plugin overwrites saved CallbackContext reference, replace with passed in object.

Then steps 3-4 same as above.

Hope that helps :)

Share:
21,047

Related videos on Youtube

Anas Azeem
Author by

Anas Azeem

Updated on July 10, 2022

Comments

  • Anas Azeem
    Anas Azeem almost 2 years

    So, everybody knows that we make a Class extending CordovaPlugin and override the execute() and then creates a bridge between the JS and native Java (for Android). Further we use PluginResult to return the result back to the JS.

    So, all of this happens when there is a request fired from the JS to the Java Plugin. My question is, how to send a result back to JS (and therefore to HTML) asynchronously ?

    I don't know if the word asynchronous is right here. The thing is I want to send something back to the JS out of the blue (say, when wifi becomes enable/disable).

    I have already researched on this but haven't got anything which suits to my case.

    The thing I've tried is -

    • Created a BroadcastReceiver listening to the WiFi events using the WifiManager class.
    • Registered the receiver.
    • And finally, popping a Toast when WiFi is enabled/disabled, and sending the result using CallbackContext

      callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, "Wifi Connected")) and for disconnected with a different message.

    MyPlugin.java

    import org.apache.cordova.CallbackContext;
    import org.apache.cordova.CordovaPlugin;
    import org.apache.cordova.PluginResult;
    import org.json.JSONArray;
    
    ...
    
    public class MyPlugin extends CordovaPlugin {
    private WifiReceiver wifiBroadcastReceiver = null;
    private CallbackContext callbackContext = null;
    
    ...
    
        public MyPlugin() {     
            wifiBroadcastReceiver = new WifiReceiver();
        ...
        }
        ...
        public boolean execute(String action, final JSONArray args,
                final CallbackContext callbackId) throws JSONException {
            IntentFilter wifiFilter = new IntentFilter(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION);
            cordova.getActivity().registerReceiver(wifiBroadcastReceiver, wifiFilter);
            this.callbackContext = callbackId;
    
        ...
        }
        public class WifiReceiver extends BroadcastReceiver{
    
            @Override
            public void onReceive(Context context, Intent intent) {
                final String action = intent.getAction();
                if (action.equals(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION)) {
                    if (intent.getBooleanExtra(WifiManager.EXTRA_SUPPLICANT_CONNECTED, false)) {
                        Toast.makeText(cordova.getActivity(), "Wifi Connected", Toast.LENGTH_SHORT).show();
                        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, "Wifi Connected"));
                    } else {
                        Toast.makeText(cordova.getActivity(), "Wifi Disconnected", Toast.LENGTH_SHORT).show();
                        callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR, "Wifi Disconnected"));
                    }
                }           
            }
    
    }
    

    The Toast pops but the PluginResult isn't sent to the JS.


    PS : Listening to WiFi events isn't my actual problem, I want to replicate the Android Bluetooth Chat app in Phonegap. So, it has to be asynchronous in nature.

  • Anas Azeem
    Anas Azeem over 10 years
    Thank you for your answer, i'll just check it out.
  • Anas Azeem
    Anas Azeem over 10 years
    I implemented this in my original Bluetooth Chat app. JS does receive the result but it is in the form of Second Callback. And how did I notice it? It reflects in the Logcat that W/CordovaPlugin(6976): Attempted to send a second callback for ID: BluetoothPlugin1980589494<BR>W/CordovaPlugin(6976): Result was: "Hello". So, now the question is, How to handle these Second Callbacks in JS or in HTML?
  • Anas Azeem
    Anas Azeem over 10 years
    Please reply Simon, i'm desperately waiting.
  • Anas Azeem
    Anas Azeem over 10 years
    Hey Simon, you must be pretty busy? That's why you are not replying. I hope that everything is okay. Can you suggest me using sendJavascript() to send back the result back to Javascript, I didn't find any proper documentation for this method. Please tell me what its implications could be instead of using the callback.
  • Anas Azeem
    Anas Azeem over 10 years
    The solution you suggested was good if there is only one exec call from Javascript to Java Plugin's execute(). My problem is that, I have many exec calls from Javascript to Java, so, I am wondering that in which exec call's callback I will handle those Second Callbacks. That was the reason I switched to sendJavascript() so that I may call a Javascript function whenever I want [say, when a new chat msg arrived]. This is my stuff and I am stuck on it. And no one's replying. What should I use, second callback or sendJavascript()?
  • Dunc
    Dunc over 9 years
    Wow, there is a LOT of insight in this answer... maybe you should write the Cordova docs!
  • Paul Weber
    Paul Weber almost 9 years
    One thing I found out the hard way is that if you return false in the execute Method, there will be a result generated for you and you cannot return any more results. So always return true there then it works like a charm! This can be another reason for the "second callback warning".
  • SkyTreasure
    SkyTreasure over 8 years
    In the javascript/html file how do you register this event?
  • Jules
    Jules over 6 years
    @Dunc - certainly somebody should try writing proper documentation for writing Cordova plugins. Seriously. The documentation is absurdly bad, at least on the Android side. For example, it states that if you need to perform any initialization when the web view starts, you should implement initialize(CordovaInterface, CordovaWebView) ... but if you read the source, there's a comment there that states that plugins shouldn't really be doing this, but should implement pluginInitialize() instead.