apache/cordova

Cordova with registerForActivityResult.

Closed this issue · 2 comments

Bug Report

In the existing code we are using Cordova with startActivityForResult(), as part of third party plugin update we are using registerForActivityResult().
I am looking for an example of Cordova with registerForActivityResult()

Below is the code implemented.
package com.xyz.emcd;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.util.Base64;
import android.util.Log;
import com.miteksystems.misnap.core.MiSnapSettings;
import com.miteksystems.misnap.workflow.MiSnapWorkflowStep;
import com.miteksystems.misnap.workflow.fragment.DocumentAnalysisFragment;
import com.miteksystems.misnap.workflow.fragment.DocumentAnalysisFragment.Companion;
import com.miteksystems.misnap.workflow.fragment.DocumentAnalysisFragment.ReviewCondition;
import com.miteksystems.misnap.workflow.MiSnapWorkflowActivity;
import com.miteksystems.misnap.workflow.MiSnapFinalResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.activity.result.contract.ActivityResultContracts;

import java.util.List;
import java.util.Set;

/**

  • This cordova plugin handles taking a picture using the MiSnap SDK

  • @author Ewan Summers
    */
    public class CameraPlugin extends CordovaPlugin {

    // The tag we'd like to use for this class
    public static final String TAG = "XYZ_CAMERA_PLUGIN";

    // Actions
    public static final String TAKE_PICTURE_ACTION = "takePicture";

    private static final String CHECK_FRONT = "CheckFront";
    private static final String CHECK_BACK = "CheckBack";

    private static int REQUEST_CODE_FRONT = 56104;
    private static int REQUEST_CODE_BACK = 56105;

    private CallbackContext callbackContext = null;

    ActivityResultLauncher activityResultLauncher;

    /**

    • Constructor.
      */
      public CameraPlugin() {
      super();
      }

    private void logError(String msg) {
    Log.e(TAG, msg);
    }

    private void logDebug(String msg) {
    Log.d(TAG, msg);
    }

    /**

    • Convenience method to both add to the debug log and error on the callback
    • context
    • @param msg
      */
      private void doContextError(String msg) {
      logDebug(msg);
      callbackContext.error(msg);
      }

    /**

    • Convenience method to both log error and error on the callback context
    • @param msg
      */
      private void doError(String msg) {
      logError(msg);
      callbackContext.error(msg);
      }

    @OverRide
    public void initialize(CordovaInterface cordova, CordovaWebView webView) {
    super.initialize(cordova, webView);

     activityResultLauncher = cordova.getActivity().registerForActivityResult(
             (ActivityResultContract) new ActivityResultContracts.StartActivityForResult(),
             (ActivityResultCallback) new ActivityResultCallback<Set<String>>() {
                 @Override
                 public void onActivityResult(Set<String> result) {
                     logDebug("got results from authorization request");
                     if (callbackContext != null) {
                         for (String res : result) {
                             logDebug(res);
                         }
                         if (result.isEmpty()) {
                             callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, false));
                         } else {
                             callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, true));
                         }
                     } else {
                         logDebug("Got activity results before callback was created");
                     }
                 }
             });
    

    }

    /**

    • Executes the request and returns PluginResult.

    • @param action

    •                    The action to execute.
      
    • @param args

    •                    JSONArry of arguments for the plugin.
      
    • @param callbackContext

    •                    The callback id used when calling back into
      
    •                    JavaScript.
      
    • @return True if the action was valid, false if not.
      */
      public boolean execute(String action, JSONArray args,
      CallbackContext callbackContext) throws JSONException {

      logDebug("Executing action: " + action);

      // Unfortunately the callbackContext is not serializable so we cannot
      // pass it through to the
      // intent. Let's just give the plugin a reference to the callbackContext
      this.callbackContext = callbackContext;

      if (action.equals(TAKE_PICTURE_ACTION)) {

       final CameraPluginConfigTakePicture config = new CameraPluginConfigTakePicture(
               args);
       logDebug("Received " + action + " action with config: "
               + config.toString());
      
       // Kick off taking a picture in a new thread rather
      
       this.cordova.getThreadPool().execute(new Runnable() {
      
           @Override
           public void run() {
               logDebug("before takePicture call : " + config.getMiSnapSDKLicenseKey());
               takePicture(config.isFront(), config.isDisableAutoCapture(), config.getMiSnapSDKLicenseKey());
               logDebug("Harika ::: Received miSnapSDKLicenseKey: " + config.getMiSnapSDKLicenseKey());
           }
       });
       return true;
      

      } else {
      doError("Unknown Action '" + (action != null ? action : "") + "'");
      return false;
      }
      }

    /**

    • Constructs the intent with the MiSnap settings and starts the activity
      */
      private void takePicture(boolean isFrontOfCheque, boolean disableAutoCapture, String miSnapSDKLicenseKey) {
      logDebug("inside takePicture call : " + miSnapSDKLicenseKey);
      JSONObject jjs = null;
      MiSnapSettings miSnapSettings = null;

      try {
      jjs = new JSONObject();

       jjs.put("MiSnapAllowScreenshots", "1");
       jjs.put("MiSnapAngle", "150");
       
       if (disableAutoCapture) {
           jjs.put("MiSnapCaptureMode", "1");
       } else {
           jjs.put("MiSnapCaptureMode", "2");
       }
       
       if (isFrontOfCheque) {
           
           miSnapSettings = new MiSnapSettings(MiSnapSettings.UseCase.CHECK_FRONT, miSnapSDKLicenseKey);
           jjs.put("MiSnapSharpness", "600");
       } else {
           
           miSnapSettings = new MiSnapSettings(MiSnapSettings.UseCase.CHECK_BACK, miSnapSDKLicenseKey);
           
           jjs.put("MiSnapSharpness", "100");
       }
      

      } catch (JSONException e) {
      doError("JSON exception occurred when constructing MiSnap options");
      }

      miSnapSettings.analysis.document.setTrigger(MiSnapSettings.Analysis.Document.Trigger.MANUAL);
      miSnapSettings.analysis.document.setEnableEnhancedManual(true);

      MiSnapSettings.Workflow misnapWorkflow = miSnapSettings.workflow;
      misnapWorkflow.add("CMD App",
      DocumentAnalysisFragment.buildWorkflowSettings((Integer) null, (Float) null, (Float) null,
      (Boolean) null, (Integer) null, (Boolean) null, (Integer) null, (Integer) null, (Integer) null,
      (Integer) null, (Integer) null, (Boolean) null, (Integer) null, (Integer) null, (Integer) null,
      (Integer) null, (Integer) null, (Integer) null, (Boolean) null, (String) null, (Boolean) null,
      (Boolean) null, ReviewCondition.NEVER, (Boolean) null, (Boolean) null));

      MiSnapWorkflowStep[] workflowSteps = new MiSnapWorkflowStep[1];
      MiSnapWorkflowStep miSnapWorkflowStep = new MiSnapWorkflowStep(miSnapSettings);
      workflowSteps[0] = miSnapWorkflowStep;
      Intent i = MiSnapWorkflowActivity.buildIntent(this.cordova.getActivity().getApplicationContext(),
      miSnapWorkflowStep, workflowSteps, false);

      activityResultLauncher.launch(i);
      }

    private class CameraPluginConfigTakePicture {
    private boolean isFront;
    private boolean disableAutoCapture;
    private String miSnapSDKLicenseKey;

     public CameraPluginConfigTakePicture(JSONArray configArray) {
         try {
             logDebug("CameraPluginConfigTakePicture: configArray.length() ::: " + configArray.length());
             logDebug("CameraPluginConfigTakePicture: configArray.toString() ::: " + configArray.toString());
             if (configArray.length() > 0) {
                 logDebug("Inside if condition for configArray.length(), configArray: "
                         + configArray.toString());
                 isFront = (Boolean) configArray.get(0);
                 disableAutoCapture = (Boolean) configArray.get(1);
                 miSnapSDKLicenseKey = (String) configArray.get(2);
    
                 logDebug("CameraPluginConfigTakePicture: miSnapSDKLicenseKey ::: " + miSnapSDKLicenseKey);
             }
         } catch (JSONException e) {
             logError("Failure to parse given configuration: "
                     + e.getMessage());
         }
     }
    
     public boolean isFront() {
         return isFront;
     }
    
     public boolean isDisableAutoCapture() {
         return disableAutoCapture;
     }
    
     public String getMiSnapSDKLicenseKey() {
         logDebug("Inside getMiSnapSDKLicenseKey: " + miSnapSDKLicenseKey);
         return miSnapSDKLicenseKey;
     }
    
     @Override
     public String toString() {
         return "CameraPluginConfig [isFront=" + isFront + ", isDisableAutoCapture=" + disableAutoCapture
                 + ", miSnapSDKLicenseKey= " + miSnapSDKLicenseKey + "]";
     }
    

    }
    }

This is the exception i am getting.

07-08 15:23:59.554 7396 7468 W System.err: java.lang.IllegalStateException: LifecycleOwner com.ncr.cmd.qa.Emcd@acc2928 is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.
07-08 15:23:59.554 7396 7468 W System.err: at androidx.activity.result.ActivityResultRegistry.register(ActivityResultRegistry.java:123)
07-08 15:23:59.555 7396 7468 W System.err: at androidx.activity.ComponentActivity.registerForActivityResult(ComponentActivity.java:833)
07-08 15:23:59.555 7396 7468 W System.err: at androidx.activity.ComponentActivity.registerForActivityResult(ComponentActivity.java:842)
07-08 15:23:59.555 7396 7468 W System.err: at com.xyz.emcd.CameraPlugin.initialize(CameraPlugin.java:97)
07-08 15:23:59.555 7396 7468 W System.err: at org.apache.cordova.CordovaPlugin.privateInitialize(CordovaPlugin.java:59)
07-08 15:23:59.555 7396 7468 W System.err: at org.apache.cordova.PluginManager.getPlugin(PluginManager.java:187)
07-08 15:23:59.555 7396 7468 W System.err: at org.apache.cordova.PluginManager.exec(PluginManager.java:138)
07-08 15:23:59.555 7396 7468 W System.err: at org.apache.cordova.CordovaBridge.jsExec(CordovaBridge.java:59)
07-08 15:23:59.555 7396 7468 W System.err: at org.apache.cordova.engine.SystemExposedJsApi.exec(SystemExposedJsApi.java:41)
07-08 15:23:59.556 7396 7468 W System.err: at android.os.MessageQueue.nativePollOnce(Native Method)
07-08 15:23:59.556 7396 7468 W System.err: at android.os.MessageQueue.next(MessageQueue.java:335)
07-08 15:23:59.556 7396 7468 W System.err: at android.os.Looper.loopOnce(Looper.java:161)
07-08 15:23:59.556 7396 7468 W System.err: at android.os.Looper.loop(Looper.java:288)
07-08 15:23:59.556 7396 7468 W System.err: at android.os.HandlerThread.run(HandlerThread.java:67)

Version information

Cordova: 12.0.0
Operating System, Android.

the androidx's registerForActivityResult must be called before the activity is created. I believe it is valid to register activity result callbacks during the onCreate lifecycle event as that's something I've done here.

Note: You must call registerForActivityResult() before the fragment or activity is created, but you can't launch the ActivityResultLauncher until the fragment or activity's Lifecycle has reached CREATED.

https://developer.android.com/training/basics/intents/result#register

I'm not aware of any cordova plugins that actually makes use of the androidx's APIs, most plugins will hook into the older API, including the cordova framework itself, which exposes the original activity result APIs. But I can provide some information that might assist you.

Cordova plugins are lazy-loaded by default. This means they are not created/initialised until they are called upon by the javascript. This will be well-after the onCreate lifecycle event and if you're plugin initialises at this time, it will be too late to register activity result callbacks.

This is undocumented, and potentially an implementation detail, so it might be dangerous to rely on this... but if the plugin is marked to initialize onload, then the plugin will be initialised when Cordova is initialised on load, which happens to be during the onCreate lifecycle event. Therefore if the plugin has onload enabled, and it registers the activity result callback in it's initialize method, then it should work.

I'm also going to migrate this issue to discussions since it isn't describing a bug with cordova-android.