Skip to content

Conversation

@hashamyounis9
Copy link
Contributor

/claim #2689

Product Description

This PR ensures that EXIF metadata (like orientation, timestamp, GPS, etc.) is preserved when an image is scaled down in the CommCare Android app. This improves user experience in workflows that rely on image metadata.

Technical Summary

Passthrough EXIF metadata while scaling images

Modified the image scaling logic to preserve EXIF metadata by copying it from the original image to the scaled-down version as can be seen in the following screenshot

Screenshot from 2025-04-06 18-04-38

Values on the left side of -> are coming from original image and those on the right side are coming from the scaled image.

Feature Flag

Not behind a feature flag.

Safety Assurance

Safety story

  • The changes were tested locally with various images containing different EXIF metadata.
  • Verified that the metadata is retained correctly after scaling, and that existing image handling workflows remain unaffected.
  • The change is safe because it does not modify the core image input/output logic, only appends metadata after scaling.

Automated test coverage

  • No new unit tests added due to the nature of the change (image files), but tested locally with debug logs to verify correct metadata retention.

QA Plan

  • A manual QA test is recommended using images with EXIF metadata (e.g., orientation, GPS location).
  • Verify that the metadata is preserved in the scaled version.
  • No regression expected in image capture or submission workflows.

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

@damagatchi
Copy link

Can one of the admins verify this patch?

@coderabbitai
Copy link

coderabbitai bot commented Apr 6, 2025

📝 Walkthrough

Walkthrough

This change introduces two new private methods within the FileUtil class: copyExifData and logExifAttributesToCheckRetention. The copyExifData method iterates through a list of predefined EXIF tags, retrieves the corresponding values from a source ExifInterface, and sets these values on a destination ExifInterface while adjusting image dimensions using the provided Bitmap. The logExifAttributesToCheckRetention method logs the EXIF attributes from both the source and destination objects to verify metadata retention. In addition, the scaleAndSaveImage method has been updated to read the original EXIF data before scaling the image, apply the metadata transfer to the scaled image, and handle exceptions appropriately during the EXIF data processing.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Caller
    participant FileUtil as FileUtil
    participant SrcExif as Source ExifInterface
    participant DestExif as Dest ExifInterface

    Caller->>FileUtil: call scaleAndSaveImage(imageFile, Bitmap)
    FileUtil->>SrcExif: read original EXIF data from image file
    FileUtil->>FileUtil: scale image and save scaled file
    FileUtil->>DestExif: create ExifInterface for scaled file
    FileUtil->>FileUtil: call copyExifData(SrcExif, DestExif, Bitmap)
    FileUtil->>DestExif: update with selected EXIF attributes
    FileUtil->>FileUtil: call logExifAttributesToCheckRetention(SrcExif, DestExif)
    FileUtil->>Caller: return success or failure status
Loading

Possibly related issues

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 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.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @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: 0

🧹 Nitpick comments (3)
app/src/org/commcare/utils/FileUtil.java (3)

625-642: Consider refactoring duplicated EXIF tag list.

The EXIF tags list is duplicated in both methods. Consider defining this list once as a private static final field to improve maintainability.

+    private static final String[] EXIF_TAGS = {
+            ExifInterface.TAG_DATETIME,
+            ExifInterface.TAG_MAKE,
+            ExifInterface.TAG_MODEL,
+            ExifInterface.TAG_ORIENTATION,
+            ExifInterface.TAG_GPS_LATITUDE,
+            ExifInterface.TAG_GPS_LONGITUDE,
+            ExifInterface.TAG_GPS_LATITUDE_REF,
+            ExifInterface.TAG_GPS_LONGITUDE_REF,
+            ExifInterface.TAG_FLASH,
+            ExifInterface.TAG_EXPOSURE_TIME,
+    };

     private static void copyExifData(ExifInterface sourceExif, ExifInterface destExif, Bitmap scaledBitmap) {
-        String[] exifTags = {
-                ExifInterface.TAG_DATETIME,
-                ExifInterface.TAG_MAKE,
-                ExifInterface.TAG_MODEL,
-                ExifInterface.TAG_ORIENTATION,
-                ExifInterface.TAG_GPS_LATITUDE,
-                ExifInterface.TAG_GPS_LONGITUDE,
-                ExifInterface.TAG_GPS_LATITUDE_REF,
-                ExifInterface.TAG_GPS_LONGITUDE_REF,
-                ExifInterface.TAG_FLASH,
-                ExifInterface.TAG_EXPOSURE_TIME,
-        };
-
-        for (String tag : exifTags) {
+        for (String tag : EXIF_TAGS) {

And similar change in the logExifAttributesToCheckRetention method.


640-640: Consider using class-specific log tag.

The generic "ImageTest" log tag could make it difficult to filter logs. Consider using LOG_TOKEN which is already defined for this class.

-            Log.i("ImageTest", tag + " : " + src.getAttribute(tag) + " -> " + dest.getAttribute(tag));
+            Log.i(LOG_TOKEN, tag + " : " + src.getAttribute(tag) + " -> " + dest.getAttribute(tag));

673-682: Consider conditional logging for production code.

Line 681 has a comment suggesting this is logging that might need to be removed. Instead of requiring code modification, consider using conditional logging based on a debug flag.

-                logExifAttributesToCheckRetention(originalExif, newExif); // comment this line if logging is not required
+                if (BuildConfig.DEBUG) {
+                    logExifAttributesToCheckRetention(originalExif, newExif);
+                }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa1462 and 3a1675a.

📒 Files selected for processing (1)
  • app/src/org/commcare/utils/FileUtil.java (3 hunks)
🔇 Additional comments (4)
app/src/org/commcare/utils/FileUtil.java (4)

10-10: Import addition looks good.

Adding the androidx ExifInterface import is appropriate for working with EXIF metadata in images.


599-623: Well-implemented EXIF data copying.

The method effectively copies important EXIF metadata from source to destination while also updating image dimensions appropriately. The implementation handles a comprehensive set of EXIF tags including datetime, location, and camera metadata.


658-664: Good error handling for EXIF reading.

The code correctly handles potential exceptions when reading EXIF data, ensuring the method gracefully returns false if there are issues accessing the metadata.


673-682: Functionality looks good.

The implementation successfully preserves EXIF metadata during image scaling by reading the original data, saving the scaled image, and then applying the metadata to the new file.

@shubham1g5
Copy link
Contributor

@damagatchi ok to test

@hashamyounis9
Copy link
Contributor Author

hashamyounis9 commented Apr 7, 2025

@shubham1g5 @damagatchi

seeing an "Access Denied" error while trying to open the jenkins job, already logged in but don't have permission to view it. Could you please help or grant access if needed?

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.

Hello, thanks for submitting a PR.

Unfortunately our Jenkins Jobs are restricted, but this is the error we are getting for you reference -

FileUtil.java:10: error: package androidx.exifinterface.media does not exist

Also in terms of testing this, can you make sure all these following cases are working correctly -

  1. Basic EXIF Check

Snap a pic with GPS turned on.
Save the form.
Check that all EXIF data is intact using your favorite EXIF viewer.

  1. Scaled Image Test

Take a high-res image.
Verify the EXIF data after it gets scaled down.

  1. Encrypted Image Test

Capture a GPS-tagged image.
Confirm EXIF data survived the encryption.

  1. Edge Cases

Use images with no EXIF data.
Test images from various sources (camera, gallery, etc.).
Try different formats (JPG, PNG).

try {
originalExif = new ExifInterface(originalImage.getAbsolutePath());
} catch (IOException e) {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Think we don't want to retreat our normal functionality due to errors with exif manipulation. As such we should not return from here but just log the exceptions using Logger.exception

@hashamyounis9
Copy link
Contributor Author

@shubham1g5

2c17817 resolves the package not found issue, updates the EXIF_TAGS list as asked, also retains the default functionality of keep going if the exif properties were not read.

Here is what we are capturing now with

GPS on:
gps-on

GPS off:
gps-off

@shubham1g5
Copy link
Contributor

thanks @hashamyounis9 , can you confirm the test cases I outlined above as well ?

@hashamyounis9
Copy link
Contributor Author

@shubham1g5

also can you please update me with error in jenkins job?

@hashamyounis9
Copy link
Contributor Author

thanks @hashamyounis9 , can you confirm the test cases I outlined above as well ?

Sure, I'll do in a bit...

@hashamyounis9
Copy link
Contributor Author

hashamyounis9 commented Apr 7, 2025

@shubham1g5

confirming the mentioned cases, it works well in the situations mentioned above, here are some catches

  • PNG format does not support EXIF metadata, so it will return nulls with PNG images, but that isn't a problem because android phones by default use jpg format, with which it works seamlessly.

  • Also when choosing images from gallery, some of the GPS attributes are not provided as it is most probably due to restriction of android allowing for media and restricts some properties while sharing data (BTW this is out of the scope of this issue/PR as it is only responsible for copying available EXIF properties from original image to scaled one.)

@shubham1g5
Copy link
Contributor

Got it, that makes sense to me. However we must make sure that the exif data survives the image encryption. Have you been able to check on that ?

@hashamyounis9
Copy link
Contributor Author

hashamyounis9 commented Apr 7, 2025

@shubham1g5

can you please help me understanding when does the encryption trigger, here is what I did:

I started filling the form and captured the image using camera and completed the form and form got sent to the server. During all this encryption didn't occur as I did not see the debug logs that I put in the encryption method.

@hashamyounis9
Copy link
Contributor Author

hashamyounis9 commented Apr 7, 2025

@shubham1g5

For the sake of testing if the EXIF metadata was preserved or not in the images that are sent to the server, I exported the form media from the CommCare HQ for my app sent by mobile worker and viewed the EXIF metadata using some online tool making sure that all the EXIF metadata was present in the images that were sent to the server!

@shubham1g5
Copy link
Contributor

Ahh Sorry, that might have been incorrect path, Curious what does this check returns for you ? And if true does the underlining encryption method gets called for you ?

@hashamyounis9
Copy link
Contributor Author

hashamyounis9 commented Apr 7, 2025

Ahh Sorry, that might have been incorrect path, Curious what does this check returns for you ? And if true does the underlining encryption method gets called for you ?

So I've checked it and the check you mentioned returns true for me and the underlying encryption method gets called, which essentially means that encryption is getting applied and the EXIF metadata survives the encryption, as the images exported from the CommCare HQ contains the retained EXIF metadata!

@shubham1g5
Copy link
Contributor

@hashamyounis9 that's great, Is it possible for you to provide a commcare hq link to one such form with image here. Thanks!

@damagatchi retest this please

@hashamyounis9
Copy link
Contributor Author

hashamyounis9 commented Apr 7, 2025

@hashamyounis9 that's great, Is it possible for you to provide a commcare hq link to one such form with image here. Thanks!

@damagatchi retest this please

@shubham1g5 you mean qr code for the app?

I'll need to share the qr code and create a user(mobile worker) for you and then share the credentials for that user, the form will be available in the app

@shubham1g5
Copy link
Contributor

you mean qr code for the app?

no I meant the link to the form itself where you uploaded the image.

@hashamyounis9
Copy link
Contributor Author

you mean qr code for the app?

no I meant the link to the form itself where you uploaded the image.

oh, can you guide me how to share this?

@shubham1g5
Copy link
Contributor

@damagatchi retest this please

@shubham1g5
Copy link
Contributor

shubham1g5 commented Apr 8, 2025

@hashamyounis9 Can you provide the link to the final image you are seeing on HQ. You can just take the link from the browser address bar where you see the image and paste it here or upload the image here itself or on Google drive, somewhere I can take the image from and verify the exif attributes on my end.

@hashamyounis9
Copy link
Contributor Author

@hashamyounis9 Can you provide the link to the final image you are seeing on HQ. You can just take the link from the browser address bar where you see the image and paste it here or upload the image here itself or on Google drive, somewhere I can take the image from and verify the exif attributes on my end.

Here is the zip exported from CommCare HQ, with multimedia only

I used this tool to view exif metadata

@shubham1g5
Copy link
Contributor

@hashamyounis9 thanks! let me verify it. Also we are facing an unrealted error with our CI job and I am looking into it.
@damagatchi retest this please

shubham1g5
shubham1g5 previously approved these changes Apr 8, 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.

one minor omition request, but all things look good otherwise.

@hashamyounis9
Copy link
Contributor Author

@shubham1g5

b9dc391 removes the logs

@shubham1g5 shubham1g5 added the skip-integration-tests Skip android tests. label Apr 8, 2025
@shubham1g5
Copy link
Contributor

@hashamyounis9 we just merged an unrelated fix for the test failure that's causing the job to fail here, would you be able to rebase your branch from our master so that we can get the build passing here.

@hashamyounis9
Copy link
Contributor Author

hashamyounis9 commented Apr 8, 2025

@shubham1g5

rebase done with master, please let me know if it resolves the issue.

@shubham1g5 shubham1g5 merged commit 096deed into dimagi:master Apr 8, 2025
2 checks passed
@hashamyounis9
Copy link
Contributor Author

@shubham1g5

Thanks for your guidance and support along the way! ❤️

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.

3 participants