keyToTitleMap = new HashMap<>();
+ private static final String CONNECT_ID_ENABLED = "connect_id-enabled";
+
static {
keyToTitleMap.put(ENABLE_PRIVILEGE, "menu.enable.privileges");
@@ -72,4 +73,16 @@ public static void setDeveloperPreferencesEnabled(boolean enabled) {
public static boolean isDeveloperPreferencesEnabled() {
return GlobalPrivilegesManager.getGlobalPrefsRecord().getBoolean(DEVELOPER_PREFERENCES_ENABLED, false);
}
+
+ public static void setConnectIdEnabled(boolean enabled) {
+ GlobalPrivilegesManager.getGlobalPrefsRecord()
+ .edit()
+ .putBoolean(CONNECT_ID_ENABLED, enabled)
+ .apply();
+ }
+
+ public static boolean isConnectIdEnabled() {
+ //NOTE: Setting default case to true for initial user testing, but production should keep the default false
+ return GlobalPrivilegesManager.getGlobalPrefsRecord().getBoolean(CONNECT_ID_ENABLED, false);
+ }
}
diff --git a/app/src/org/commcare/utils/BiometricsHelper.java b/app/src/org/commcare/utils/BiometricsHelper.java
new file mode 100644
index 0000000000..e448369ff1
--- /dev/null
+++ b/app/src/org/commcare/utils/BiometricsHelper.java
@@ -0,0 +1,230 @@
+package org.commcare.utils;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.provider.Settings;
+
+import com.google.zxing.integration.android.IntentIntegrator;
+
+import org.commcare.connect.ConnectConstants;
+import org.commcare.dalvik.R;
+
+import androidx.biometric.BiometricManager;
+import androidx.biometric.BiometricPrompt;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * Helper class for biometric configuration and verification.
+ * Provides methods to check biometric availability, configure biometrics,
+ * and perform authentication using fingerprint or PIN or Password.
+ * Supports both biometric strong authentication and device credentials.
+ */
+public class BiometricsHelper {
+
+ /**
+ * Enum representing the availability and configuration status of a biometric method.
+ */
+ public enum ConfigurationStatus {
+ NotAvailable, // Biometrics not available on the device
+ NotConfigured, // Biometrics available but not set up
+ Configured // Biometrics set up and ready for authentication
+ }
+
+ /**
+ * Checks the fingerprint authentication status.
+ *
+ * @param context The application context.
+ * @param biometricManager The BiometricManager instance.
+ * @return The fingerprint configuration status.
+ */
+ public static ConfigurationStatus checkFingerprintStatus(Context context, BiometricManager biometricManager) {
+ return checkStatus(context, biometricManager, BiometricManager.Authenticators.BIOMETRIC_STRONG);
+ }
+
+ /**
+ * Determines if fingerprint authentication is configured on the device.
+ *
+ * @param context The application context.
+ * @param biometricManager The BiometricManager instance.
+ * @return True if fingerprint authentication is configured, false otherwise.
+ */
+ public static boolean isFingerprintConfigured(Context context, BiometricManager biometricManager) {
+ return checkStatus(context, biometricManager, BiometricManager.Authenticators.BIOMETRIC_STRONG) == ConfigurationStatus.Configured;
+ }
+
+ /**
+ * Prompts the user to configure fingerprint authentication.
+ *
+ * @param activity The current activity.
+ * @return True if the configuration process starts successfully, false otherwise.
+ */
+ public static boolean configureFingerprint(Activity activity) {
+ return configureBiometric(activity, BiometricManager.Authenticators.BIOMETRIC_STRONG);
+ }
+
+ /**
+ * Initiates fingerprint authentication.
+ *
+ * @param activity The fragment activity.
+ * @param biometricManager The BiometricManager instance.
+ * @param allowExtraOptions Whether to allow alternative authentication options (e.g., PIN).
+ * @param callback The callback for authentication results.
+ */
+ public static void authenticateFingerprint(FragmentActivity activity,
+ BiometricManager biometricManager,
+ boolean allowExtraOptions,
+ BiometricPrompt.AuthenticationCallback callback) {
+ if (BiometricsHelper.isFingerprintConfigured(activity, biometricManager)) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+ // For Android 11+ (R), use PIN as an alternative unlock method
+ authenticatePin(activity, biometricManager, callback);
+ } else {
+ BiometricPrompt prompt = new BiometricPrompt(activity,
+ ContextCompat.getMainExecutor(activity),
+ callback);
+
+ BiometricPrompt.PromptInfo.Builder builder = new BiometricPrompt.PromptInfo.Builder()
+ .setTitle(activity.getString(R.string.connect_unlock_fingerprint_title))
+ .setSubtitle(activity.getString(R.string.connect_unlock_fingerprint_message))
+ .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
+
+ if (allowExtraOptions) {
+ builder.setNegativeButtonText(activity.getString(R.string.connect_unlock_other_options));
+ }
+
+ prompt.authenticate(builder.build());
+ }
+ }
+ }
+
+ /**
+ * Checks the status of PIN-based authentication.
+ *
+ * @param context The application context.
+ * @param biometricManager The BiometricManager instance.
+ * @return The PIN configuration status.
+ */
+ public static ConfigurationStatus checkPinStatus(Context context, BiometricManager biometricManager) {
+ int authStatus = canAuthenticate(context, biometricManager, BiometricManager.Authenticators.DEVICE_CREDENTIAL);
+
+ if (authStatus == BiometricManager.BIOMETRIC_SUCCESS) {
+ return ConfigurationStatus.Configured;
+ } else {
+ return ConfigurationStatus.NotConfigured;
+ }
+ }
+
+ /**
+ * Determines if PIN authentication is configured on the device.
+ *
+ * @param context The application context.
+ * @param biometricManager The BiometricManager instance.
+ * @return True if PIN authentication is configured, false otherwise.
+ */
+ public static boolean isPinConfigured(Context context, BiometricManager biometricManager) {
+ return checkStatus(context, biometricManager, BiometricManager.Authenticators.DEVICE_CREDENTIAL) == ConfigurationStatus.Configured;
+ }
+
+ /**
+ * Prompts the user to configure PIN-based authentication.
+ *
+ * @param activity The current activity.
+ * @return True if the configuration process starts successfully, false otherwise.
+ */
+ public static boolean configurePin(Activity activity) {
+ return configureBiometric(activity, BiometricManager.Authenticators.DEVICE_CREDENTIAL);
+ }
+
+ private static BiometricPrompt.AuthenticationCallback biometricPromptCallbackHolder;
+
+ /**
+ * Initiates PIN-based authentication.
+ *
+ * @param activity The fragment activity.
+ * @param biometricManager The BiometricManager instance.
+ * @param biometricPromptCallback The callback for authentication results.
+ */
+ public static void authenticatePin(FragmentActivity activity, BiometricManager biometricManager,
+ BiometricPrompt.AuthenticationCallback biometricPromptCallback) {
+ if (BiometricsHelper.isPinConfigured(activity, biometricManager)) {
+ biometricPromptCallbackHolder = biometricPromptCallback;
+ KeyguardManager manager = (KeyguardManager)activity.getSystemService(Context.KEYGUARD_SERVICE);
+ activity.startActivityForResult(
+ manager.createConfirmDeviceCredentialIntent(
+ activity.getString(R.string.connect_unlock_title),
+ activity.getString(R.string.connect_unlock_message)),
+ ConnectConstants.CONNECT_UNLOCK_PIN);
+ }
+ }
+
+ /**
+ * Handles the result of the PIN authentication activity.
+ *
+ * @param requestCode The request code for the authentication intent.
+ * @param resultCode The result code from the authentication activity.
+ * @return True if the request was handled, false otherwise.
+ */
+ public static boolean handlePinUnlockActivityResult(int requestCode, int resultCode) {
+ if (requestCode == ConnectConstants.CONNECT_UNLOCK_PIN) {
+ if (resultCode == Activity.RESULT_OK) {
+ biometricPromptCallbackHolder.onAuthenticationSucceeded(null);
+ } else {
+ biometricPromptCallbackHolder.onAuthenticationFailed();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Checks the biometric authentication status for a specific authentication method.
+ *
+ * @param context The application context.
+ * @param biometricManager The BiometricManager instance.
+ * @param authenticator The authenticator type (e.g., fingerprint, PIN).
+ * @return The biometric configuration status.
+ */
+ public static ConfigurationStatus checkStatus(Context context, BiometricManager biometricManager,
+ int authenticator) {
+ int val = canAuthenticate(context, biometricManager, authenticator);
+ switch (val) {
+ case BiometricManager.BIOMETRIC_SUCCESS -> {
+ return ConfigurationStatus.Configured;
+ }
+ case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
+ return ConfigurationStatus.NotConfigured;
+ }
+ }
+ return ConfigurationStatus.NotAvailable;
+ }
+
+ private static int canAuthenticate(Context context, BiometricManager biometricManager, int authenticator) {
+ if (authenticator == BiometricManager.Authenticators.DEVICE_CREDENTIAL && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+ KeyguardManager manager = (KeyguardManager)context.getSystemService(Context.KEYGUARD_SERVICE);
+ boolean isSecure = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ?
+ manager.isDeviceSecure() : manager.isKeyguardSecure();
+
+ return isSecure ? BiometricManager.BIOMETRIC_SUCCESS : BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED;
+ }
+ return biometricManager.canAuthenticate(authenticator);
+ }
+
+ private static boolean configureBiometric(Activity activity, int authenticator) {
+ Intent enrollIntent;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ enrollIntent = new Intent(Settings.ACTION_BIOMETRIC_ENROLL);
+ enrollIntent.putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, authenticator);
+ } else if (authenticator == BiometricManager.Authenticators.BIOMETRIC_STRONG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ enrollIntent = new Intent(Settings.ACTION_FINGERPRINT_ENROLL);
+ } else {
+ return false;
+ }
+
+ activity.startActivityForResult(enrollIntent, IntentIntegrator.REQUEST_CODE);
+ return true;
+ }
+}
diff --git a/app/src/org/commcare/utils/CrashUtil.java b/app/src/org/commcare/utils/CrashUtil.java
index 2a2bc9cbc2..a08103743d 100644
--- a/app/src/org/commcare/utils/CrashUtil.java
+++ b/app/src/org/commcare/utils/CrashUtil.java
@@ -2,12 +2,14 @@
import org.commcare.CommCareApplication;
import org.commcare.android.logging.ReportingUtils;
+import org.commcare.connect.ConnectIDManager;
import org.commcare.dalvik.BuildConfig;
+
import com.google.firebase.crashlytics.FirebaseCrashlytics;
/**
* Contains constants and methods used in Crashlytics reporting.
- *
+ *
* Created by shubham on 8/09/17.
*/
public class CrashUtil {
@@ -16,6 +18,7 @@ public class CrashUtil {
private static final String APP_NAME = "app_name";
private static final String DOMAIN = "domain";
private static final String DEVICE_ID = "device_id";
+ private static final String CCC_USER = "ccc_user_id";
private static boolean crashlyticsEnabled = BuildConfig.USE_CRASHLYTICS;
@@ -53,4 +56,24 @@ public static void log(String message) {
FirebaseCrashlytics.getInstance().log(message);
}
}
+
+ /**
+ * Registers the current Connect user with Firebase Crashlytics for error tracking.
+ *
+ * If Crashlytics is enabled and a Connect ID is configured, this method retrieves
+ * the user ID from ConnectManager and sets it as a custom key in Crashlytics.
+ *
+ * In case of any exceptions during this process, the exception is recorded in
+ * Crashlytics to aid debugging.
+ */
+ public static void registerConnectUser() {
+ if (crashlyticsEnabled && ConnectIDManager.isLoggedIN()) {
+ try {
+ String userId = ConnectIDManager.getInstance().getUser(CommCareApplication.instance()).getUserId();
+ FirebaseCrashlytics.getInstance().setCustomKey(CCC_USER, userId);
+ } catch (Exception e) {
+ FirebaseCrashlytics.getInstance().recordException(e);
+ }
+ }
+ }
}
diff --git a/app/src/org/commcare/utils/KeyboardHelper.java b/app/src/org/commcare/utils/KeyboardHelper.java
new file mode 100644
index 0000000000..a44e9666bf
--- /dev/null
+++ b/app/src/org/commcare/utils/KeyboardHelper.java
@@ -0,0 +1,39 @@
+package org.commcare.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+/**
+ * Utility class for handling keyboard interactions.
+ * Provides a method to show the keyboard on a given input field.
+ *
+ * @author dviggiano
+ */
+public class KeyboardHelper {
+
+ /**
+ * Displays the soft keyboard for the specified input view.
+ * This method ensures the view gains focus before attempting to show the keyboard.
+ * A slight delay is added to ensure the keyboard appears properly.
+ *
+ * @param activity The activity context used to retrieve the InputMethodManager.
+ * @param view The input view that should receive focus and trigger the keyboard.
+ */
+ public static void showKeyboardOnInput(Activity activity, View view) {
+ view.requestFocus();
+
+ InputMethodManager inputMethodManager = (InputMethodManager)activity.getSystemService(
+ Context.INPUT_METHOD_SERVICE);
+
+ view.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (inputMethodManager != null) {
+ inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }
+ }, 250);
+ }
+}
diff --git a/app/src/org/commcare/utils/PhoneNumberHelper.java b/app/src/org/commcare/utils/PhoneNumberHelper.java
new file mode 100644
index 0000000000..cfd6c00253
--- /dev/null
+++ b/app/src/org/commcare/utils/PhoneNumberHelper.java
@@ -0,0 +1,119 @@
+package org.commcare.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+import com.google.android.gms.auth.api.identity.GetPhoneNumberHintIntentRequest;
+import com.google.android.gms.auth.api.identity.Identity;
+import com.google.android.gms.auth.api.identity.SignInClient;
+import com.google.android.gms.common.api.ApiException;
+
+import org.commcare.connect.ConnectConstants;
+
+import java.util.Locale;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.IntentSenderRequest;
+import io.michaelrocks.libphonenumber.android.NumberParseException;
+import io.michaelrocks.libphonenumber.android.PhoneNumberUtil;
+import io.michaelrocks.libphonenumber.android.Phonenumber;
+
+/**
+ * Utility class for handling phone number-related operations.
+ * Provides functions for phone number validation, formatting,
+ * country code retrieval, and requesting phone number hints.
+ */
+public class PhoneNumberHelper {
+ private final PhoneNumberUtil phoneNumberUtil;
+
+ // Constructor initializes PhoneNumberUtil
+ public PhoneNumberHelper(Context context) {
+ this.phoneNumberUtil = PhoneNumberUtil.createInstance(context);
+ }
+
+ /**
+ * Combines the country code and phone number into a single formatted string.
+ * Removes any spaces, dashes, or parentheses from the phone number.
+ *
+ * @param countryCode The country code as a string (e.g., "+1").
+ * @param phone The phone number as a string.
+ * @return A formatted phone number string with no special characters.
+ */
+ public String buildPhoneNumber(String countryCode, String phone) {
+ return String.format("%s%s", countryCode, phone).replaceAll("[-() ]", "");
+ }
+
+ /**
+ * Validates whether the given phone number is valid.
+ */
+ public boolean isValidPhoneNumber(String phone) {
+ try {
+ Phonenumber.PhoneNumber phoneNumber = phoneNumberUtil.parse(phone, null);
+ return phoneNumberUtil.isValidNumber(phoneNumber);
+ } catch (NumberParseException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Extracts the country code from a given phone number.
+ */
+ public int getCountryCode(String phone) {
+ try {
+ Phonenumber.PhoneNumber phoneNumber = phoneNumberUtil.parse(phone, null);
+ return phoneNumberUtil.isValidNumber(phoneNumber) ? phoneNumber.getCountryCode() : -1;
+ } catch (NumberParseException e) {
+ Log.d("PhoneNumberHelper", "Failed to parse number: " + e.getMessage());
+ return -1;
+ }
+ }
+
+ /**
+ * Retrieves the country code for the user's current locale.
+ */
+ public int getCountryCode(Locale locale) {
+ return phoneNumberUtil.getCountryCodeForRegion(locale.getCountry());
+ }
+
+ /**
+ * Requests a phone number hint from Google Identity API.
+ */
+ public void requestPhoneNumberHint(ActivityResultLauncher phoneNumberHintLauncher, Activity activity) {
+ GetPhoneNumberHintIntentRequest hintRequest = GetPhoneNumberHintIntentRequest.builder().build();
+ Identity.getSignInClient(activity).getPhoneNumberHintIntent(hintRequest)
+ .addOnSuccessListener(pendingIntent -> {
+ try {
+ IntentSenderRequest intentSenderRequest = new IntentSenderRequest.Builder(pendingIntent).build();
+ phoneNumberHintLauncher.launch(intentSenderRequest);
+ } catch (SecurityException | IllegalStateException e) {
+ Log.e("PhoneNumberHelper", "Error launching phone number hint", e);
+ }
+ });
+ }
+
+ /**
+ * Handles the result of a phone number picker request.
+ */
+ public String handlePhoneNumberPickerResult(int requestCode, int resultCode, Intent intent, Activity activity) {
+ if (requestCode == ConnectConstants.CREDENTIAL_PICKER_REQUEST && resultCode == Activity.RESULT_OK) {
+ SignInClient signInClient = Identity.getSignInClient(activity);
+ try {
+ return signInClient.getPhoneNumberFromIntent(intent);
+ } catch (ApiException e) {
+ Log.e("PhoneNumberHelper", "Failed to get phone number: " + e.getMessage(), e);
+ }
+ }
+ return "";
+ }
+
+ /**
+ * Returns the default country code formatted as a string with a "+" prefix.
+ */
+ public String getDefaultCountryCode() {
+ Locale locale = Locale.getDefault(); // Get default locale
+ int code = phoneNumberUtil.getCountryCodeForRegion(locale.getCountry());
+ return (code > 0) ? "+" + code : "";
+ }
+}
diff --git a/app/src/org/commcare/views/connect/CustomOtpView.java b/app/src/org/commcare/views/connect/CustomOtpView.java
new file mode 100644
index 0000000000..df0f06806b
--- /dev/null
+++ b/app/src/org/commcare/views/connect/CustomOtpView.java
@@ -0,0 +1,200 @@
+package org.commcare.views.connect;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+
+import androidx.annotation.Nullable;
+
+import org.commcare.dalvik.R;
+
+/**
+ * CustomOtpView is a customizable OTP input field view that consists of multiple EditText fields.
+ * Users can configure the number of digits, border properties, text color, and other attributes via XML.
+ */
+public class CustomOtpView extends LinearLayout {
+
+ private int digitCount;
+ private int borderColor;
+ private int errorBorderColor;
+ private int borderRadius;
+ private int borderWidth;
+ private int textColor;
+ private int errorTextColor;
+ private float textSize;
+ private OtpCompleteListener otpCompleteListener;
+ private OnOtpChangedListener otpChangedListener;
+ private boolean isErrorState = false;
+
+ /**
+ * Constructor for programmatic instantiation.
+ */
+ public CustomOtpView(Context context) {
+ super(context);
+ init(null);
+ }
+
+ /**
+ * Constructor used when inflating from XML.
+ */
+ public CustomOtpView(Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ init(attrs);
+ }
+
+ /**
+ * Initializes the view and reads XML attributes if provided.
+ */
+ private void init(AttributeSet attrs) {
+ setOrientation(HORIZONTAL);
+ setGravity(Gravity.CENTER);
+
+ // Set default values
+ digitCount = 4;
+ borderColor = Color.BLACK;
+ errorBorderColor = Color.RED;
+ borderRadius = 5;
+ borderWidth = 2;
+ textColor = Color.BLACK;
+ errorTextColor = Color.RED;
+ textSize = 14;
+
+ if (attrs != null) {
+ TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CustomOtpView);
+ digitCount = typedArray.getInt(R.styleable.CustomOtpView_otpViewDigitCount, digitCount);
+ borderColor = typedArray.getColor(R.styleable.CustomOtpView_otpViewBorderColor, borderColor);
+ errorBorderColor = typedArray.getColor(R.styleable.CustomOtpView_otpViewErrorBorderColor, errorBorderColor);
+ borderRadius = typedArray.getDimensionPixelSize(R.styleable.CustomOtpView_otpViewBorderRadius, borderRadius);
+ borderWidth = typedArray.getDimensionPixelSize(R.styleable.CustomOtpView_otpViewBorderWidth, borderWidth);
+ textColor = typedArray.getColor(R.styleable.CustomOtpView_otpViewTextColor, textColor);
+ errorTextColor = typedArray.getColor(R.styleable.CustomOtpView_otpViewErrorTextColor, errorTextColor);
+ textSize = typedArray.getDimension(R.styleable.CustomOtpView_otpViewTextSize, textSize);
+ typedArray.recycle();
+ }
+
+ createOtpFields();
+ }
+
+ /**
+ * Creates the OTP input fields dynamically based on the digit count.
+ */
+ private void createOtpFields() {
+ removeAllViews();
+ for (int i = 0; i < digitCount; i++) {
+ EditText otpEditText = createOtpEditText(i);
+ addView(otpEditText);
+ }
+ }
+
+ /**
+ * Creates an individual EditText for OTP input.
+ */
+ private EditText createOtpEditText(int index) {
+ EditText editText = new EditText(getContext());
+ LayoutParams params = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1.0f);
+ params.setMargins(8, 8, 8, 8);
+ editText.setLayoutParams(params);
+ editText.setGravity(Gravity.CENTER);
+ editText.setTextColor(isErrorState ? errorTextColor : textColor);
+ editText.setTextSize(textSize);
+ editText.setInputType(InputType.TYPE_CLASS_NUMBER);
+ editText.setBackground(createBackgroundDrawable());
+ editText.setId(index);
+ return editText;
+ }
+
+ /**
+ * Retrieves the entered OTP as a string.
+ */
+ public String getOtpValue() {
+ StringBuilder otp = new StringBuilder();
+ for (int i = 0; i < digitCount; i++) {
+ EditText editText = (EditText) getChildAt(i);
+ otp.append(editText.getText().toString());
+ }
+ return otp.toString();
+ }
+
+ /**
+ * Creates a background drawable for OTP fields.
+ */
+ private GradientDrawable createBackgroundDrawable() {
+ GradientDrawable drawable = new GradientDrawable();
+ drawable.setCornerRadius(borderRadius);
+ drawable.setStroke(borderWidth, isErrorState ? errorBorderColor : borderColor);
+ return drawable;
+ }
+
+ /**
+ * Sets an OTP completion listener.
+ */
+ public void setOtpCompleteListener(OtpCompleteListener listener) {
+ this.otpCompleteListener = listener;
+ }
+
+ /**
+ * Sets an OTP changed listener.
+ */
+ public void setOnOtpChangedListener(OnOtpChangedListener listener) {
+ this.otpChangedListener = listener;
+ }
+ /**
+ * Sets the error in the view make the view red in case of error.
+ * Dont forgot to set it to false when the error is resolved
+ */
+ public void setErrorState(boolean isError) {
+ this.isErrorState = isError;
+ post(this::updateUi); // Ensure UI updates happen on the main thread
+ }
+
+ private void updateUi() {
+ for (int i = 0; i < getChildCount(); i++) {
+ EditText editText = (EditText) getChildAt(i);
+ if (editText != null) {
+ editText.setTextColor(isErrorState ? errorTextColor : textColor);
+ editText.setBackground(createBackgroundDrawable());
+ }
+ }
+ }
+
+ /**
+ * Sets the OTP in the fields programmatically.
+ */
+ public void setOtp(String otp) {
+ if (otp.length() > digitCount) {
+ throw new IllegalArgumentException("OTP length exceeds the digit count");
+ }
+ for (int i = 0; i < digitCount; i++) {
+ EditText editText = (EditText) getChildAt(i);
+ if (i < otp.length()) {
+ editText.setText(String.valueOf(otp.charAt(i)));
+ } else {
+ editText.setText("");
+ }
+ editText.clearFocus();
+ }
+ }
+
+ /**
+ * Interface for OTP completion event.
+ */
+ public interface OtpCompleteListener {
+ void onOtpComplete(String otp);
+ }
+
+ /**
+ * Interface for OTP changed event.
+ */
+ public interface OnOtpChangedListener {
+ void onOtpChanged(String otp);
+ }
+}
diff --git a/app/src/org/commcare/views/connect/connecttextview/ConnectBoldTextView.java b/app/src/org/commcare/views/connect/connecttextview/ConnectBoldTextView.java
new file mode 100644
index 0000000000..c23c2d04ad
--- /dev/null
+++ b/app/src/org/commcare/views/connect/connecttextview/ConnectBoldTextView.java
@@ -0,0 +1,35 @@
+package org.commcare.views.connect.connecttextview;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+
+import org.commcare.dalvik.R;
+import org.javarosa.core.services.Logger;
+
+import java.time.format.TextStyle;
+
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.content.res.ResourcesCompat;
+
+public class ConnectBoldTextView extends AppCompatTextView {
+ private static final String TAG = ConnectBoldTextView.class.getSimpleName();
+ public ConnectBoldTextView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public ConnectBoldTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public ConnectBoldTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ private void init(Context context) {
+ setTypeface(null, Typeface.BOLD);
+ }
+}
diff --git a/app/src/org/commcare/views/connect/connecttextview/ConnectMediumTextView.java b/app/src/org/commcare/views/connect/connecttextview/ConnectMediumTextView.java
new file mode 100644
index 0000000000..5179df2091
--- /dev/null
+++ b/app/src/org/commcare/views/connect/connecttextview/ConnectMediumTextView.java
@@ -0,0 +1,34 @@
+package org.commcare.views.connect.connecttextview;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+
+import org.commcare.dalvik.R;
+import org.javarosa.core.services.Logger;
+
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.content.res.ResourcesCompat;
+
+public class ConnectMediumTextView extends AppCompatTextView {
+ private static final String TAG = ConnectMediumTextView.class.getSimpleName();
+
+ public ConnectMediumTextView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public ConnectMediumTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public ConnectMediumTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ private void init(Context context) {
+ setTypeface(null, Typeface.DEFAULT_BOLD.getStyle());
+ }
+}
diff --git a/app/src/org/commcare/views/connect/connecttextview/ConnectRegularTextView.java b/app/src/org/commcare/views/connect/connecttextview/ConnectRegularTextView.java
new file mode 100644
index 0000000000..25d21ae682
--- /dev/null
+++ b/app/src/org/commcare/views/connect/connecttextview/ConnectRegularTextView.java
@@ -0,0 +1,34 @@
+package org.commcare.views.connect.connecttextview;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.util.AttributeSet;
+
+import org.commcare.dalvik.R;
+import org.javarosa.core.services.Logger;
+
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.content.res.ResourcesCompat;
+
+public class ConnectRegularTextView extends AppCompatTextView {
+ private static final String TAG = ConnectRegularTextView.class.getSimpleName();
+
+ public ConnectRegularTextView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public ConnectRegularTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public ConnectRegularTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ private void init(Context context) {
+ setTypeface(null,Typeface.NORMAL);
+ }
+}
diff --git a/build.gradle b/build.gradle
index 3eb760f227..f916758bab 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,5 +1,6 @@
buildscript {
ext.kotlin_version = '1.8.20'
+ ext.nav_version = '2.8.5'
repositories {
google()
mavenCentral()
@@ -12,6 +13,7 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.jacoco:org.jacoco.core:0.8.10'
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.15.1'
+ classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
}
}