diff --git a/app/assets/locales/android_translatable_strings.txt b/app/assets/locales/android_translatable_strings.txt index 9a41195833..80abdeeb0e 100644 --- a/app/assets/locales/android_translatable_strings.txt +++ b/app/assets/locales/android_translatable_strings.txt @@ -867,6 +867,9 @@ intent.callout.unable.to.process=Unable to process callout result intent.callout.activity.missing=Couldn't find intent for callout! intent.callout.not.supported=This intent callout is not supported on this device fingerprints.scanned=Fingerprints scanned: ${0} +intent.callout.biometrics.capture.result.success=All data stored successfully +intent.callout.biometrics.capture.result.fail=No data was stored +intent.callout.biometrics.capture.result.partialfail=Failed to store some data settings.developer.options=Developer Options settings.developer.title=Developer Options diff --git a/app/src/org/commcare/provider/IdentityCalloutHandler.java b/app/src/org/commcare/provider/IdentityCalloutHandler.java index 3ec7d0359c..a460aa004d 100644 --- a/app/src/org/commcare/provider/IdentityCalloutHandler.java +++ b/app/src/org/commcare/provider/IdentityCalloutHandler.java @@ -1,8 +1,10 @@ package org.commcare.provider; import android.content.Intent; +import android.util.Base64; import org.commcare.android.javarosa.IntentCallout; +import org.commcare.commcaresupportlibrary.identity.BiometricIdentifier; import org.commcare.commcaresupportlibrary.identity.IdentityResponseBuilder; import org.commcare.commcaresupportlibrary.identity.model.IdentificationMatch; import org.commcare.commcaresupportlibrary.identity.model.MatchResult; @@ -23,6 +25,7 @@ import java.util.Collections; import java.util.Hashtable; import java.util.List; +import java.util.Map; import java.util.Vector; import androidx.annotation.StringDef; @@ -37,13 +40,11 @@ public class IdentityCalloutHandler { public static final String GENERALIZED_IDENTITY_PROVIDER = "generalized_identity_provider"; - @StringDef({GENERALIZED_IDENTITY_PROVIDER, SimprintsCalloutProcessing.SIMPRINTS_IDENTITY_PROVIDER}) @Retention(RetentionPolicy.SOURCE) public @interface IdentityProvider { } - public static boolean isIdentityCalloutResponse(Intent intent) { return isRegistrationResponse(intent) || isVerificationResponse(intent) || isIdentificationResponse(intent); } @@ -109,8 +110,34 @@ private static boolean processRegistrationReponse(FormDef formDef, Hashtable> responseToRefMap) { RegistrationResult registrationResult = intent.getParcelableExtra(IdentityResponseBuilder.REGISTRATION); String guid = registrationResult.getGuid(); + storeValueFromCalloutInForm(formDef, responseToRefMap, intentQuestionRef, REF_GUID, guid); - IntentCallout.setNodeValue(formDef, intentQuestionRef, guid); + + int numOfTemplatesStored = 0; + int numOfTemplates = 0; + for (Map.Entry template : registrationResult.getTemplates().entrySet()) { + boolean success = storeValueFromCalloutInForm(formDef, responseToRefMap, + intentQuestionRef, + template.getKey().getCalloutResponseKey(), + Base64.encodeToString(template.getValue(), Base64.DEFAULT)); + if (success) { + numOfTemplatesStored++; + } + numOfTemplates++; + } + + String result = ""; + if (registrationResult.getTemplates().isEmpty() || (numOfTemplates == numOfTemplatesStored)) { + result = Localization.get("intent.callout.biometrics.capture.result.success"); + } else if (numOfTemplates > 0) { + if (numOfTemplatesStored == 0) { + result = Localization.get("intent.callout.biometrics.capture.result.fail"); + } else { + result = Localization.get("intent.callout.biometrics.capture.result.partialfail"); + } + } + + IntentCallout.setNodeValue(formDef, intentQuestionRef, result); // Empty out any references present for duplicate handling storeValueFromCalloutInForm(formDef, responseToRefMap, intentQuestionRef, REF_MATCH_GUID, ""); diff --git a/app/unit-tests/resources/commcare-apps/identity_callouts/default/app_strings.txt b/app/unit-tests/resources/commcare-apps/identity_callouts/default/app_strings.txt index 90164a5a0f..89742f0f58 100644 --- a/app/unit-tests/resources/commcare-apps/identity_callouts/default/app_strings.txt +++ b/app/unit-tests/resources/commcare-apps/identity_callouts/default/app_strings.txt @@ -10,8 +10,9 @@ case_autoload.raw.case_missing=Unable to find case referenced by auto-select cas case_autoload.raw.property_missing=The custom xpath expression specified for case auto-selecting could not be found: ${0} case_autoload.user.case_missing=Unable to find case referenced by auto-select case ID. case_autoload.user.property_missing=The user data key specified for case auto-selecting could not be found: ${0} -case_autoload.usercase.case_missing=Unable to find case referenced by auto-select case ID. +case_autoload.usercase.case_missing=This form affects the user case, but no user case id was found. Please contact your supervisor. case_autoload.usercase.property_missing=The user case specified for case auto-selecting could not be found: ${0} +case_search.claimed_case.case_missing=Unable to find the selected case after performing a sync. Please try again. case_sharing.exactly_one_group=The case sharing settings for your user are incorrect. This user must be in exactly one case sharing group. Please contact your supervisor. cchq.case=Case cchq.referral=Referral @@ -23,9 +24,9 @@ cchq.report_name_header=Report Name cchq.reports_last_updated_on=Reports last updated on en=English forms.m0f0=Registration Form +forms.m0f1=Registration with Templates homescreen.title=Identity Integration Test lang.current=en m0.case_long.case_name_1.header=Name m0.case_short.case_name_1.header=Name modules.m0=Case List -usercase.missing_id=This form affects the user case, but no user case id was found. Please contact your supervisor. diff --git a/app/unit-tests/resources/commcare-apps/identity_callouts/en/app_strings.txt b/app/unit-tests/resources/commcare-apps/identity_callouts/en/app_strings.txt index 42339f89e5..243b00ab79 100644 --- a/app/unit-tests/resources/commcare-apps/identity_callouts/en/app_strings.txt +++ b/app/unit-tests/resources/commcare-apps/identity_callouts/en/app_strings.txt @@ -8,6 +8,7 @@ cchq.report_menu=Reports cchq.report_name_header=Report Name cchq.reports_last_updated_on=Reports last updated on forms.m0f0=Registration Form +forms.m0f1=Registration with Templates homescreen.title=Identity Integration Test lang.current=en m0.case_long.case_name_1.header=Name diff --git a/app/unit-tests/resources/commcare-apps/identity_callouts/media_suite.xml b/app/unit-tests/resources/commcare-apps/identity_callouts/media_suite.xml index 22824b89c8..89b829b6ea 100644 --- a/app/unit-tests/resources/commcare-apps/identity_callouts/media_suite.xml +++ b/app/unit-tests/resources/commcare-apps/identity_callouts/media_suite.xml @@ -1,2 +1,2 @@ - + diff --git a/app/unit-tests/resources/commcare-apps/identity_callouts/modules-0/forms-0.xml b/app/unit-tests/resources/commcare-apps/identity_callouts/modules-0/forms-0.xml index 29de4d811f..5f3d0618e2 100644 --- a/app/unit-tests/resources/commcare-apps/identity_callouts/modules-0/forms-0.xml +++ b/app/unit-tests/resources/commcare-apps/identity_callouts/modules-0/forms-0.xml @@ -1,58 +1,91 @@ - - Registration Form - - - - - - - - - - - - - case - - - - - - - - - - - - - - Registration - - - Verification - - - - - - - - - - - - - - - - - - - - + + Registration Form + + + + + + + + + + + + + + + + + case + + + + + + + + + + + + + + + + + + + + + + + + + + + + Register + + + Verification + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/unit-tests/resources/commcare-apps/identity_callouts/modules-0/forms-1.xml b/app/unit-tests/resources/commcare-apps/identity_callouts/modules-0/forms-1.xml new file mode 100644 index 0000000000..03050af4c6 --- /dev/null +++ b/app/unit-tests/resources/commcare-apps/identity_callouts/modules-0/forms-1.xml @@ -0,0 +1,69 @@ + + + Registration with Templates + + + + + + + + + + + + case + + + + + + + + + + + + + + + + + + + + + + + Register + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/unit-tests/resources/commcare-apps/identity_callouts/profile.ccpr b/app/unit-tests/resources/commcare-apps/identity_callouts/profile.ccpr index fb75ce8dd8..01a2868eed 100644 --- a/app/unit-tests/resources/commcare-apps/identity_callouts/profile.ccpr +++ b/app/unit-tests/resources/commcare-apps/identity_callouts/profile.ccpr @@ -1,6 +1,6 @@ - - - - - + + + + - + @@ -29,17 +29,11 @@ - - - - - - - + @@ -55,20 +49,21 @@ + - + ./suite.xml - https://www.commcarehq.org/a/shubhamgoyaltest/apps/download/844a399d13204c2792e4f81ae878eb7e/suite.xml + https://www.commcarehq.org/a/shubhamgoyaltest/apps/download/4908aeeaceec4c0e8357028418db78f1/suite.xml - + ./media_suite.xml - https://www.commcarehq.org/a/shubhamgoyaltest/apps/download/844a399d13204c2792e4f81ae878eb7e/media_suite.xml + https://www.commcarehq.org/a/shubhamgoyaltest/apps/download/4908aeeaceec4c0e8357028418db78f1/media_suite.xml diff --git a/app/unit-tests/resources/commcare-apps/identity_callouts/suite.xml b/app/unit-tests/resources/commcare-apps/identity_callouts/suite.xml index 3206001933..5b8b28ddeb 100644 --- a/app/unit-tests/resources/commcare-apps/identity_callouts/suite.xml +++ b/app/unit-tests/resources/commcare-apps/identity_callouts/suite.xml @@ -1,19 +1,25 @@ - + - + ./modules-0/forms-0.xml ./modules-0/forms-0.xml + + + ./modules-0/forms-1.xml + ./modules-0/forms-1.xml + + - + ./default/app_strings.txt ./default/app_strings.txt - + ./en/app_strings.txt ./en/app_strings.txt @@ -72,10 +78,22 @@ + +
http://openrosa.org/formdesigner/E3900AF4-43AC-435A-B0BB-E043AD9608A1
+ + + + + + + + +
+
diff --git a/app/unit-tests/src/org/commcare/android/tests/formentry/IdentityCalloutTests.kt b/app/unit-tests/src/org/commcare/android/tests/formentry/IdentityCalloutTests.kt index 3258b001ca..954475a607 100644 --- a/app/unit-tests/src/org/commcare/android/tests/formentry/IdentityCalloutTests.kt +++ b/app/unit-tests/src/org/commcare/android/tests/formentry/IdentityCalloutTests.kt @@ -3,6 +3,7 @@ package org.commcare.android.tests.formentry import android.app.Activity import android.app.Instrumentation import android.content.Intent +import android.util.Base64 import android.widget.Button import android.widget.ImageButton import androidx.test.espresso.intent.Intents @@ -16,6 +17,7 @@ import org.commcare.android.resource.installers.XFormAndroidInstaller import org.commcare.android.util.ActivityLaunchUtils import org.commcare.android.util.TestAppInstaller import org.commcare.android.util.TestUtils +import org.commcare.commcaresupportlibrary.identity.BiometricIdentifier import org.commcare.commcaresupportlibrary.identity.IdentityResponseBuilder import org.commcare.commcaresupportlibrary.identity.model.IdentificationMatch import org.commcare.commcaresupportlibrary.identity.model.MatchResult @@ -23,6 +25,7 @@ import org.commcare.commcaresupportlibrary.identity.model.MatchStrength import org.commcare.dalvik.R import org.commcare.provider.IdentityCalloutHandler import org.junit.After +import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Rule @@ -39,6 +42,7 @@ class IdentityCalloutTests { @JvmField var intentsRule = ActivityScenarioRule(FormEntryActivity::class.java) + @Before fun setup() { Intents.init() @@ -96,6 +100,33 @@ class IdentityCalloutTests { assertEquals(guidToConfidenceMap.elementAt(2), "★★★") } + @Test + fun testRegistrationWithTemplates() { + val formEntryActivity = ActivityLaunchUtils.launchFormEntry("m0-f1") + + var templates: HashMap = HashMap(2) + templates[BiometricIdentifier.LEFT_INDEX_FINGER] = + byteArrayOf(0, 0, -21, -67, 0, -64, 25, 62, -69, -124, -91, 29, -50, -107, 58) + templates[BiometricIdentifier.LEFT_MIDDLE_FINGER] = + byteArrayOf(122, -91, 114, 62, 107, -95, -69, 28, 110, 123, 72, 71, -86, -117, 126) + + // get Identity response and save Base64 encoded template to form question + intendRegistrationWithTemplatesIntent(templates) + performIntentCallout(formEntryActivity) + TestUtils.assertFormValue("/data/guid", "test-case-unique-guid") + for ((key, value) in templates) { + TestUtils.assertFormValue("/data/" + key.calloutResponseKey, + Base64.encodeToString(value, Base64.DEFAULT)) + } + + // retrieve Base64 encoded template from form question, decode and compare with initial + // value + for ((key, value) in templates) { + var base64EncodedTemplate = TestUtils.getFormValue("/data/" + key.calloutResponseKey) as String; + assertArrayEquals(Base64.decode(base64EncodedTemplate, Base64.DEFAULT), value) + } + } + private fun getIdentificationIntent(): Intent { val identifications = ArrayList() identifications.add(IdentificationMatch("guid-1", MatchResult(80, MatchStrength.FOUR_STARS))) @@ -120,6 +151,14 @@ class IdentityCalloutTests { intending(hasAction("org.commcare.identity.bioenroll")).respondWith(result) } + private fun intendRegistrationWithTemplatesIntent(templates: HashMap) { + val registration = IdentityResponseBuilder + .registrationResponse("test-case-unique-guid", templates) + .build() + val result = Instrumentation.ActivityResult(Activity.RESULT_OK, registration) + intending(hasAction("org.commcare.identity.bioenroll")).respondWith(result) + } + private fun intendDuplicatesDuringRegistration() { val result = Instrumentation.ActivityResult(Activity.RESULT_OK, getIdentificationIntent()) intending(hasAction("org.commcare.identity.bioenroll")).respondWith(result) diff --git a/app/unit-tests/src/org/commcare/android/util/TestUtils.java b/app/unit-tests/src/org/commcare/android/util/TestUtils.java index 01e7c0712a..86c04d4af6 100644 --- a/app/unit-tests/src/org/commcare/android/util/TestUtils.java +++ b/app/unit-tests/src/org/commcare/android/util/TestUtils.java @@ -47,6 +47,7 @@ import org.javarosa.test_utils.ExprEvalUtils; import org.javarosa.xml.util.InvalidStructureException; import org.javarosa.xml.util.UnfullfilledRequirementsException; +import org.javarosa.xpath.parser.XPathSyntaxException; import org.robolectric.RuntimeEnvironment; import org.xmlpull.v1.XmlPullParserException; @@ -362,7 +363,7 @@ private static EvaluationContext buildContextWithInstances(UserSandbox sandbox, return new EvaluationContext(null, instances); } - public static void assertFormValue(String expr, Object expectedValue){ + public static void assertFormValue(String expr, Object expectedValue) { FormDef formDef = FormEntryActivity.mFormController.getFormEntryController().getModel().getForm(); FormInstance instance = formDef.getMainInstance(); @@ -370,4 +371,11 @@ public static void assertFormValue(String expr, Object expectedValue){ errorMsg = ExprEvalUtils.expectedEval(expr, instance, null, expectedValue, null); assertTrue(errorMsg, "".equals(errorMsg)); } + + public static Object getFormValue(String expr) throws XPathSyntaxException { + FormDef formDef = FormEntryActivity.mFormController.getFormEntryController().getModel().getForm(); + FormInstance instance = formDef.getMainInstance(); + EvaluationContext ec = new EvaluationContext(instance); + return ExprEvalUtils.xpathEval(ec, expr); + } } diff --git a/commcare-support-library/build.gradle b/commcare-support-library/build.gradle index 4c6a6a4c79..dfb24782f6 100644 --- a/commcare-support-library/build.gradle +++ b/commcare-support-library/build.gradle @@ -57,6 +57,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' diff --git a/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/BiometricIdentifier.java b/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/BiometricIdentifier.java new file mode 100644 index 0000000000..637c6d599c --- /dev/null +++ b/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/BiometricIdentifier.java @@ -0,0 +1,25 @@ +package org.commcare.commcaresupportlibrary.identity; + +public enum BiometricIdentifier { + RIGHT_THUMB("right_thumb_template"), + RIGHT_INDEX_FINGER("right_index_finger_template"), + RIGHT_MIDDLE_FINGER("right_middle_finger_template"), + RIGHT_RING_FINGER("right_ring_finger_template"), + RIGHT_PINKY_FINGER("right_pinky_finger_template"), + LEFT_THUMB("left_thumb_template"), + LEFT_INDEX_FINGER("left_index_finger_template"), + LEFT_MIDDLE_FINGER("left_middle_finger_template"), + LEFT_RING_FINGER("left_ring_finger_template"), + LEFT_PINKY_FINGER("left_pinky_finger_template"), + FACE("face_template"); + + private final String calloutResponseKey; + + BiometricIdentifier(String calloutResponseKey) { + this.calloutResponseKey = calloutResponseKey; + } + + public String getCalloutResponseKey() { + return calloutResponseKey; + } +} diff --git a/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/IdentityResponseBuilder.java b/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/IdentityResponseBuilder.java index a69f758bc4..e8cfd628c2 100644 --- a/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/IdentityResponseBuilder.java +++ b/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/IdentityResponseBuilder.java @@ -9,6 +9,7 @@ import org.commcare.commcaresupportlibrary.identity.model.VerificationMatch; import java.util.ArrayList; +import java.util.Map; import static android.app.Activity.RESULT_OK; @@ -41,6 +42,18 @@ public static IdentityResponseBuilder registrationResponse(String guid) { return new IdentityResponseBuilder(intent); } + /** + * Creates response for result of a new Identity Registration with biometric templates + * + * @param templates data captured as part of the new registration in the Identity Provider + * @return IdentityResponseBuilder for a registration workflow response + */ + public static IdentityResponseBuilder registrationResponse(String guid, Map templates) { + Intent intent = new Intent(); + intent.putExtra(REGISTRATION, new RegistrationResult(guid, templates)); + return new IdentityResponseBuilder(intent); + } + /** * Creates response for result of a identification workflow * diff --git a/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/identity_integration.md b/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/identity_integration.md index ea34dd3ec9..51b48638cb 100644 --- a/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/identity_integration.md +++ b/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/identity_integration.md @@ -20,7 +20,17 @@ Often you will need this generated guid to be passed back to CommCare so that it IdentityResponseBuilder.registrationResponse(guid) .finalizeResponse(activity) ```` - +Alternatively, in case the biometric templates are to be stored in CommCare, use the following instead: +```` +IdentityResponseBuilder.registrationResponse(guid, templates) + .finalizeResponse(activity) +```` +* `templates` is a [`Map`](https://docs.oracle.com/javase/8/docs/api/java/util/Map.html) containing all the biometric templates and whose _keys_ are [`BiometricIdentifier`](BiometricIdentifier.java) elements and _values_ are the actual biometric templates in the form of a byte array. See an example below in `kotlin`: +```` + var templates: HashMap = HashMap(2) + templates[BiometricIdentifier.LEFT_INDEX_FINGER] = byteArrayOf(0, 0, -21, -67, 0, -64, 25, 62, -69, -124, -91, 29, -50, -107, 58) + templates[BiometricIdentifier.LEFT_MIDDLE_FINGER] = byteArrayOf(122, -91, 114, 62, 107, -95, -69, 28, 110, 123, 72, 71, -86, -117, 126) +```` This creates an appropriate resulting Intent for the Identity registration workflow and finish your activity after setting the response as a result to returning intent. diff --git a/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/model/RegistrationResult.java b/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/model/RegistrationResult.java index fe16353e6c..11e4b2e135 100644 --- a/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/model/RegistrationResult.java +++ b/commcare-support-library/src/main/java/org/commcare/commcaresupportlibrary/identity/model/RegistrationResult.java @@ -3,10 +3,16 @@ import android.os.Parcel; import android.os.Parcelable; +import org.commcare.commcaresupportlibrary.identity.BiometricIdentifier; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + @SuppressWarnings("unused") public class RegistrationResult implements Parcelable { - private String guid; + private Map templates; /** * Result of the identity enrollment workflow @@ -15,10 +21,25 @@ public class RegistrationResult implements Parcelable { */ public RegistrationResult(String guid) { this.guid = guid; + this.templates = new HashMap<>(0); + } + + public RegistrationResult(String guid, Map templates) { + this.guid = guid; + this.templates = templates; } protected RegistrationResult(Parcel in) { guid = in.readString(); + int numTemplates = in.readInt(); + templates = new HashMap<>(numTemplates); + for (int i=0;i < numTemplates; i++){ + BiometricIdentifier biometricIdentifier = BiometricIdentifier.values()[in.readInt()]; + int templateSize = in.readInt(); + byte[] template = new byte[templateSize]; + in.readByteArray(template); + templates.put(biometricIdentifier, template); + } } public static final Creator CREATOR = new Creator() { @@ -41,12 +62,26 @@ public int describeContents() { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(guid); + dest.writeInt(getNumberOfTemplates()); + for (Map.Entry template : templates.entrySet()){ + dest.writeInt(template.getKey().ordinal()); + dest.writeInt(template.getValue().length); + dest.writeByteArray(template.getValue()); + } } public String getGuid() { return guid; } + public Map getTemplates() { + return templates; + } + + public int getNumberOfTemplates() { + return templates.size(); + } + @Override public boolean equals(Object o) { if (o == this) { @@ -59,12 +94,25 @@ public boolean equals(Object o) { if (!guid.equals(other.guid)) { return false; } + if (getNumberOfTemplates() != other.getNumberOfTemplates()){ + return false; + } + + for (Map.Entry template : templates.entrySet()){ + byte[] otherTemplate = other.getTemplates().get(template.getKey()); + if (!Arrays.equals(template.getValue(), otherTemplate)) { + return false; + } + } return true; } @Override public int hashCode() { int hash = guid.hashCode(); + for (Map.Entry template : templates.entrySet()){ + hash += Arrays.hashCode(template.getValue()); + } return hash; } }