Skip to content

[SAAS-19868] Add face capture widget#3780

Open
avazirna wants to merge 20 commits into
masterfrom
saas-19868_create-face-capture-widget
Open

[SAAS-19868] Add face capture widget#3780
avazirna wants to merge 20 commits into
masterfrom
saas-19868_create-face-capture-widget

Conversation

@avazirna

@avazirna avazirna commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Product Description

Adds a dedicated face capture widget for <upload mediatype="image/*" appearance="face"> questions. When a user reaches such a question, CommCare opens a guided face-capture screen instead of the standard image widget:

  • Auto capture via ML Kit face detection when Google Play Services is available; the user centers their face and holds still to trigger capture automatically.
  • Manual capture fallback (shutter button) when Play Services is unavailable.
  • A capture-mode indicator (green = auto, orange = manual) and on-screen instructions that adapt to the active mode.
  • Front/back camera lens switching.
Auto capture Manual capture
face_capture_auto_mode.mp4
face_capture_manual_mode.mp4

Technical Summary

Ticket: SAAS-19868

Face capture is selected purely from the appearance attribute on image-upload questions, so this PR is self-contained on the client side:

  • WidgetFactory routes the CONTROL_IMAGE_CHOOSE case to FaceCaptureWidget when appearance="face" (alongside the existing appearance="signature"SignatureWidget), defaulting to ImageWidget for null or unrecognized appearances.
  • FaceCaptureWidget launches MicroImageActivity and delivers the result to a temp file for form submission; wired through WidgetFactory, ImageWidget, FormEntryActivity, and FormEntryConstants.
  • MicroImageActivity gains capture-mode handling (auto/manual), front/back lens switching gated by the ALLOW_CAMERA_LENS_SWITCH_EXTRA intent extra, plus the instruction and mode-indicator UI.
  • New drawables, color selector, and strings for the camera controls and indicator.

Safety Assurance

Safety story

  • The new widget is only selected for image uploads carrying appearance="face"; all other upload questions (plain image/*, signature, audio, video, document) keep their current behavior.
  • The widget reuses the existing MicroImageActivity capture/output pipeline and add a route to save the image to a temp folder for processing;
  • These changes were successfully tested locally. These tests covered:
    • Auto-mode in both front and rear cameras;
    • Manual mode in both front and back cameras;
    • Connect user registration workflow was also covered

Automated test coverage

No automated coverage added — the change is UI/camera-centric (CameraX + ML Kit), which isn't exercised by the current instrumentation suite. Behavior was validated as described above.

Labels and Review

  • Do we need to enhance the manual QA test coverage ? If yes, RELEASES.md is updated accordingly
  • Does the PR introduce any major changes worth communicating ? If yes, RELEASES.md is updated accordingly
  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces FaceCaptureWidget, a new form widget that subclasses ImageWidget and launches MicroImageActivity configured for front-camera capture with temp-file output. ImageWidget fields (mChooseButton, mBinaryName, pendingCalloutInterface) and takePicture() are widened to protected to support the subclass. WidgetFactory gains a "face" appearance hint constant that dispatches to FaceCaptureWidget. A new FormEntryConstants.FACE_CAPTURE request code (17) is added, and FormEntryActivity routes it through the same ImageCaptureProcessing path as IMAGE_CAPTURE. MicroImageActivity is extended with camera lens selection, a lens-switch button, CaptureOutputMode enum (Base64/temp-file), and a refactored deliverResult() delivery pipeline. Supporting XML resources (layout, drawables, color selector, strings) are added.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • dimagi/commcare-android#3116: Introduces MicroImageActivity which this PR extends with lens switching, output mode routing, and new UI controls.

Suggested reviewers

  • Jignesh-dimagi
  • shubham1g5
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding a face capture widget.
Description check ✅ Passed The description includes all required sections and provides product, technical, safety, and testing details.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch saas-19868_create-face-capture-widget

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@avazirna avazirna changed the title Create face capture widget [SAAS-19868] Add face capture widget Jun 30, 2026
@avazirna avazirna marked this pull request as draft June 30, 2026 12:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/org/commcare/views/widgets/ImageWidget.java (1)

206-216: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the discard action available when preview reconstruction fails.

mBinaryName can already be set while toDisplay is missing (for example after cleanup or decryption/display-file issues). With mDiscardButton.setVisibility(View.VISIBLE) gated by toDisplay.exists(), the widget shows a stale answer but removes the normal way to clear it.

Suggested fix
             if (toDisplay.exists()) {
                 Bitmap bmp = MediaUtil.getBitmapScaledToContainer(toDisplay,
                         screenHeight, screenWidth);
                 if (bmp == null) {
                     mErrorTextView.setVisibility(View.VISIBLE);
                 }
                 mImageView.setImageBitmap(bmp);
-                mDiscardButton.setVisibility(View.VISIBLE);
             } else {
                 mImageView.setImageBitmap(null);
             }
+            mDiscardButton.setVisibility(View.VISIBLE);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/org/commcare/views/widgets/ImageWidget.java` around lines 206 - 216,
The discard button visibility in ImageWidget’s preview update flow is
incorrectly tied to the reconstructed file existing, which hides the user’s only
way to clear a stale binary when `mBinaryName` is still set but `toDisplay` is
missing. Update the logic around the `toDisplay.exists()` check in `ImageWidget`
so `mDiscardButton.setVisibility(View.VISIBLE)` still runs whenever an
attachment is present, even if `MediaUtil.getBitmapScaledToContainer()` returns
null or the file is absent, while keeping the image preview handling separate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/org/commcare/fragments/MicroImageActivity.java`:
- Around line 421-423: The capture output mode parsing in getCaptureOutputMode()
can still crash on unexpected intent values because
CaptureOutputMode.valueOf(...) throws for unknown strings. Update
MicroImageActivity’s getCaptureOutputMode() to treat invalid or stale
CAPTURE_OUTPUT_MODE_EXTRA values the same as a missing extra by falling back to
DEFAULT_CAPTURE_OUTPUT_MODE, using the existing CaptureOutputMode and
CAPTURE_OUTPUT_MODE_EXTRA symbols to locate the logic.
- Around line 138-142: Gate the camera lens switch UI and initial camera binding
on actual lens availability. In MicroImageActivity, use
ProcessCameraProvider.hasCamera(...) before the first bindToLifecycle(...) call
to verify the selected CameraSelector exists, and only show/enable the
switchCameraLensButton when the alternate lens is present. Keep
getAllowCameraLensSwitch() as the feature flag, but prevent exposing a selector
that the device cannot satisfy.

---

Outside diff comments:
In `@app/src/org/commcare/views/widgets/ImageWidget.java`:
- Around line 206-216: The discard button visibility in ImageWidget’s preview
update flow is incorrectly tied to the reconstructed file existing, which hides
the user’s only way to clear a stale binary when `mBinaryName` is still set but
`toDisplay` is missing. Update the logic around the `toDisplay.exists()` check
in `ImageWidget` so `mDiscardButton.setVisibility(View.VISIBLE)` still runs
whenever an attachment is present, even if
`MediaUtil.getBitmapScaledToContainer()` returns null or the file is absent,
while keeping the image preview handling separate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 38330e7d-a48e-4049-9157-3942b520a27e

📥 Commits

Reviewing files that changed from the base of the PR and between 49350c5 and 97892b5.

📒 Files selected for processing (14)
  • app/assets/locales/android_translatable_strings.txt
  • app/res/color/face_capture_indicator_selector.xml
  • app/res/drawable/capture_mode_indicator.xml
  • app/res/drawable/ic_camera_shutter.xml
  • app/res/drawable/ic_switch_camera_lens.xml
  • app/res/drawable/instruction_hand.xml
  • app/res/layout/micro_image_widget.xml
  • app/res/values/strings.xml
  • app/src/org/commcare/activities/FormEntryActivity.java
  • app/src/org/commcare/activities/components/FormEntryConstants.java
  • app/src/org/commcare/fragments/MicroImageActivity.java
  • app/src/org/commcare/views/widgets/FaceCaptureWidget.kt
  • app/src/org/commcare/views/widgets/ImageWidget.java
  • app/src/org/commcare/views/widgets/WidgetFactory.java

Comment on lines +138 to 142
if (getAllowCameraLensSwitch()) {
cameraControlsContainer.setVisibility(VISIBLE);
switchCameraLensButton.setVisibility(VISIBLE);
switchCameraLensButton.setOnClickListener(v -> switchCameraLensFacing());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Gate camera selection on available lenses.
ALLOW_CAMERA_LENS_SWITCH_EXTRA can expose a selector the device doesn’t support, and bindToLifecycle(...) will fail when the chosen CameraSelector has no match. Check ProcessCameraProvider.hasCamera(...) before the initial bind and disable the switch when the alternate lens is missing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/org/commcare/fragments/MicroImageActivity.java` around lines 138 -
142, Gate the camera lens switch UI and initial camera binding on actual lens
availability. In MicroImageActivity, use ProcessCameraProvider.hasCamera(...)
before the first bindToLifecycle(...) call to verify the selected CameraSelector
exists, and only show/enable the switchCameraLensButton when the alternate lens
is present. Keep getAllowCameraLensSwitch() as the feature flag, but prevent
exposing a selector that the device cannot satisfy.

Comment on lines +421 to +423
private CaptureOutputMode getCaptureOutputMode() {
return CaptureOutputMode.valueOf(Objects.requireNonNullElse(getIntent().getStringExtra(CAPTURE_OUTPUT_MODE_EXTRA), DEFAULT_CAPTURE_OUTPUT_MODE));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Fall back on unknown capture-mode values instead of crashing.

CaptureOutputMode.valueOf(...) throws on any unexpected string. Since this activity already has a default for a missing extra, stale or mistyped callers should fall back to DEFAULT_CAPTURE_OUTPUT_MODE here too rather than crashing before the image is returned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/org/commcare/fragments/MicroImageActivity.java` around lines 421 -
423, The capture output mode parsing in getCaptureOutputMode() can still crash
on unexpected intent values because CaptureOutputMode.valueOf(...) throws for
unknown strings. Update MicroImageActivity’s getCaptureOutputMode() to treat
invalid or stale CAPTURE_OUTPUT_MODE_EXTRA values the same as a missing extra by
falling back to DEFAULT_CAPTURE_OUTPUT_MODE, using the existing
CaptureOutputMode and CAPTURE_OUTPUT_MODE_EXTRA symbols to locate the logic.

@avazirna avazirna force-pushed the saas-19868_create-face-capture-widget branch from 97892b5 to 07c70bd Compare June 30, 2026 22:01
@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 26.37%. Comparing base (e51dabf) to head (09c1642).
⚠️ Report is 4 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3780      +/-   ##
============================================
- Coverage     26.38%   26.37%   -0.02%     
- Complexity     4522     4526       +4     
============================================
  Files           969      971       +2     
  Lines         57973    58025      +52     
  Branches       6898     6904       +6     
============================================
+ Hits          15296    15303       +7     
- Misses        40802    40848      +46     
+ Partials       1875     1874       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@avazirna avazirna force-pushed the saas-19868_create-face-capture-widget branch from 07c70bd to d139a1f Compare July 1, 2026 08:53
@avazirna avazirna force-pushed the saas-19868_create-face-capture-widget branch from 544445b to f7c0649 Compare July 7, 2026 16:55
@avazirna avazirna force-pushed the saas-19868_create-face-capture-widget branch from 4298152 to be79650 Compare July 8, 2026 09:19
@avazirna avazirna marked this pull request as ready for review July 8, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant