Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ef2f6a7
add iOS qrcode opener and aadhaar screen
remicolin Sep 10, 2025
c2f5ed4
Merge branch 'dev' into app/add-aadhaar-onboarding
transphorm Sep 11, 2025
1d3049f
format
transphorm Sep 11, 2025
6705d8c
fix test
transphorm Sep 11, 2025
fe9ef1a
Merge branch 'dev' of https://github.com/selfxyz/self into app/add-aa…
remicolin Sep 16, 2025
999937a
add Image-picker android (#1077)
seshanthS Sep 16, 2025
81aa409
Merge branch 'app/add-aadhaar-onboarding' of https://github.com/selfx…
remicolin Sep 16, 2025
6f1c97d
feat: implement Aadhaar upload success and error screens, enhance Aad…
remicolin Sep 17, 2025
e36f3db
feat: generate mock aadhar (#1083)
seshanthS Sep 17, 2025
5ab135f
Merge branch 'dev' of https://github.com/selfxyz/self into app/add-aa…
remicolin Sep 17, 2025
7e6b5fc
update protocolStore, update types, start modifying provingMachine
remicolin Sep 18, 2025
3933f59
Register mock aadhar (#1093)
seshanthS Sep 18, 2025
f1f65c6
Add Aadhaar support to ID card component and screens
remicolin Sep 18, 2025
b6bf07b
aadhaar disclose - wip (#1094)
seshanthS Sep 19, 2025
3040442
fix: timestamp cal of extractQRDataFields
Vishalkulkarni45 Sep 19, 2025
da64feb
Feat/aadhar fixes (#1099)
seshanthS Sep 19, 2025
f9b0e68
yarn nice
remicolin Sep 19, 2025
abbf13e
Merge branch 'dev' of https://github.com/selfxyz/self into app/add-aa…
remicolin Sep 19, 2025
a33580e
run prettier
remicolin Sep 19, 2025
fdcb6d9
Add mock Aadhaar certificates for development
remicolin Sep 19, 2025
708bad9
prettier write
remicolin Sep 19, 2025
3c0c2dd
add 'add-aadhaar' button (#1100)
seshanthS Sep 19, 2025
ab61711
Update .gitleaks.toml to include path for mock certificates in the co…
remicolin Sep 19, 2025
ea89c5f
yarn nice
remicolin Sep 19, 2025
3474761
Enhance Aadhaar error handling with specific error types
remicolin Sep 19, 2025
63f4076
Update passport handling in proving machine to support Aadhaar docume…
remicolin Sep 19, 2025
9d60335
tweak layout, text, change email to support, hide help button
transphorm Sep 19, 2025
01d630a
fix ci, remove aadhaar logging, add aadhaar events
transphorm Sep 19, 2025
5d264bf
remove unused aadhaar tracking events
transphorm Sep 19, 2025
c79adaa
update globs
transphorm Sep 19, 2025
1743737
fix gitguardian config
transphorm Sep 20, 2025
12c3866
don't track id
transphorm Sep 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ paths = [
'''pnpm-lock.yaml''',
'''Podfile.lock''',
'''common/src/mock_certificates/.*''',
'''common/dist/.*/mock_certificates/.*''',
'''common/src/constants/mockCertificates.ts''',
'''Database.refactorlog''',
'''vendor''',
Expand Down
3 changes: 3 additions & 0 deletions app/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -217,5 +217,8 @@ dependencies {
implementation "com.google.guava:guava:31.1-android"
implementation "androidx.profileinstaller:profileinstaller:1.3.1"

implementation "androidx.activity:activity:1.9.3"
implementation "androidx.activity:activity-ktx:1.9.3"

implementation "com.google.android.play:app-update:2.1.0"
}
15 changes: 15 additions & 0 deletions app/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,20 @@
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/notification_color"
tools:replace="android:resource" />

<activity
android:name=".PhotoPickerActivity"
android:theme="@style/Theme.AppCompat.Translucent"
android:exported="false" />

<service android:name="com.google.android.gms.metadata.ModuleDependencies"
android:enabled="false"
android:exported="false"
tools:ignore="MissingClass">
<intent-filter>
<action android:name="com.google.android.gms.metadata.MODULE_DEPENDENCIES" />
</intent-filter>
<meta-data android:name="photopicker_activity:0:required" android:value="" />
</service>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.

package com.proofofpassportapp;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.PickVisualMediaRequest;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;

public class PhotoPickerActivity extends AppCompatActivity {

public static final String EXTRA_SELECTED_URI = "selected_uri";
public static final String EXTRA_ERROR_MESSAGE = "error_message";
private static final String TAG = "PhotoPickerActivity";

private ActivityResultLauncher<PickVisualMediaRequest> photoPickerLauncher;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Register the photo picker launcher using the recommended API
photoPickerLauncher = registerForActivityResult(
new ActivityResultContracts.PickVisualMedia(),
this::handlePhotoPickerResult
);

launchPhotoPicker();
}

private void launchPhotoPicker() {
try {
Log.d(TAG, "Launching modern PickVisualMedia photo picker");

// Create the request using the recommended builder pattern
PickVisualMediaRequest request = new PickVisualMediaRequest.Builder()
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE)
.build();

photoPickerLauncher.launch(request);

} catch (Exception e) {
Log.e(TAG, "Failed to launch photo picker: " + e.getMessage());
finishWithError("Failed to launch photo picker: " + e.getMessage());
}
}

private void handlePhotoPickerResult(Uri selectedUri) {
if (selectedUri != null) {
Log.d(TAG, "Photo picker returned URI: " + selectedUri);
finishWithResult(selectedUri);
Comment on lines +55 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don’t log selected URIs (privacy/PII).

URIs can reveal local paths or cloud provider IDs. Avoid emitting them in production logs.

-            Log.d(TAG, "Photo picker returned URI: " + selectedUri);
+            Log.d(TAG, "Photo picker returned URI");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (selectedUri != null) {
Log.d(TAG, "Photo picker returned URI: " + selectedUri);
finishWithResult(selectedUri);
if (selectedUri != null) {
Log.d(TAG, "Photo picker returned URI");
finishWithResult(selectedUri);
🤖 Prompt for AI Agents
In app/android/app/src/main/java/com/proofofpassportapp/PhotoPickerActivity.java
around lines 55-57, the code currently logs the selectedUri which may expose
local paths or cloud IDs; remove the direct logging of the URI and instead
either (1) omit the Log.d call entirely and only call
finishWithResult(selectedUri), or (2) if you need a debug trace, wrap a log that
does not include the URI (e.g., Log.d(TAG, "Photo picker returned a URI" ) )
behind a BuildConfig.DEBUG check; ensure no production logs emit the raw URI and
if any identifier is logged, mask or hash it first.

} else {
Log.d(TAG, "Photo picker was cancelled");
finishWithError("Photo selection was cancelled");
}
}

private void finishWithResult(Uri selectedUri) {
Intent resultIntent = new Intent();
resultIntent.putExtra(EXTRA_SELECTED_URI, selectedUri.toString());
setResult(RESULT_OK, resultIntent);
finish();
}

private void finishWithError(String errorMessage) {
Intent resultIntent = new Intent();
resultIntent.putExtra(EXTRA_ERROR_MESSAGE, errorMessage);
setResult(RESULT_CANCELED, resultIntent);
finish();
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
// SPDX-License-Identifier: BUSL-1.1; Copyright (c) 2025 Social Connect Labs, Inc.; Licensed under BUSL-1.1 (see LICENSE); Apache-2.0 from 2029-06-11
// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc.
// SPDX-License-Identifier: BUSL-1.1
// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.

package com.proofofpassportapp;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
Expand All @@ -14,14 +20,21 @@
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.LifecycleEventListener;
import com.blikoon.qrcodescanner.QrCodeActivity;
import android.Manifest;
import com.proofofpassportapp.utils.QrCodeDetectorProcessor;
import example.jllarraz.com.passportreader.mlkit.FrameMetadata;
import java.io.InputStream;

public class QRCodeScannerModule extends ReactContextBaseJavaModule {
public class QRCodeScannerModule extends ReactContextBaseJavaModule implements LifecycleEventListener {

private static final int REQUEST_CODE_QR_SCAN = 101;
private static final int REQUEST_CODE_PHOTO_PICK = 102;
private static final int REQUEST_CODE_MODERN_PHOTO_PICK = 103;
private static final int PERMISSION_REQUEST_CAMERA = 1;
private Promise scanPromise;
private Promise photoLibraryPromise;

private final ActivityEventListener activityEventListener = new BaseActivityEventListener() {
@Override
Expand All @@ -36,13 +49,38 @@ public void onActivityResult(Activity activity, int requestCode, int resultCode,
}
scanPromise = null;
}
} else if (requestCode == REQUEST_CODE_PHOTO_PICK && photoLibraryPromise != null) {
// Handle legacy photo picker result for older devices
if (resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
processImageForQRCode(data.getData());
} else {
photoLibraryPromise.reject("PHOTO_PICKER_CANCELLED", "Photo selection was cancelled");
photoLibraryPromise = null;
}
} else if (requestCode == REQUEST_CODE_MODERN_PHOTO_PICK && photoLibraryPromise != null) {
// Handle modern photo picker result from dedicated activity
if (resultCode == Activity.RESULT_OK && data != null) {
String uriString = data.getStringExtra(PhotoPickerActivity.EXTRA_SELECTED_URI);
if (uriString != null) {
processImageForQRCode(Uri.parse(uriString));
} else {
photoLibraryPromise.reject("PHOTO_PICKER_ERROR", "No URI returned from photo picker");
photoLibraryPromise = null;
}
} else {
String errorMessage = data != null ? data.getStringExtra(PhotoPickerActivity.EXTRA_ERROR_MESSAGE) : "Photo selection was cancelled";
photoLibraryPromise.reject("PHOTO_PICKER_CANCELLED", errorMessage);
photoLibraryPromise = null;
}
}
}
};

QRCodeScannerModule(ReactApplicationContext reactContext) {
public QRCodeScannerModule(ReactApplicationContext reactContext) {
super(reactContext);
reactContext.addActivityEventListener(activityEventListener);
reactContext.addLifecycleEventListener(this);

}

@NonNull
Expand Down Expand Up @@ -71,12 +109,111 @@ public void scanQRCode(Promise promise) {
}
}

@ReactMethod
public void scanQRCodeFromPhotoLibrary(Promise promise) {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
promise.reject("ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist");
return;
}

photoLibraryPromise = promise;

// we first try with the recomended approach. This should be sufficient for most devices with play service.
// It fallsback to document picker if photo-picker is not available.
try {
android.util.Log.d("QRCodeScanner", "Using recommended PickVisualMedia photo picker via dedicated activity");
Intent intent = new Intent(currentActivity, PhotoPickerActivity.class);
currentActivity.startActivityForResult(intent, REQUEST_CODE_MODERN_PHOTO_PICK);
return;
} catch (Exception e) {
android.util.Log.d("QRCodeScanner", "Modern photo picker activity failed: " + e.getMessage());
}

// Fallback to intent-based photo picker for Android 13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
android.util.Log.d("QRCodeScanner", "Using intent-based modern photo picker (Android 13+)");
Intent intent = new Intent("android.provider.action.PICK_IMAGES");
intent.setType("image/*");
currentActivity.startActivityForResult(intent, REQUEST_CODE_PHOTO_PICK);
return;
} catch (Exception e) {
android.util.Log.d("QRCodeScanner", "Intent-based modern photo picker failed: " + e.getMessage());
}
}

// Final fallback to legacy photo picker
android.util.Log.d("QRCodeScanner", "Using legacy Intent.ACTION_PICK photo picker");
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
currentActivity.startActivityForResult(intent, REQUEST_CODE_PHOTO_PICK);
}
Comment on lines +112 to +151
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Legacy photo picker lacks storage permission handling (pre‑Android 13)

ACTION_PICK on API < 33 can require READ_EXTERNAL_STORAGE. Without requesting it, selection may fail on older devices.

Apply this diff to request permission before legacy fallback:

   public void scanQRCodeFromPhotoLibrary(Promise promise) {
     ...
-    // Final fallback to legacy photo picker
+    // Final fallback to legacy photo picker (requires READ_EXTERNAL_STORAGE on pre-Android 13)
     android.util.Log.d("QRCodeScanner", "Using legacy Intent.ACTION_PICK photo picker");
-    Intent intent = new Intent(Intent.ACTION_PICK);
-    intent.setType("image/*");
-    currentActivity.startActivityForResult(intent, REQUEST_CODE_PHOTO_PICK);
+    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
+        ContextCompat.checkSelfPermission(currentActivity, Manifest.permission.READ_EXTERNAL_STORAGE)
+            != PackageManager.PERMISSION_GRANTED) {
+      ActivityCompat.requestPermissions(
+        currentActivity,
+        new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
+        REQUEST_CODE_PHOTO_PICK /* reuse same code for simplicity */
+      );
+      return;
+    }
+    Intent intent = new Intent(Intent.ACTION_PICK);
+    intent.setType("image/*");
+    currentActivity.startActivityForResult(intent, REQUEST_CODE_PHOTO_PICK);
   }

Note: If you want stricter separation, introduce a dedicated permission request code and handle it.


private void startQRScanner(Activity activity) {
Intent intent = new Intent(activity, QrCodeActivity.class);
activity.startActivityForResult(intent, REQUEST_CODE_QR_SCAN);
}

// Add this method to handle permission result
private void processImageForQRCode(Uri imageUri) {
try {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist");
photoLibraryPromise = null;
}
return;
}

InputStream inputStream = currentActivity.getContentResolver().openInputStream(imageUri);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
inputStream.close();

if (bitmap == null) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("IMAGE_LOAD_FAILED", "Failed to load selected image");
photoLibraryPromise = null;
}
return;
}

// use the exising qrcode processor we already have.
QrCodeDetectorProcessor processor = new QrCodeDetectorProcessor();
processor.detectQrCodeInBitmap(bitmap, new QrCodeDetectorProcessor.Listener() {
@Override
public void onSuccess(String results, FrameMetadata frameMetadata, long timeRequired, Bitmap bitmap) {
if (photoLibraryPromise != null) {
photoLibraryPromise.resolve(results);
photoLibraryPromise = null;
}
}

@Override
public void onFailure(Exception e, long timeRequired) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image: " + e.getMessage());
photoLibraryPromise = null;
}
}

@Override
public void onCompletedFrame(long timeRequired) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image");
photoLibraryPromise = null;
}
}
});

} catch (Exception e) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("IMAGE_PROCESSING_ERROR", "Error processing image: " + e.getMessage());
photoLibraryPromise = null;
}
}
}
Comment on lines +158 to +215
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Decode large images off the UI thread and downsample to avoid OOM/ANR

BitmapFactory.decodeStream runs on UI thread and loads full-resolution images; this can freeze or crash on modern photos. Move decode to a background thread and sample down to a sane max size.

Apply this diff to downsample and offload:

-  private void processImageForQRCode(Uri imageUri) {
-    try {
-      Activity currentActivity = getCurrentActivity();
-      ...
-      InputStream inputStream = currentActivity.getContentResolver().openInputStream(imageUri);
-      Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
-      inputStream.close();
-      if (bitmap == null) { ... }
-      // use the exising qrcode processor we already have.
-      QrCodeDetectorProcessor processor = new QrCodeDetectorProcessor();
-      processor.detectQrCodeInBitmap(bitmap, new QrCodeDetectorProcessor.Listener() {
+  private void processImageForQRCode(Uri imageUri) {
+    Activity currentActivity = getCurrentActivity();
+    if (currentActivity == null) {
+      if (photoLibraryPromise != null) {
+        photoLibraryPromise.reject("ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist");
+        photoLibraryPromise = null;
+      }
+      return;
+    }
+    new Thread(() -> {
+      try (InputStream isBounds = currentActivity.getContentResolver().openInputStream(imageUri)) {
+        if (isBounds == null) throw new Exception("Cannot open image");
+        BitmapFactory.Options opts = new BitmapFactory.Options();
+        opts.inJustDecodeBounds = true;
+        BitmapFactory.decodeStream(isBounds, null, opts);
+        int req = 1600; // max dimension
+        int scale = 1;
+        while ((opts.outWidth / scale) > req || (opts.outHeight / scale) > req) {
+          scale *= 2;
+        }
+        BitmapFactory.Options decodeOpts = new BitmapFactory.Options();
+        decodeOpts.inSampleSize = Math.max(1, scale);
+        try (InputStream isDecode = currentActivity.getContentResolver().openInputStream(imageUri)) {
+          Bitmap bitmap = BitmapFactory.decodeStream(isDecode, null, decodeOpts);
+          if (bitmap == null) throw new Exception("Failed to decode image");
+          QrCodeDetectorProcessor processor = new QrCodeDetectorProcessor();
+          processor.detectQrCodeInBitmap(bitmap, new QrCodeDetectorProcessor.Listener() {
             @Override
             public void onSuccess(String results, FrameMetadata frameMetadata, long timeRequired, Bitmap bitmap) {
               if (photoLibraryPromise != null) {
                 photoLibraryPromise.resolve(results);
                 photoLibraryPromise = null;
               }
             }
             @Override
             public void onFailure(Exception e, long timeRequired) {
               if (photoLibraryPromise != null) {
                 photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image: " + e.getMessage());
                 photoLibraryPromise = null;
               }
             }
             @Override
             public void onCompletedFrame(long timeRequired) {
               if (photoLibraryPromise != null) {
                 photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image");
                 photoLibraryPromise = null;
               }
             }
-      });
-    } catch (Exception e) {
-      if (photoLibraryPromise != null) {
-        photoLibraryPromise.reject("IMAGE_PROCESSING_ERROR", "Error processing image: " + e.getMessage());
-        photoLibraryPromise = null;
-      }
-    }
+          });
+        }
+      } catch (Exception e) {
+        if (photoLibraryPromise != null) {
+          photoLibraryPromise.reject("IMAGE_PROCESSING_ERROR", "Error processing image: " + e.getMessage());
+          photoLibraryPromise = null;
+        }
+      }
+    }).start();
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void processImageForQRCode(Uri imageUri) {
try {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist");
photoLibraryPromise = null;
}
return;
}
InputStream inputStream = currentActivity.getContentResolver().openInputStream(imageUri);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
inputStream.close();
if (bitmap == null) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("IMAGE_LOAD_FAILED", "Failed to load selected image");
photoLibraryPromise = null;
}
return;
}
// use the exising qrcode processor we already have.
QrCodeDetectorProcessor processor = new QrCodeDetectorProcessor();
processor.detectQrCodeInBitmap(bitmap, new QrCodeDetectorProcessor.Listener() {
@Override
public void onSuccess(String results, FrameMetadata frameMetadata, long timeRequired, Bitmap bitmap) {
if (photoLibraryPromise != null) {
photoLibraryPromise.resolve(results);
photoLibraryPromise = null;
}
}
@Override
public void onFailure(Exception e, long timeRequired) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image: " + e.getMessage());
photoLibraryPromise = null;
}
}
@Override
public void onCompletedFrame(long timeRequired) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image");
photoLibraryPromise = null;
}
}
});
} catch (Exception e) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("IMAGE_PROCESSING_ERROR", "Error processing image: " + e.getMessage());
photoLibraryPromise = null;
}
}
}
private void processImageForQRCode(Uri imageUri) {
Activity currentActivity = getCurrentActivity();
if (currentActivity == null) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("ACTIVITY_DOES_NOT_EXIST", "Activity doesn't exist");
photoLibraryPromise = null;
}
return;
}
new Thread(() -> {
try (InputStream isBounds = currentActivity.getContentResolver().openInputStream(imageUri)) {
if (isBounds == null) throw new Exception("Cannot open image");
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeStream(isBounds, null, opts);
int req = 1600; // max dimension
int scale = 1;
while ((opts.outWidth / scale) > req || (opts.outHeight / scale) > req) {
scale *= 2;
}
BitmapFactory.Options decodeOpts = new BitmapFactory.Options();
decodeOpts.inSampleSize = Math.max(1, scale);
try (InputStream isDecode = currentActivity.getContentResolver().openInputStream(imageUri)) {
Bitmap bitmap = BitmapFactory.decodeStream(isDecode, null, decodeOpts);
if (bitmap == null) throw new Exception("Failed to decode image");
QrCodeDetectorProcessor processor = new QrCodeDetectorProcessor();
processor.detectQrCodeInBitmap(bitmap, new QrCodeDetectorProcessor.Listener() {
@Override
public void onSuccess(String results, FrameMetadata frameMetadata, long timeRequired, Bitmap bitmap) {
if (photoLibraryPromise != null) {
photoLibraryPromise.resolve(results);
photoLibraryPromise = null;
}
}
@Override
public void onFailure(Exception e, long timeRequired) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image: " + e.getMessage());
photoLibraryPromise = null;
}
}
@Override
public void onCompletedFrame(long timeRequired) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("QR_DETECTION_FAILED", "No QR code found in selected image");
photoLibraryPromise = null;
}
}
});
}
} catch (Exception e) {
if (photoLibraryPromise != null) {
photoLibraryPromise.reject("IMAGE_PROCESSING_ERROR", "Error processing image: " + e.getMessage());
photoLibraryPromise = null;
}
}
}).start();
}
🤖 Prompt for AI Agents
In app/android/app/src/main/java/com/proofofpassportapp/QRCodeScannerModule.java
around lines 158 to 215, the current implementation decodes the full-resolution
image on the UI thread which can cause OOMs and ANRs; move image loading and
downsampling to a background thread and only run QR detection on the
decoded/downsampled bitmap. Specifically: open the InputStream, read image
bounds with BitmapFactory.Options(inJustDecodeBounds=true) to compute a safe
inSampleSize for a configured max dimension (e.g. 1024px), then reopen the
stream and decode with that inSampleSize off the UI thread using an Executor or
other background worker, ensure streams are closed in finally blocks, and post
the decoded bitmap back to the main thread before invoking the existing
QrCodeDetectorProcessor callbacks so promises are resolved/rejected on the UI
thread; also keep existing null/exception handling and clear photoLibraryPromise
in all code paths.


public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode == PERMISSION_REQUEST_CAMERA) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Expand All @@ -92,4 +229,19 @@ public void onRequestPermissionsResult(int requestCode, String[] permissions, in
}
}
}

// Lifecycle methods
@Override
public void onHostResume() {
}

@Override
public void onHostPause() {
}

@Override
public void onHostDestroy() {
getReactApplicationContext().removeActivityEventListener(activityEventListener);
getReactApplicationContext().removeLifecycleEventListener(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public List<ViewManager> createViewManagers(ReactApplicationContext reactContext

@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
return List.of(
new QRCodeScannerModule(reactContext)
);
}
}
Loading
Loading