Designing an expo react-native app to return a value to Collect

I want to use an expo react-native app to find a value and return it to an odk-collect form. I am able to open my app from the odk form with ex:android.intent.action.VIEW(uri_data='myapp://hello/path')
but I have not been able to respond with parameters. The odk documentation says that the external app should do the following:

Uri uri = ...
Intent returnIntent = new Intent();
returnIntent.clipData = ClipData.newRawUri(null, uri);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
setResult(Activity.RESULT_OK, intent);
finish();

Has anyone been able to do this with a react native or expo app?

I've been doing variations of this without success:

await startActivityAsync('android.intent.action.VIEW',
      {
        extra: { my_variable: 'my_response' },
        flags: 1,  // which I believe would be the FLAG_GRANT_READ_URI_PERMISSION value
      }

Were you able to figure this one out? I see there's also a similar post on StackOverflow.

There's nothing in your code to set a clipData value but it looks like maybe you just want to send back text so that would be ok.

I agree from the Android docs that 1 should set the desired flag but this is only relevant if you're sending back media.

I believe the fundamental issue is that startActivityAsync is not providing an activity result. What you've written launches a whole new intent when you want to return from your app to Collect. You may want to look into a package or approach like https://github.com/rozele/react-native-activity-result

Thank you for following up on this LN

I found a lot of limitations using react native's intent options, and took a different route.
I'm using expo's option to create a native module, and that way I've been able to consume ODK Collect's API more directly using Kotlin.

Here are some examples (I'm new to Kotlin so there might be issues with this):

package expo.modules.odksync

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.odksync.OdkSyncView
import android.content.Intent
import android.net.Uri
import android.provider.BaseColumns
import android.app.Activity
import expo.modules.kotlin.exception.CodedException


class OdkSyncModule : Module() {
  override fun definition() = ModuleDefinition {
    // Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
    Function("selectParcela") { datos: Map<String, Any> ->
      return@Function returnMultipleData(datos)
    }

    Function("getInstances") { 
       return@Function getInstances()
    }

      Function("openOdkForms") { 
       return@Function openOdkForms()
    }
  }

  private val context
  get() = requireNotNull(appContext.reactContext)

  private val currentActivity
  get() = appContext.activityProvider?.currentActivity ?: throw CodedException("Activity which was provided during module initialization is no longer available")

  private fun openOdkForms() {
      val myIntent = Intent(Intent.ACTION_VIEW)
      myIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
      myIntent.setType("vnd.android.cursor.dir/vnd.odk.form")
      context.startActivity(myIntent)
  }

  private fun getInstances(): List<Map<String, Any>> {

      val odkUrl = "content://org.odk.collect.android.provider.odk.instances/instances"
      val uri: Uri = Uri.parse(odkUrl)
        var cursor = context.contentResolver.query( uri, null, null, null, null)
        
        val odkFormData = mutableListOf<Map<String, Any>>()
        if (cursor != null) {
            try {
                while (cursor.moveToNext()) {
                    val dataMap = mutableMapOf<String, Any>()
                    var id = cursor.getInt(cursor.getColumnIndex(BaseColumns._ID))
                    dataMap["displayName"] = cursor.getString(cursor.getColumnIndex("displayName"))
                    dataMap["jrFormId"] = cursor.getString(cursor.getColumnIndex("jrFormId"))
                    dataMap["jrVersion"] = cursor.getString(cursor.getColumnIndex("jrVersion"))
                    dataMap["status"] = cursor.getString(cursor.getColumnIndex("status"))
                    odkFormData.add(dataMap)
                }
            } finally {
                cursor.close()
            }
            return odkFormData
        } else {
            val dataMap = mutableMapOf<String, Any>()
            dataMap["warning"] = "Unable to get forms"
            odkFormData.add(dataMap)
            return odkFormData
        }
        
  }

    private fun returnMultipleData(datos: Map<String, Any>) {
      if (currentActivity.getReferrer().toString() == "android-app://org.odk.collect.android") {
          val intent = Intent()

          val nomParcela = datos["nombre_parcela"]?.toString() ?: ""
          val uuidParcela = datos["uuid_parcela"]?.toString() ?: ""
          val idCoop = datos["id_cooperativa"]?.toString() ?: ""
          val nomCoop = datos["nombre_cooperativa"]?.toString() ?: ""

          intent.putExtra("id_parcela_nombre", nomParcela)
          intent.putExtra("id_parcela_uuid" , uuidParcela)
          intent.putExtra("id_cooperativa", idCoop)
          intent.putExtra("id_cooperativa_nombre", nomCoop)
          currentActivity.setResult(Activity.RESULT_OK, intent)
          currentActivity.finish()
      }
  }
}

With that I can now export functions to use in my react native code:

import OdkSyncModule from './src/OdkSyncModule';


export function selectParcela(data: Object): string {
  return OdkSyncModule.selectParcela(data);
}

export function getInstances(): Object {
  return OdkSyncModule.getInstances();
}


export function openOdkForms(): Object {
  return OdkSyncModule.openOdkForms();
}

export function getForms(): Object {
  return OdkSyncModule.getForms();
}
1 Like

I'm glad you've figured something out! Creating a native module makes a lot of sense for this very Android-specific behavior.

@spwoodcock you may want to consider this kind of concept to address your need to have an external web app set an entity in your form. Basically you could have your web app launch Collect and remember the last entityid that was selected, then have the first question in the form that's launched call to your app and request the last selected entityid. It's a bit silly but could be a fine stopgap measure while we define a forward-looking API that allows for setting the entityid directly.

@mgonzalez does your React Native app have a frontend? In an ideal world, could you use it like @spwoodcock wants to launch a form and set some values directly?

I'd be very interested in learning more about what you're up to. Consider writing an introduction when you have a moment. :blush:

Nice! That's a pretty neat approach.

Thanks for the heads up @LN :smile:

I think this would require us to develop a native app, however.
Or at least use a framework that compiles to a native app, such as Expo (javascript --> native), or Flutter (dart --> native).

To use a full web app with the approach suggested, I think we would need the ability to launch a Trusted Web Activity from an XLSForm field, which I'm not sure can be done.
(please correct me if I am wrong!)

Summary of how to achieve this (for future ref)

  1. Develop and app that compiles to native, with a module written in Kotlin that can both:
    a. Save a selected Entity ID somewhere.
    b. Receive an Android intent to return the saved Entity ID.

  2. Within the created app the user can select an Entity ID, then trigger opening an specific form in ODK Collect via intent: https://docs.getodk.org/collect-api/#using-a-uri-to-edit-a-form-or-instance

  3. The XLSForm in ODK Collect can be designed with a field containing the intent attribute: https://docs.getodk.org/collect-external-apps/#external-apps-to-populate-multiple-fields
    This attribute calls another Android app via intent. It can call the app described above, to return the saved Entity ID. The XLSForm field is populated with the Entity ID.

1 Like

Ah yes, of course, that's the whole essence of @mgonzalez's solution. :see_no_evil:

Thanks for the nice summary of the approach even though it's not going to work for you!

1 Like