Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Micro Image widget - 1/3 #2954

Open
wants to merge 19 commits into
base: master
Choose a base branch
from

Conversation

avazirna
Copy link
Contributor

@avazirna avazirna commented Feb 17, 2025

Product Description

This PR is the first of a series of three, each responsible for a specific aspect of the Micro Image feature:

  1. Micro image widget
  2. Micro image renderization in case lists
  3. Manual mode capabilities for when face detection mode is not possible.

The main purpose of the Micro image widget is to capture a facial image, compress it to up to 2KB and store it as a normal case property in Base64 format. In order to achieve this, the widget relies on the Google ML Kit Face detection module to identify and analyze faces in the image stream displayed in the preview surface, triggering the next steps when the image is considered stable.
The workflow involves:

  1. Identification - an oval marks the area where the face needs to be positioned, and once a face is detected, a smaller oval highlights it
  2. Stabilization - based on the image stream, the widget establishes whether the detected face moved compared to its coordinates in the previous frame. And depending on the outcome:
    1.1. Face didn't move - the face is considered stable and the countdown is triggered. Once the countdown gets to zero, the face is considered stabilized and the capture process triggered.
    1.2. Image moved - the image is not considered stable and the workflow goes back to the identification step and the counter reset.
  3. Image capture and processing - the coordinates of the detected face are converted from the preview system to the predefined image size, cropped, compressed to the desired size and converted to the base64 format.

The video below depicts the process:

micro_image_widget.mp4

Technical Summary

Tech spec: https://docs.google.com/document/d/14yDNK6RyQGB-8H6DioGgoj0c2mwJuI8nWck4zFMYFAs

Feature Flag

FF: https://www.commcarehq.org/hq/flags/edit/case_micro_image/

Safety Assurance

Safety story

This feature was successfully tested locally and has undergone a couple of rounds of UAT.

Automated test coverage

QA Plan

The QA Test plan will be to be updated to cover this feature.

Labels and Review

  • Do we need to enhance the manual QA test coverage ? If yes, the "QA Note" label is set correctly
  • Does the PR introduce any major changes worth communicating ? If yes, the "Release Note" label is set and a "Release Note" is specified in PR description.
  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

cross-request: dimagi/commcare-core#1459

Copy link

coderabbitai bot commented Feb 17, 2025

📝 Walkthrough

Walkthrough

This pull request introduces enhancements to the application's image capture and face detection functionality. A new activity, MicroImageActivity, is added to support micro image captures, with its corresponding layout defined in micro_image_widget.xml. Updates in the Android manifest include a new meta-data entry for ML Kit’s face detection dependencies and an activity declaration for MicroImageActivity. The build script now includes new dependencies for the AndroidX Camera libraries and ML Kit face detection. New resource entries in XML files provide additional styling, string resources, and color definitions for the user interface components. Additionally, support for micro image capture is integrated into the existing form entry flow by extending constants, modifying response handling in FormEntryActivity, and creating a new widget class MicroImageWidget. Enhancements in MediaUtil and FaceCaptureView facilitate image cropping, compression, and processing, while access modifiers in ImageWidget have been adjusted for better subclass integration.

Sequence Diagram(s)

sequenceDiagram
    participant U as User
    participant MIW as MicroImageWidget
    participant MIA as MicroImageActivity
    participant CX as CameraX Provider
    participant ML as ML Kit Face Detection
    participant FCV as FaceCaptureView
    participant MU as MediaUtil

    U->>MIW: Initiates image capture
    MIW->>MIA: Launch MicroImageActivity (Intent)
    MIA->>CX: Request camera provider
    CX-->>MIA: Provide camera provider
    MIA->>CX: Bind preview & image analyzer
    CX->>ML: Process image frames
    ML-->>CX: Return detection results
    CX->>MIA: Forward frame analysis
    MIA->>FCV: Update face capture view
    FCV-->>MIA: Notify image stabilization
    MIA->>MU: Process image (crop & compress)
    MU-->>MIA: Return processed image
    MIA->>MIW: Deliver image result
    MIW->>U: Display captured image
Loading

Possibly related PRs

  • Backmerge Commcare 2.55 #2940: The changes in the main PR are related to the modifications in the AndroidManifest.xml file, as both PRs involve updates to this file, although the specific changes differ.

Suggested labels

Release Note, QA Note, High Risk

Suggested reviewers

  • shubham1g5
  • OrangeAndGreen
✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (11)
app/src/org/commcare/fragments/MicroImageActivity.java (3)

52-55: Consider adding user guards for critical UI references.
In onCreate, the cameraView and faceCaptureView references are used soon after retrieving them via findViewById. If these views are crucial for functionality, you may want to add non-null checks or logs for better robustness in the event that layout inflation fails or a future layout change might remove these views.


66-71: Handle incompatible camera hardware gracefully.
When calling startCamera() inside onCreate(), any ExecutionException or InterruptedException leads to an error message and returns. However, consider whether you also need a user-friendly fallback or an alternate path if the device lacks camera support or lacks the required ML capabilities, so that the user isn’t left in a dead-end activity.


127-148: Close the FaceDetector to free resources.
While the imageProxy is correctly closed, the FaceDetector might also need explicit shutdown when you’re done (e.g., in onDestroy()) to release underlying resources. This is particularly helpful if the activity runs for an extended time or if multiple detectors could be used.

app/src/org/commcare/views/FaceCaptureView.java (4)

48-60: Handle orientation changes dynamically.
Currently, imageWidth and imageHeight are set based on the device’s orientation at initialization. If the user rotates the device after launch, the sizes might no longer match the reality. Consider re-initializing or recalculating these in onConfigurationChanged or ensuring you lock the orientation.


74-100: Avoid redrawing background overlay on every orientation pass.
initCameraView always recreates a large bitmap and draws a face capture overlay. If the view size changes frequently (e.g., orientation changes or split-screen mode), it might be more efficient to recalculate only when strictly necessary, or to move this logic into a dedicated onDraw override.


129-138: Notify listeners only upon actual face transitions.
Currently, every time updateFace is called with a non-blank or blank face, postInvalidate() is triggered. Depending on the detection frequency, this can cause frequent UI updates. If performance becomes an issue on less capable devices, you might consider a threshold-based approach (e.g., only re-draw if face position changed meaningfully).


213-219: Optionally handle multiple faces.
In updateFace, the code processes the first detected face. If future needs require tracking multiple faces simultaneously, you might expand logic to loop over all faces. Clarify in doc comments that only a single face is tracked if that’s by design.

app/src/org/commcare/views/widgets/MicroImageWidget.java (1)

80-101: Address the TODO comment and improve bitmap configuration handling.

The scaling logic is correct, but consider:

  1. Moving the scaling logic to a utility class for reuse
  2. Adding null checks for input bitmap
  3. Using more efficient bitmap configuration options

Would you like me to generate a refactored implementation in a utility class?

app/src/org/commcare/utils/MediaUtil.java (2)

45-45: Consider adjusting the quality reduction factor.

The constant IMAGE_QUALIY_REDUCTION_FACTOR = 10 might be too aggressive for quality reduction. A smaller value like 5 would provide more granular control over compression.

-    private static final int IMAGE_QUALIY_REDUCTION_FACTOR = 10;
+    private static final int IMAGE_QUALIY_REDUCTION_FACTOR = 5;

538-543: Simplify the validation logic.

The validation logic can be simplified for better readability.

     private static boolean validateCropArea(Bitmap bitmap, Rect cropArea) {
-        if (bitmap.getHeight() >= cropArea.top && bitmap.getHeight() >= cropArea.bottom && bitmap.getWidth() >= cropArea.left && bitmap.getWidth() >= cropArea.right){
-            return true;
-        }
-        return false;
+        return bitmap.getHeight() >= cropArea.bottom && 
+               bitmap.getWidth() >= cropArea.right &&
+               cropArea.top >= 0 && 
+               cropArea.left >= 0;
     }
app/res/layout/micro_image_widget.xml (1)

20-28: Consider adding state handling attributes.

The FaceCaptureView should handle state changes and configuration changes gracefully.

     <org.commcare.views.FaceCaptureView
         android:id="@+id/face_overlay"
         android:layout_width="match_parent"
         android:layout_height="0dp"
+        android:saveEnabled="true"
         app:background_color="@color/cc_neutral_bg_tr"
         app:face_capture_area_delimiter_color="@color/white"
         app:face_marker_color="@color/cc_attention_positive_color"
         app:countdown_text_size="@dimen/font_size_large"
         android:layout_weight="1"/>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8323e54 and 4bc607a.

📒 Files selected for processing (16)
  • app/AndroidManifest.xml (2 hunks)
  • app/build.gradle (1 hunks)
  • app/res/layout/micro_image_widget.xml (1 hunks)
  • app/res/values/attrs.xml (1 hunks)
  • app/res/values/colors.xml (1 hunks)
  • app/res/values/strings.xml (1 hunks)
  • app/src/org/commcare/activities/FormEntryActivity.java (1 hunks)
  • app/src/org/commcare/activities/components/FormEntryConstants.java (1 hunks)
  • app/src/org/commcare/fragments/MicroImageActivity.java (1 hunks)
  • app/src/org/commcare/utils/MediaUtil.java (4 hunks)
  • app/src/org/commcare/views/FaceCaptureView.java (1 hunks)
  • app/src/org/commcare/views/widgets/ImageWidget.java (4 hunks)
  • app/src/org/commcare/views/widgets/MediaWidget.java (1 hunks)
  • app/src/org/commcare/views/widgets/MicroImageWidget.java (1 hunks)
  • app/src/org/commcare/views/widgets/WidgetFactory.java (1 hunks)
  • build.gradle (1 hunks)
🔇 Additional comments (20)
app/src/org/commcare/fragments/MicroImageActivity.java (1)

174-185: Validate cropping area before saving.
In onImageStabilizedListener, you assume inputImage is valid and the cropping coordinates are within bounds. Consider verifying that the rectangle does not exceed the bitmap dimensions (especially if rotation or scaling edge cases occur). This reduces the risk of runtime errors.

app/src/org/commcare/activities/components/FormEntryConstants.java (1)

24-24: No concerns; the constant is well-scoped.
The addition of MICRO_IMAGE_CAPTURE aligns with the existing pattern of request codes. Looks good.

app/src/org/commcare/views/widgets/MicroImageWidget.java (4)

24-27: LGTM! Constants are well-defined.

The size limits (72px dimensions, 2KB file size) are appropriate for micro images.


29-36: LGTM! Constructor is properly implemented.

The constructor correctly initializes the widget, hides unnecessary UI elements, and handles existing image data.


38-43: LGTM! Image capture is properly implemented.

The method correctly launches the micro image capture activity and handles the form entry state.


65-78: LGTM! Answer handling is properly implemented.

The methods correctly handle the binary data and cleanup.

app/src/org/commcare/views/widgets/WidgetFactory.java (2)

54-56: LGTM! Widget factory integration is correct.

The new case properly handles the micro image control type.


54-56: LGTM!

The new case for handling CONTROL_MICRO_IMAGE is correctly positioned and properly instantiates the MicroImageWidget with the required parameters.

app/src/org/commcare/views/widgets/MediaWidget.java (2)

359-361: LGTM! File existence check prevents potential crashes.

The added check ensures the media file exists before attempting to play it.


359-361: LGTM!

The added file existence check improves error handling by preventing potential crashes when attempting to play non-existent media files.

app/src/org/commcare/activities/FormEntryActivity.java (1)

361-362: LGTM! Micro image capture handling added.

The micro image capture is correctly integrated into the existing image capture flow.

build.gradle (1)

33-33:

✅ Verification successful

Verify CameraX version compatibility.

The specified CameraX version 1.2.3 should be verified for compatibility with the minimum supported Android API level.


🌐 Web query:

What is the minimum Android API level required for CameraX version 1.2.3?

💡 Result:

CameraX version 1.2.3 requires a minimum Android API level of 21 (Android 5.0 Lollipop)[5][7]. This applies to all core CameraX functionality, including preview, image capture, and analysis use cases.

The requirement for API 21 ensures compatibility with approximately 90% of active Android devices[1][3]. While newer versions of Android may include additional optimizations or features, CameraX maintains backward compatibility through abstraction layers and automated device testing[1][5].

Citations:


Action: Ensure your project's minSdkVersion is set to API level 21 or higher.

The cameraX_version is set to '1.2.3' in build.gradle (line 33), and as confirmed, CameraX v1.2.3 requires a minimum Android API level of 21. Please verify that your project's build configuration (e.g., in the module-level build.gradle) specifies a minSdkVersion of at least 21 to maintain compatibility.

app/res/values/attrs.xml (1)

56-61: New FaceCaptureView Styleable Declaration Added
The new <declare-styleable name="FaceCaptureView"> with its four attributes (background_color, face_capture_area_delimiter_color, face_marker_color, and countdown_text_size) is clear and well defined. Ensure that these attribute names match the naming conventions used elsewhere in the project for consistency.

app/res/values/colors.xml (1)

89-89: Addition of New Color cc_neutral_bg_tr
A new color resource <color name="cc_neutral_bg_tr">#80D6D6D4</color> has been added to expand the palette. This color—with its transparency setting—appears to be designed for use in overlays or subtle backgrounds. Verify that its visual integration with UI components (especially in the Micro Image Widget) meets design expectations.

app/build.gradle (1)

69-73: Integration of New CameraX and ML Kit Dependencies
New dependencies for the AndroidX Camera libraries (camera-view, camera-core, camera-camera2, camera-lifecycle) and ML Kit face detection (com.google.android.gms:play-services-mlkit-face-detection:17.1.0) have been added. They’re correctly parameterized using the cameraX_version variable. Please ensure that the cameraX_version is defined appropriately elsewhere and that these versions are compatible with the rest of the codebase.

app/AndroidManifest.xml (2)

142-145: Adding ML Kit Face Detection Meta-data
The new <meta-data> element for com.google.mlkit.vision.DEPENDENCIES with a value of "face" is appropriately added. This configuration informs ML Kit which dependencies to load for face detection. Confirm that this value aligns with the ML Kit implementation expectations in your app.


324-327: New MicroImageActivity Declaration
A new <activity> for org.commcare.fragments.MicroImageActivity is introduced with android:windowSoftInputMode="adjustResize". This ensures that the activity’s UI adapts correctly when the soft keyboard appears. Verify that the package name and declared behavior match the implementation of the Micro Image Widget, and that any corresponding resources (layouts, strings, etc.) are properly configured.

app/res/values/strings.xml (1)

464-467: New String Resources for Micro Image Functionality
Four new string resources related to the micro image capture feature have been added:

  • micro_image_activity_title: "Capture Face Picture"
  • camera_start_failed: "Camera failed to start"
  • face_detection_mode_failed: "Face detection mode failed"
  • micro_image_cropping_failed: "Image cropping failed, make sure the target is in the capture area"

These resources follow the project’s naming conventions and support clear user feedback in cases of error.

app/src/org/commcare/views/widgets/ImageWidget.java (2)

67-67: LGTM!

The access modifier changes from private to protected enable proper inheritance and extension by subclasses like MicroImageWidget.

Also applies to: 71-71, 78-78, 247-247, 273-273


212-212: LGTM!

Moving the discard button visibility update inside the conditional block ensures proper UI state management, showing the button only when an image exists.

Comment on lines 45 to 63
@Override
public void setBinaryData(Object binaryPath) {
if (mBinaryName != null) {
deleteMedia();
}

File f = new File(binaryPath.toString());
Bitmap originalImage = BitmapFactory.decodeFile(binaryPath.toString());

Bitmap scaledDownBitmap = scaleImage(originalImage, IMAGE_DIMEN_SCALED_MAX_PX, IMAGE_DIMEN_SCALED_MAX_PX);
byte[] compressedBitmapByteArray = MediaUtil.compressBitmapToTargetSize(scaledDownBitmap, MICRO_IMAGE_MAX_SIZE_BYTES);

try {
mBinary = Base64.encodeToString(compressedBitmapByteArray, Base64.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
mBinaryName = f.getName();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling and resource cleanup for bitmap operations.

The bitmap operations could lead to OutOfMemoryError in low memory conditions. Consider:

  1. Adding try-catch blocks for OOM errors
  2. Explicitly recycling bitmap resources

Apply this diff to improve error handling and resource cleanup:

 @Override
 public void setBinaryData(Object binaryPath) {
     if (mBinaryName != null) {
         deleteMedia();
     }

     File f = new File(binaryPath.toString());
-    Bitmap originalImage = BitmapFactory.decodeFile(binaryPath.toString());
+    Bitmap originalImage = null;
+    Bitmap scaledDownBitmap = null;
+    try {
+        originalImage = BitmapFactory.decodeFile(binaryPath.toString());
+        if (originalImage == null) {
+            throw new RuntimeException("Failed to decode image file");
+        }
+        scaledDownBitmap = scaleImage(originalImage, IMAGE_DIMEN_SCALED_MAX_PX, IMAGE_DIMEN_SCALED_MAX_PX);
+        byte[] compressedBitmapByteArray = MediaUtil.compressBitmapToTargetSize(scaledDownBitmap, MICRO_IMAGE_MAX_SIZE_BYTES);
+        mBinary = Base64.encodeToString(compressedBitmapByteArray, Base64.DEFAULT);
+    } catch (OutOfMemoryError e) {
+        throw new RuntimeException("Insufficient memory to process image", e);
+    } finally {
+        if (originalImage != null) {
+            originalImage.recycle();
+        }
+        if (scaledDownBitmap != null) {
+            scaledDownBitmap.recycle();
+        }
+    }
-    Bitmap scaledDownBitmap = scaleImage(originalImage, IMAGE_DIMEN_SCALED_MAX_PX, IMAGE_DIMEN_SCALED_MAX_PX);
-    byte[] compressedBitmapByteArray = MediaUtil.compressBitmapToTargetSize(scaledDownBitmap, MICRO_IMAGE_MAX_SIZE_BYTES);

-    try {
-        mBinary = Base64.encodeToString(compressedBitmapByteArray, Base64.DEFAULT);
-    } catch (Exception e) {
-        throw new RuntimeException(e);
-    }
     mBinaryName = f.getName();
 }
📝 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
@Override
public void setBinaryData(Object binaryPath) {
if (mBinaryName != null) {
deleteMedia();
}
File f = new File(binaryPath.toString());
Bitmap originalImage = BitmapFactory.decodeFile(binaryPath.toString());
Bitmap scaledDownBitmap = scaleImage(originalImage, IMAGE_DIMEN_SCALED_MAX_PX, IMAGE_DIMEN_SCALED_MAX_PX);
byte[] compressedBitmapByteArray = MediaUtil.compressBitmapToTargetSize(scaledDownBitmap, MICRO_IMAGE_MAX_SIZE_BYTES);
try {
mBinary = Base64.encodeToString(compressedBitmapByteArray, Base64.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
mBinaryName = f.getName();
}
@Override
public void setBinaryData(Object binaryPath) {
if (mBinaryName != null) {
deleteMedia();
}
File f = new File(binaryPath.toString());
Bitmap originalImage = null;
Bitmap scaledDownBitmap = null;
try {
originalImage = BitmapFactory.decodeFile(binaryPath.toString());
if (originalImage == null) {
throw new RuntimeException("Failed to decode image file");
}
scaledDownBitmap = scaleImage(originalImage, IMAGE_DIMEN_SCALED_MAX_PX, IMAGE_DIMEN_SCALED_MAX_PX);
byte[] compressedBitmapByteArray = MediaUtil.compressBitmapToTargetSize(scaledDownBitmap, MICRO_IMAGE_MAX_SIZE_BYTES);
mBinary = Base64.encodeToString(compressedBitmapByteArray, Base64.DEFAULT);
} catch (OutOfMemoryError e) {
throw new RuntimeException("Insufficient memory to process image", e);
} finally {
if (originalImage != null) {
originalImage.recycle();
}
if (scaledDownBitmap != null) {
scaledDownBitmap.recycle();
}
}
mBinaryName = f.getName();
}

Comment on lines 519 to 536
/**
* Crops an image according to a given area and saves the resulting image
*/
public static void cropAndSaveImage(Bitmap bitmap, Rect cropArea, File imageFile) {
if (!validateCropArea(bitmap, cropArea)) {
throw new RuntimeException("Cropping failed due to invalid area!");
}

Bitmap croppedBitmap = Bitmap.createBitmap(bitmap, cropArea.left, cropArea.top,
cropArea.right - cropArea.left, cropArea.bottom - cropArea.top);

try {
FileUtil.writeBitmapToDiskAndCleanupHandles(croppedBitmap,
ImageType.fromExtension(FileUtil.getExtension(imageFile.getPath())), imageFile);
} catch (IOException e) {
throw new RuntimeException("Failed to save image after cropping", e);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Handle bitmap recycling to prevent memory leaks.

The cropped bitmap should be recycled after saving to prevent memory leaks, especially since bitmap operations are memory-intensive.

     public static void cropAndSaveImage(Bitmap bitmap, Rect cropArea, File imageFile) {
         if (!validateCropArea(bitmap, cropArea)) {
             throw new RuntimeException("Cropping failed due to invalid area!");
         }
 
         Bitmap croppedBitmap = Bitmap.createBitmap(bitmap, cropArea.left, cropArea.top,
                 cropArea.right - cropArea.left, cropArea.bottom - cropArea.top);
 
         try {
             FileUtil.writeBitmapToDiskAndCleanupHandles(croppedBitmap,
                     ImageType.fromExtension(FileUtil.getExtension(imageFile.getPath())), imageFile);
+            if (croppedBitmap != bitmap) {
+                croppedBitmap.recycle();
+            }
         } catch (IOException e) {
             throw new RuntimeException("Failed to save image after cropping", e);
         }
     }
📝 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
/**
* Crops an image according to a given area and saves the resulting image
*/
public static void cropAndSaveImage(Bitmap bitmap, Rect cropArea, File imageFile) {
if (!validateCropArea(bitmap, cropArea)) {
throw new RuntimeException("Cropping failed due to invalid area!");
}
Bitmap croppedBitmap = Bitmap.createBitmap(bitmap, cropArea.left, cropArea.top,
cropArea.right - cropArea.left, cropArea.bottom - cropArea.top);
try {
FileUtil.writeBitmapToDiskAndCleanupHandles(croppedBitmap,
ImageType.fromExtension(FileUtil.getExtension(imageFile.getPath())), imageFile);
} catch (IOException e) {
throw new RuntimeException("Failed to save image after cropping", e);
}
}
/**
* Crops an image according to a given area and saves the resulting image
*/
public static void cropAndSaveImage(Bitmap bitmap, Rect cropArea, File imageFile) {
if (!validateCropArea(bitmap, cropArea)) {
throw new RuntimeException("Cropping failed due to invalid area!");
}
Bitmap croppedBitmap = Bitmap.createBitmap(bitmap, cropArea.left, cropArea.top,
cropArea.right - cropArea.left, cropArea.bottom - cropArea.top);
try {
FileUtil.writeBitmapToDiskAndCleanupHandles(croppedBitmap,
ImageType.fromExtension(FileUtil.getExtension(imageFile.getPath())), imageFile);
if (croppedBitmap != bitmap) {
croppedBitmap.recycle();
}
} catch (IOException e) {
throw new RuntimeException("Failed to save image after cropping", e);
}
}

Comment on lines 545 to 562
public static byte[] compressBitmapToTargetSize(Bitmap bitmap, int targetSize) {
if (bitmap == null) {
return null;
}

byte[] byteArray = null;
int quality = 100;
while (quality != 0) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.WEBP, quality, baos);
byteArray = baos.toByteArray();
if (byteArray.length <= targetSize) {
break;
}
quality -= IMAGE_QUALIY_REDUCTION_FACTOR;
}
return byteArray;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null check for ByteArrayOutputStream and handle resource cleanup.

The ByteArrayOutputStream should be properly closed to prevent resource leaks.

     public static byte[] compressBitmapToTargetSize(Bitmap bitmap, int targetSize) {
         if (bitmap == null) {
             return null;
         }
 
         byte[] byteArray = null;
         int quality = 100;
         while (quality != 0) {
-            ByteArrayOutputStream baos = new ByteArrayOutputStream();
-            bitmap.compress(Bitmap.CompressFormat.WEBP, quality, baos);
-            byteArray = baos.toByteArray();
-            if (byteArray.length <= targetSize) {
-                break;
+            try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+                bitmap.compress(Bitmap.CompressFormat.WEBP, quality, baos);
+                byteArray = baos.toByteArray();
+                if (byteArray.length <= targetSize) {
+                    break;
+                }
+                quality -= IMAGE_QUALIY_REDUCTION_FACTOR;
             }
-            quality -= IMAGE_QUALIY_REDUCTION_FACTOR;
         }
         return byteArray;
     }
📝 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
public static byte[] compressBitmapToTargetSize(Bitmap bitmap, int targetSize) {
if (bitmap == null) {
return null;
}
byte[] byteArray = null;
int quality = 100;
while (quality != 0) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.WEBP, quality, baos);
byteArray = baos.toByteArray();
if (byteArray.length <= targetSize) {
break;
}
quality -= IMAGE_QUALIY_REDUCTION_FACTOR;
}
return byteArray;
}
public static byte[] compressBitmapToTargetSize(Bitmap bitmap, int targetSize) {
if (bitmap == null) {
return null;
}
byte[] byteArray = null;
int quality = 100;
while (quality != 0) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
bitmap.compress(Bitmap.CompressFormat.WEBP, quality, baos);
byteArray = baos.toByteArray();
if (byteArray.length <= targetSize) {
break;
}
quality -= IMAGE_QUALIY_REDUCTION_FACTOR;
}
}
return byteArray;
}

Comment on lines +564 to +571
public static Bitmap decodeBase64EncodedBitmap(String base64Image){
try {
byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT);
return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
} catch(Exception e){
return null;
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling in base64 decoding.

The current implementation silently returns null for any exception. Consider logging the error and handling specific exceptions.

     public static Bitmap decodeBase64EncodedBitmap(String base64Image){
+        if (base64Image == null) {
+            return null;
+        }
         try {
             byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT);
             return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
-        } catch(Exception e){
+        } catch(IllegalArgumentException e){
+            Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "Invalid base64 string: " + e.getMessage());
+            return null;
+        } catch(OutOfMemoryError e) {
+            Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "Out of memory decoding base64 bitmap: " + e.getMessage());
             return null;
         }
     }
📝 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
public static Bitmap decodeBase64EncodedBitmap(String base64Image){
try {
byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT);
return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
} catch(Exception e){
return null;
}
}
public static Bitmap decodeBase64EncodedBitmap(String base64Image){
if (base64Image == null) {
return null;
}
try {
byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT);
return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
} catch(IllegalArgumentException e){
Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "Invalid base64 string: " + e.getMessage());
return null;
} catch(OutOfMemoryError e) {
Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "Out of memory decoding base64 bitmap: " + e.getMessage());
return null;
}
}

Comment on lines +9 to +12
<androidx.camera.view.PreviewView
android:id="@+id/view_finder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add content description for accessibility.

The PreviewView should have a content description for screen readers.

     <androidx.camera.view.PreviewView
         android:id="@+id/view_finder"
         android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+        android:layout_height="match_parent"
+        android:contentDescription="@string/camera_preview_content_description" />
📝 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
<androidx.camera.view.PreviewView
android:id="@+id/view_finder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.camera.view.PreviewView
android:id="@+id/view_finder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/camera_preview_content_description" />

@avazirna
Copy link
Contributor Author

@coderabbitai full review

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (2)
app/src/org/commcare/utils/MediaUtil.java (1)

571-578: 🛠️ Refactor suggestion

Improve error handling in base64 decoding.

The current implementation silently returns null for any exception. Consider:

  1. Adding null check for input
  2. Handling specific exceptions separately
  3. Adding logging for debugging

Apply this diff to improve error handling:

-    public static Bitmap decodeBase64EncodedBitmap(String base64Image){
+    public static Bitmap decodeBase64EncodedBitmap(String base64Image) {
+        if (base64Image == null) {
+            return null;
+        }
         try {
             byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT);
             return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
-        } catch(Exception e){
+        } catch(IllegalArgumentException e) {
+            Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "Invalid base64 string: " + e.getMessage());
+            return null;
+        } catch(OutOfMemoryError e) {
+            Logger.log(LogTypes.TYPE_ERROR_ASSERTION, "Out of memory decoding base64 bitmap: " + e.getMessage());
             return null;
         }
     }
app/res/layout/micro_image_widget.xml (1)

9-12: 🛠️ Refactor suggestion

Add content description for accessibility.

The PreviewView should have a content description for screen readers.

Apply this diff to improve accessibility:

     <androidx.camera.view.PreviewView
         android:id="@+id/view_finder"
         android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+        android:layout_height="match_parent"
+        android:contentDescription="@string/camera_preview_content_description" />
🧹 Nitpick comments (12)
app/src/org/commcare/views/FaceCaptureView.java (3)

27-29: Consider moving interface out of the view class.
Defining the ImageStabilizedListener interface inside FaceCaptureView tightly couples the listener definition to this view. Consider extracting this interface to promote reusability across other face-capture-related components.


48-60: Parameterize default image dimensions.
Currently, DEFAULT_IMAGE_WIDTH and DEFAULT_IMAGE_HEIGHT are set to 480x640 or swapped based on orientation. In certain devices or use cases, these hard-coded defaults may be unsuitable. Consider making these values configurable via XML attributes or resource values.


129-138: Provide clearer null handling.
When updateFace(null) is called, the code simply resets the face. Clarify or document the scenario under which face detection yields a null face. This ensures future maintainers understand the intended flow for missed or lost face-tracking events.

app/src/org/commcare/views/widgets/MicroImageWidget.java (2)

33-37: Validate correct handling of existing base64 images.
When an existing Base64ImageData is found, the widget sets mBinary but does not preview it. If you'd like the user to see a preview in micro form, consider displaying the decoded and scaled image directly in the widget.


94-115: Refactor scaleImage for improved clarity.
The logic is functional, but consider extracting ratio calculations or including safety checks for edge cases (e.g., zero-width or zero-height images). More descriptive naming could enhance maintainability.

app/src/org/commcare/fragments/MicroImageActivity.java (2)

46-51: Consider adding state restoration.

The activity holds important state in member variables but doesn't implement onSaveInstanceState(). This could lead to state loss during configuration changes (e.g., rotation).

Add state restoration:

+private static final String KEY_INPUT_IMAGE = "input_image";
+
+@Override
+protected void onSaveInstanceState(@NonNull Bundle outState) {
+    super.onSaveInstanceState(outState);
+    if (inputImage != null) {
+        outState.putParcelable(KEY_INPUT_IMAGE, inputImage);
+    }
+}
+
+@Override
+protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+    super.onRestoreInstanceState(savedInstanceState);
+    if (savedInstanceState.containsKey(KEY_INPUT_IMAGE)) {
+        inputImage = savedInstanceState.getParcelable(KEY_INPUT_IMAGE);
+    }
+}

110-119: Consider adding configuration options for image analysis.

The image analyzer configuration is hardcoded. Consider making it configurable for different use cases.

Extract configuration to builder pattern:

+public static class Config {
+    private Size targetResolution;
+    private int targetRotation;
+    private ImageAnalysis.BackpressureStrategy backpressureStrategy = 
+        ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST;
+    
+    public static Config.Builder builder() {
+        return new Builder();
+    }
+    
+    public static class Builder {
+        private final Config config = new Config();
+        
+        public Builder setTargetResolution(Size resolution) {
+            config.targetResolution = resolution;
+            return this;
+        }
+        
+        public Config build() {
+            return config;
+        }
+    }
+}
app/src/org/commcare/views/widgets/MediaWidget.java (1)

359-361: LGTM! Consider adding user feedback.

The check for file existence prevents crashes, but users might benefit from feedback when media is missing.

Add user feedback:

     if (!mediaFile.exists()) {
+        Toast.makeText(context, 
+            StringUtils.getStringSpannableRobust(context, R.string.media_not_found),
+            Toast.LENGTH_SHORT).show();
         return;
     }
app/src/org/commcare/views/widgets/ImageWidget.java (2)

67-67: LGTM! Document protected members.

The change in access modifiers enables subclassing, but protected members should be documented for subclass implementers.

Add documentation:

+    /**
+     * Button for choosing images from gallery. Protected to allow customization in subclasses.
+     */
     protected final Button mChooseButton;

+    /**
+     * Name of the binary file containing the image. Protected to allow access in subclasses.
+     */
     protected String mBinaryName;

+    /**
+     * Interface for handling pending callouts. Protected to allow customization in subclasses.
+     */
     protected final PendingCalloutInterface pendingCalloutInterface;

Also applies to: 71-71, 78-78


247-247: LGTM! Consider adding @OverRide annotation.

The protected methods are intended for subclassing, but missing @OverRide annotations in subclasses could lead to subtle bugs.

Add documentation encouraging @OverRide:

+    /**
+     * Takes a picture using the device camera. Subclasses must use @Override when implementing.
+     */
     protected void takePicture() {

+    /**
+     * Deletes the current media file. Subclasses must use @Override when implementing.
+     */
     protected void deleteMedia() {

Also applies to: 273-273

app/assets/locales/android_translatable_strings.txt (1)

995-1001: Nitpick: Enhance Consistency and Clarity in Micro Image Error Messages
The new error message entries for the micro image widget are clear and follow a similar key format to existing messages. However, for improved clarity and consistency across messages, consider revising the wording and punctuation. For instance, the cropping error currently reads:

microimage.cropping.failed=Image cropping failed, make sure the target is inside the capture area

A suggested revision would be:

- microimage.cropping.failed=Image cropping failed, make sure the target is inside the capture area
+ microimage.cropping.failed=Image cropping failed. Please ensure the target is within the capture area.

Additionally, you might want to standardize the punctuation in the other messages so that each sentence ends with a period (if that matches the style used elsewhere in your locales). For example:

  • Consider changing

    microimage.decoding.no.image=Image processing failed. Please try again
    

    to

    microimage.decoding.no.image=Image processing failed. Please try again.
    
  • And similarly for

    microimage.scalingdown.compression.error=Image compression failed. Please try again
    

    to

    microimage.scalingdown.compression.error=Image compression failed. Please try again.
    

These refinements are optional but can help maintain a consistent tone and improve user understanding of the error prompts.

app/src/org/commcare/activities/FormEntryActivity.java (1)

361-362: LGTM! Consider adding documentation for the micro image capture case.

The combined case statement for handling both regular and micro image captures is clean and follows DRY principles. However, adding a brief comment explaining the micro image capture functionality would improve code maintainability.

Add a comment like:

+// Handle both regular and micro image captures using the same processing logic
 case FormEntryConstants.IMAGE_CAPTURE, FormEntryConstants.MICRO_IMAGE_CAPTURE:
     ImageCaptureProcessing.processCaptureResponse(this, FormEntryInstanceState.getInstanceFolder(), true);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8323e54 and 554cfcb.

📒 Files selected for processing (17)
  • app/AndroidManifest.xml (2 hunks)
  • app/assets/locales/android_translatable_strings.txt (1 hunks)
  • app/build.gradle (1 hunks)
  • app/res/layout/micro_image_widget.xml (1 hunks)
  • app/res/values/attrs.xml (1 hunks)
  • app/res/values/colors.xml (1 hunks)
  • app/res/values/strings.xml (1 hunks)
  • app/src/org/commcare/activities/FormEntryActivity.java (1 hunks)
  • app/src/org/commcare/activities/components/FormEntryConstants.java (1 hunks)
  • app/src/org/commcare/fragments/MicroImageActivity.java (1 hunks)
  • app/src/org/commcare/utils/MediaUtil.java (4 hunks)
  • app/src/org/commcare/views/FaceCaptureView.java (1 hunks)
  • app/src/org/commcare/views/widgets/ImageWidget.java (4 hunks)
  • app/src/org/commcare/views/widgets/MediaWidget.java (1 hunks)
  • app/src/org/commcare/views/widgets/MicroImageWidget.java (1 hunks)
  • app/src/org/commcare/views/widgets/WidgetFactory.java (1 hunks)
  • build.gradle (1 hunks)
🔇 Additional comments (17)
app/src/org/commcare/views/FaceCaptureView.java (2)

118-127: Evaluate the concurrency approach.
onDraw() uses synchronized(lock) to guard faceOvalGraphic, but concurrency control for other interactions (e.g., calls to updateFace()) may be incomplete. Validate whether multiple threads can modify faceOvalGraphic or access faceCaptureArea simultaneously, and ensure thread-safety is uniformly applied if so.


234-236: Clarify coordinate space consistency.
You draw the face oval using translated coordinates, but call the listener with the raw bounding box (currFace.getBoundingBox()). If the listener expects view coordinates, provide the translated bounding box. If it expects original image coordinates, confirm that the current usage is correct.

app/src/org/commcare/activities/components/FormEntryConstants.java (1)

24-24: Ensure the new request code is uniquely handled.
MICRO_IMAGE_CAPTURE is set to 17; verify there are no conflicts with other request codes. Also confirm that higher-level logic properly branches for micro versus standard image capture.

app/src/org/commcare/views/widgets/MicroImageWidget.java (2)

46-75: Add explicit OutOfMemoryError handling.
You catch generic Exception, but not OutOfMemoryError. A low-memory device could trigger OOM during bitmap operations. Handling OOM separately can improve clarity and facilitate targeted recovery or fallbacks.


25-27: Assess 2KB image limit.
MICRO_IMAGE_MAX_SIZE_BYTES is 2KB, which is highly restrictive and may result in extremely low-resolution images. Verify that this meets use-case requirements. If the images are rendered unusable, consider relaxing the limit or introducing separate settings.

app/src/org/commcare/views/widgets/WidgetFactory.java (1)

54-56:

❓ Verification inconclusive

LGTM! Consider adding validation.

The implementation correctly adds support for the new control type. However, consider validating that the required dependencies for MicroImageWidget are available.


🏁 Script executed:

#!/bin/bash
# Check if ML Kit dependencies are available in the project
rg -l "implementation.*'com.google.mlkit:face-detection'" 

Length of output: 65


Action: Verify ML Kit Dependency Inclusion

The new control type (MicroImageWidget) is correctly added in app/src/org/commcare/views/widgets/WidgetFactory.java (lines 54–56). However, please ensure that any required dependencies—such as the ML Kit face detection library (com.google.mlkit:face-detection)—are present in your build configuration. Manual verification of this dependency is recommended since the automated check did not return any output.

app/src/org/commcare/utils/MediaUtil.java (2)

519-539: LGTM! Proper bitmap handling and error handling.

The implementation correctly:

  • Validates the crop area
  • Handles bitmap recycling in a finally block
  • Includes proper error handling with descriptive messages

548-569: LGTM! Efficient compression with proper resource management.

The implementation correctly:

  • Uses try-with-resources for proper resource cleanup
  • Implements progressive quality reduction
  • Includes logging for monitoring compression cycles
  • Handles null input gracefully
app/src/org/commcare/activities/FormEntryActivity.java (2)

361-362: LGTM! Clean integration of micro image capture.

The changes efficiently handle both standard and micro image capture using the same processing method.


361-362:

✅ Verification successful

Verify micro image capture dependencies.

Please ensure that:

  1. The FormEntryConstants.MICRO_IMAGE_CAPTURE constant is properly defined.
  2. The ImageCaptureProcessing.processCaptureResponse method can handle both regular and micro image captures correctly.

🏁 Script executed:

#!/bin/bash
# Check FormEntryConstants for MICRO_IMAGE_CAPTURE definition
echo "Checking for MICRO_IMAGE_CAPTURE constant definition..."
rg "MICRO_IMAGE_CAPTURE\s*=\s*\d+" -A 2

# Check ImageCaptureProcessing implementation
echo "Checking ImageCaptureProcessing implementation..."
rg "class ImageCaptureProcessing" -A 10

Length of output: 1944


Micro Image Capture Dependencies Verified

  • Constant Verification: The FormEntryConstants.MICRO_IMAGE_CAPTURE is defined correctly (value 17).
  • Processing Logic: The ImageCaptureProcessing class exists and its processCaptureResponse method appears to be used identically for both regular and micro image captures without additional branching.

No further action is required.

app/res/layout/micro_image_widget.xml (1)

14-29: LGTM! Well-structured layout with proper weights.

The LinearLayout is properly configured with:

  • Vertical orientation
  • Center gravity
  • Proper weight distribution for the FaceCaptureView
app/res/values/attrs.xml (1)

56-61: New FaceCaptureView Attributes Added
The new <declare-styleable name="FaceCaptureView"> block correctly defines the custom attributes used by the view (i.e. background_color, face_capture_area_delimiter_color, face_marker_color, and countdown_text_size). Ensure that these attributes are consistently referenced in the layout (e.g. in micro_image_widget.xml) and that their formats (color and dimension) match your design expectations.

app/res/values/colors.xml (1)

89-89: New Semi-Transparent Color Added
The new color <color name="cc_neutral_bg_tr">#80D6D6D4</color> introduces a semi-transparent neutral background. Verify that its opacity and hue fit with the overall UI palette and that it is correctly applied in the new face capture widget.

app/build.gradle (1)

69-73: Addition of CameraX and ML Kit Face Detection Dependencies
The dependencies added for the CameraX libraries and ML Kit face detection (using $cameraX_version and the fixed version for ML Kit) are essential for the new micro image widget functionality. Please double-check that cameraX_version is defined appropriately elsewhere in the build script and that these versions are compatible with other parts of the application.

app/AndroidManifest.xml (2)

142-144: Meta-data for ML Kit Face Detection Integrated
The new <meta-data> element with android:name="com.google.mlkit.vision.DEPENDENCIES" and android:value="face" is correctly added. This entry informs ML Kit which vision modules to load. Verify that this configuration matches the requirements of the ML Kit version you are using.


324-327: New MicroImageActivity Declaration
The <activity> declaration for org.commcare.fragments.MicroImageActivity with android:windowSoftInputMode="adjustResize" is added properly. Ensure that the associated activity implementation correctly leverages the new camera preview and FaceCaptureView features defined in other parts of the PR.

app/res/values/strings.xml (1)

464-465: New String for Micro Image Activity Title
The string resource <string name="micro_image_activity_title" cc:translatable="true">Capture Face Picture</string> has been added. This will label the new activity appropriately. Confirm that all UI references to this resource correctly display the updated text and that it follows localization conventions.

Comment on lines +179 to +189
@Override
public void onImageStabilizedListener(Rect faceArea) {
try {
MediaUtil.cropAndSaveImage(inputImage, faceArea, ImageWidget.getTempFileForImageCapture());
setResult(AppCompatActivity.RESULT_OK);
finish();
} catch (Exception e) {
logErrorAndExit(e.getMessage(), "microimage.cropping.failed", e.getCause());
}

}
Copy link

@coderabbitai coderabbitai bot Feb 18, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add cleanup in onImageStabilizedListener.

The method should clean up resources after saving the image.

Add cleanup:

     @Override
     public void onImageStabilizedListener(Rect faceArea) {
         try {
             MediaUtil.cropAndSaveImage(inputImage, faceArea, ImageWidget.getTempFileForImageCapture());
             setResult(AppCompatActivity.RESULT_OK);
+            cleanup();
             finish();
         } catch (Exception e) {
+            cleanup();
             logErrorAndExit(e.getMessage(), "microimage.cropping.failed", e.getCause());
         }
     }
+    
+    private void cleanup() {
+        if (inputImage != null) {
+            inputImage.recycle();
+            inputImage = null;
+        }
+    }
📝 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
@Override
public void onImageStabilizedListener(Rect faceArea) {
try {
MediaUtil.cropAndSaveImage(inputImage, faceArea, ImageWidget.getTempFileForImageCapture());
setResult(AppCompatActivity.RESULT_OK);
finish();
} catch (Exception e) {
logErrorAndExit(e.getMessage(), "microimage.cropping.failed", e.getCause());
}
}
@Override
public void onImageStabilizedListener(Rect faceArea) {
try {
MediaUtil.cropAndSaveImage(inputImage, faceArea, ImageWidget.getTempFileForImageCapture());
setResult(AppCompatActivity.RESULT_OK);
cleanup();
finish();
} catch (Exception e) {
cleanup();
logErrorAndExit(e.getMessage(), "microimage.cropping.failed", e.getCause());
}
}
private void cleanup() {
if (inputImage != null) {
inputImage.recycle();
inputImage = null;
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

@avazirna is this not necessary ?

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment on lines 155 to 158
private void handleErrorDuringDetection(Exception e) {
Log.e(TAG, "Error during face detection: " + e);
Toast.makeText(this, "microimage.face.detection.mode.failed", Toast.LENGTH_LONG).show();
// TODO: decide whether to switch to manual mode or close activity
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Improve error handling in handleErrorDuringDetection.

The TODO comment indicates incomplete error handling. The method should make a clear decision about the failure mode.

Implement proper error handling:

     private void handleErrorDuringDetection(Exception e) {
         Log.e(TAG, "Error during face detection: " + e);
-        Toast.makeText(this, "microimage.face.detection.mode.failed", Toast.LENGTH_LONG).show();
-        // TODO: decide whether to switch to manual mode or close activity
+        if (e instanceof MlKitException) {
+            // ML Kit errors can be recovered by switching to manual mode
+            switchToManualMode();
+        } else {
+            // Other errors require activity closure
+            logErrorAndExit("Face detection failed", "microimage.face.detection.mode.failed", e);
+        }
     }
+    
+    private void switchToManualMode() {
+        // Implement manual mode logic here
+        Toast.makeText(this, "Switching to manual mode", Toast.LENGTH_LONG).show();
+    }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +132 to +152
@Override
public void analyze(@NonNull ImageProxy imageProxy) {
@SuppressLint("UnsafeOptInUsageError") Image mediaImage = imageProxy.getImage();
if (mediaImage != null) {
InputImage image = InputImage.fromMediaImage(mediaImage, imageProxy.getImageInfo().getRotationDegrees());

FaceDetectorOptions realTimeOpts = new FaceDetectorOptions.Builder()
.setContourMode(FaceDetectorOptions.CONTOUR_MODE_NONE)
.setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_ACCURATE)
.build();
FaceDetector faceDetector = FaceDetection.getClient(realTimeOpts);
// process image with the face detector
faceDetector.process(image)
.addOnSuccessListener(faces -> processFaceDetectionResult(faces, image))
.addOnFailureListener(e -> handleErrorDuringDetection(e))
.addOnCompleteListener(task -> {
imageProxy.close();
});
} else {
imageProxy.close();
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add error recovery mechanism in analyze method.

The image analysis could fail silently if there's an exception during processing. Consider adding retry logic or graceful degradation.

Add error recovery:

     @Override
     public void analyze(@NonNull ImageProxy imageProxy) {
+        int retryCount = 0;
+        final int MAX_RETRIES = 3;
+        
+        while (retryCount < MAX_RETRIES) {
             @SuppressLint("UnsafeOptInUsageError") Image mediaImage = imageProxy.getImage();
             if (mediaImage != null) {
                 try {
                     InputImage image = InputImage.fromMediaImage(mediaImage, 
                         imageProxy.getImageInfo().getRotationDegrees());
                     processImage(image, imageProxy);
+                    return;
+                } catch (Exception e) {
+                    retryCount++;
+                    if (retryCount == MAX_RETRIES) {
+                        handleErrorDuringDetection(e);
+                    }
+                }
             }
+        }
         imageProxy.close();
     }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 74 to 85
private void startCamera() throws ExecutionException, InterruptedException {
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);

cameraProviderFuture.addListener(() -> {
ProcessCameraProvider cameraProvider = null;
try {
cameraProvider = cameraProviderFuture.get();
} catch (ExecutionException | InterruptedException e) {
logErrorAndExit("Error acquiring camera provider", "microimage.camera.start.failed", e);
}
bindUseCases(cameraProvider);
}, ContextCompat.getMainExecutor(this));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential memory leak in camera listener.

The lambda passed to addListener captures this which could lead to a memory leak if the activity is destroyed before the future completes.

Use a weak reference or move the listener to a separate class:

-        cameraProviderFuture.addListener(() -> {
+        cameraProviderFuture.addListener(new Runnable() {
+            @Override
+            public void run() {
+                if (isDestroyed()) {
+                    return;
+                }
                 ProcessCameraProvider cameraProvider = null;
                 try {
                     cameraProvider = cameraProviderFuture.get();
                 } catch (ExecutionException | InterruptedException e) {
                     logErrorAndExit("Error acquiring camera provider", "microimage.camera.start.failed", e);
                 }
                 bindUseCases(cameraProvider);
+            }
+        }

Committable suggestion skipped: line range outside the PR's diff.

@@ -30,4 +30,5 @@ allprojects {
ext {
markwon_version = '4.6.2'
lifecycle_version = '2.5.1'
cameraX_version = '1.2.3'
Copy link

@coderabbitai coderabbitai bot Feb 18, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify CameraX version.

Let's check if version 1.2.3 is the latest stable version of CameraX.


🌐 Web query:

What is the latest stable version of CameraX?

💡 Result:

The latest stable version of CameraX is 1.4.0, released on October 30, 2024[1][5]. This version focuses on stability improvements and bug fixes from earlier release candidates (RCs), including critical issues like crashes during lifecycle binding and zoom control enhancements[1]. It also introduces foundational support for upcoming features like Jetpack Compose integration through the camera-compose library (currently in alpha as part of CameraX 1.5.0)[1][3].

Key details about CameraX 1.4.0:

  • Stability: Resolved crashes related to lifecycle management (bindToLifecycle with destroyed owners)[1]
  • Feature Support: Added mirror mode for Preview (Android 13+), expanded HDR capabilities (HDR Preview and Ultra HDR), and improved effect framework integration[3]
  • Compatibility: Part of AndroidX releases up to at least January 2025[5]

Developers can access this version via Google Maven repositories[1]. The next major version in development is CameraX 1.5.0-alpha05 (as of January 2025), which introduces experimental features like dual-camera support[9].

Citations:


Action: Update CameraX version in build.gradle

The current version is set to 1.2.3, but the latest stable release is now 1.4.0 (released on October 30, 2024). Please update the variable accordingly to ensure you benefit from the latest stability improvements and bug fixes.

  • File: build.gradle
  • Location: Line 33
  • Suggested Change:
  • cameraX_version = '1.2.3'
  • cameraX_version = '1.4.0'



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

`````suggestion
  cameraX_version = '1.4.0'

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Copy link

Choose a reason for hiding this comment

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

@shubham1g5 Great! Thanks for confirming the CameraX version update to 1.4.0. This will ensure your project benefits from the latest stability improvements and bug fixes.


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

@avazirna avazirna force-pushed the sc2975_create-micro-image-widget branch from 554cfcb to 03894d2 Compare February 18, 2025 10:54
@avazirna avazirna force-pushed the sc2975_create-micro-image-widget branch from 03894d2 to b74b2b2 Compare February 18, 2025 11:31
@avazirna avazirna marked this pull request as ready for review February 20, 2025 09:31
@avazirna avazirna requested a review from pm-dimagi February 20, 2025 09:31
@avazirna avazirna changed the title Create Micro Image widget Create Micro Image widget - 1/3 Feb 20, 2025
Copy link
Contributor

@shubham1g5 shubham1g5 left a comment

Choose a reason for hiding this comment

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

The Approach looks good overall but seems like there are some pending todos in the PR and lots of generic try catches which needs to be removed.

@@ -317,6 +321,10 @@
android:name="org.commcare.activities.FormRecordListActivity"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:name="org.commcare.fragments.MicroImageActivity"
android:windowSoftInputMode="adjustResize">
Copy link
Contributor

Choose a reason for hiding this comment

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

is this needed ?

Comment on lines +69 to +73
implementation "androidx.camera:camera-view:$cameraX_version"
implementation "androidx.camera:camera-core:$cameraX_version"
implementation "androidx.camera:camera-camera2:$cameraX_version"
implementation "androidx.camera:camera-lifecycle:$cameraX_version"
implementation 'com.google.android.gms:play-services-mlkit-face-detection:17.1.0'
Copy link
Contributor

Choose a reason for hiding this comment

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

what's the impact on apk size due to these additions ?

@@ -0,0 +1,190 @@
package org.commcare.fragments;
Copy link
Contributor

Choose a reason for hiding this comment

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

package seems incorrect given it's an activity

import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;

public class MicroImageActivity extends AppCompatActivity implements ImageAnalysis.Analyzer, FaceCaptureView.ImageStabilizedListener {
Copy link
Contributor

Choose a reason for hiding this comment

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

can we add a class level javadoc here.

}
}

private void startCamera() throws ExecutionException, InterruptedException {
Copy link
Contributor

Choose a reason for hiding this comment

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

we are both throwing and catching the exceptions in this method which seems un-necessary

Comment on lines +48 to +52
if (mBinaryName != null) {
deleteMedia();
}

File f = new File(binaryPath.toString());
Copy link
Contributor

Choose a reason for hiding this comment

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

can just call super.setBinaryData() instead ?

Comment on lines +54 to +58
if (originalImage == null) {
showToast("microimage.decoding.no.image");
Logger.log(LogTypes.TYPE_EXCEPTION,"Error decoding image ");
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

this almost certainly signifies a code error and should crash the application instead

mBinary = null;
}

// TODO: Refactor
Copy link
Contributor

Choose a reason for hiding this comment

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

pending ?

Also can we explore re-using FileUtil.getBitmapScaledByMaxDimen or MediaUtil.scaleDownToTargetOrContainer instead ?

Logger.exception("Error while scaling down and compressing image: ", e);
return;
} finally {
originalImage.recycle();
Copy link
Contributor

Choose a reason for hiding this comment

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

should not we also delete the original media file here ?

scaledDownBitmap = scaleImage(originalImage, IMAGE_DIMEN_SCALED_MAX_PX, IMAGE_DIMEN_SCALED_MAX_PX);
compressedBitmapByteArray = MediaUtil.compressBitmapToTargetSize(scaledDownBitmap, MICRO_IMAGE_MAX_SIZE_BYTES);
mBinary = Base64.encodeToString(compressedBitmapByteArray, Base64.DEFAULT);
} catch (Exception e) {
Copy link
Contributor

Choose a reason for hiding this comment

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

remove generic catch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants