Skip to content

Commit

Permalink
Add ODK integration using Android intents (#1252)
Browse files Browse the repository at this point in the history
* feat: init ODK integration via intent

Signed-off-by: Paolo Miguel de Leon <[email protected]>

* fix: sending data to ODK does not finish share flow properly

Signed-off-by: Paolo Miguel de Leon <[email protected]>

* feat: add error handlers to ODK module

Signed-off-by: Paolo Miguel de Leon <[email protected]>

* fix: building on Windows fails due duplicate libs from dependencies

Signed-off-by: Paolo Miguel de Leon <[email protected]>

* chore: cleanup logs

Signed-off-by: Paolo Miguel de Leon <[email protected]>

* refactor: move strings to const

Signed-off-by: Paolo Miguel de Leon <[email protected]>

---------

Signed-off-by: Paolo Miguel de Leon <[email protected]>
  • Loading branch information
pmigueld authored Feb 14, 2024
1 parent c37faf9 commit 38fb181
Show file tree
Hide file tree
Showing 11 changed files with 281 additions and 53 deletions.
13 changes: 13 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,19 @@ android {
ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion

// Execution failed for task ':app:mergeResidentappDebugNativeLibs'
// Build fails in Windows due to duplicate library paths from these two packages:
// - expo-modules-core
// - react-native-mmkv-storage
if (System.properties['os.name'].toLowerCase().contains('windows')) {
packagingOptions {
pickFirst 'lib/arm64-v8a/liblog.so'
pickFirst 'lib/armeabi-v7a/liblog.so'
pickFirst 'lib/x86/liblog.so'
pickFirst 'lib/x86_64/liblog.so'
}
}

ext {
APP_NAME= "@string/app_name"
}
Expand Down
85 changes: 50 additions & 35 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,46 +1,61 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.mosip.residentapp" xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.mosip.residentapp"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
<application tools:replace="usesCleartextTraffic" android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false" android:theme="@style/AppTheme" android:usesCleartextTraffic="false">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true"/>
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="48.0.0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL" android:value="https://exp.host/@anonymous/inji"/>
<activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|locale|layoutDirection" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
<application tools:replace="usesCleartextTraffic" android:name=".MainApplication"
android:label="@string/app_name" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="false"
android:theme="@style/AppTheme" android:usesCleartextTraffic="false">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="true" />
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="48.0.0" />
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH"
android:value="ALWAYS" />
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0" />
<meta-data android:name="expo.modules.updates.EXPO_UPDATE_URL"
android:value="https://exp.host/@anonymous/inji" />
<activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|locale|layoutDirection"
android:launchMode="singleTask" android:windowSoftInputMode="adjustResize"
android:theme="@style/Theme.App.SplashScreen" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="io.mosip.residentapp"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="io.mosip.residentapp" />
</intent-filter>
<intent-filter>
<action android:name="io.mosip.residentapp.odk.REQUEST" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new ODKIntentPackage());
return packages;
}

Expand All @@ -53,7 +53,7 @@ protected boolean isNewArchEnabled() {
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
}
});

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.mosip.residentapp;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;

public class ODKIntentModule extends ReactContextBaseJavaModule {

private static final String MODULE_NAME = "ODKIntentModule";

/**
* The action to check for sending data to ODK Collect. Use this when configuring the ODK form.
*/
private static final String ODK_REQUEST_ACTION = "io.mosip.residentapp.odk.REQUEST";

@Override
public String getName() {
return MODULE_NAME;
}

ODKIntentModule(ReactApplicationContext context) {
super(context);
}

@ReactMethod
public void isRequestIntent(Promise promise) {
try {
Activity activity = getCurrentActivity();
Intent intent = activity.getIntent();
String action = intent.getAction();
if (activity == null || intent == null || action == null) {
promise.resolve(false);
return;
}

promise.resolve(action.equals(ODK_REQUEST_ACTION));

} catch (Exception e) {
promise.reject("E_UNKNOWN", e.getMessage());
}
}

@ReactMethod
public void sendBundleResult(ReadableMap vcData) {
try {
Activity activity = getCurrentActivity();
if (activity == null) {
throw new Exception("Activity does not exist");
}

Intent result = new Intent();
result.setPackage(activity.getPackageName());

Bundle vcBundle = new Bundle(Arguments.toBundle(vcData));
if (vcBundle == null) {
throw new Exception("Bundle could not be created");
}

for (String key : vcBundle.keySet()) {
result.putExtra(key, vcBundle.getString(key));
}

activity.setResult(Activity.RESULT_OK, result);
activity.finish();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.mosip.residentapp;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ODKIntentPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();

modules.add(new ODKIntentModule(reactContext));

return modules;
}
}
22 changes: 22 additions & 0 deletions lib/react-native-odk-intent/ODKIntentModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {NativeModules} from 'react-native';

const {ODKIntentModule} = NativeModules;

export enum ODKIntentVcField {
UIN = 'uin',
FullName = 'full_name',
DateOfBirth = 'date_of_birth',
Email = 'email',
Phone = 'phone',
Biometrics = 'biometrics',
Issuer = 'issuer',
IssuanceDate = 'issuance_date',
}

export type ODKIntentVcData = Partial<Record<ODKIntentVcField, string>>;
interface ODKIntentInterface {
isRequestIntent: () => Promise<boolean>;
sendBundleResult: (vcData: ODKIntentVcData) => void;
}

export default ODKIntentModule as ODKIntentInterface;
32 changes: 30 additions & 2 deletions machines/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@ import {
createBackupRestoreMachine,
} from './backupRestore';

import ODKIntentModule from '../lib/react-native-odk-intent/ODKIntentModule';

const model = createModel(
{
info: {} as AppInfo,
serviceRefs: {} as AppServices,
isReadError: false,
isDecryptError: false,
isKeyInvalidateError: false,
isRequestIntent: false,
},
{
events: {
Expand Down Expand Up @@ -69,6 +72,9 @@ export const appMachine = model.createMachine(
schema: {
context: model.initialContext,
events: {} as EventFrom<typeof model>,
services: {} as {
checkIntent: {data: boolean};
},
},
id: 'app',
initial: 'init',
Expand All @@ -90,8 +96,17 @@ export const appMachine = model.createMachine(
},
states: {
init: {
initial: 'store',
initial: 'intent',
states: {
intent: {
invoke: {
src: 'checkIntent',
onDone: {
target: 'store',
actions: 'setIntent',
},
},
},
store: {
entry: ['spawnStoreActor', 'logStoreEvents'],
on: {
Expand Down Expand Up @@ -196,6 +211,10 @@ export const appMachine = model.createMachine(
},
{
actions: {
setIntent: assign({
isRequestIntent: (_context, event) => event.data,
}),

forwardToServices: pure((context, event) =>
Object.values(context.serviceRefs).map(serviceRef =>
send({...event, type: `APP_${event.type}`}, {to: serviceRef}),
Expand Down Expand Up @@ -287,7 +306,7 @@ export const appMachine = model.createMachine(

if (isAndroid()) {
serviceRefs.request = spawn(
createRequestMachine(serviceRefs),
createRequestMachine(serviceRefs, context.isRequestIntent),
requestMachine.id,
);
}
Expand Down Expand Up @@ -352,6 +371,10 @@ export const appMachine = model.createMachine(
},

services: {
checkIntent: () => {
return ODKIntentModule.isRequestIntent();
},

getAppInfo: () => async callback => {
const appInfo = {
deviceId: getDeviceId(),
Expand Down Expand Up @@ -416,6 +439,11 @@ type State = StateFrom<typeof appMachine>;
export function selectAppInfo(state: State) {
return state.context.info;
}

export function selectIsRequestIntent(state: State) {
return state.context.isRequestIntent;
}

export function selectIsReady(state: State) {
return state.matches('ready');
}
Expand Down
Loading

0 comments on commit 38fb181

Please sign in to comment.