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

[feat] Support scoped storage when sharing images #148 #149

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

tuancoltech
Copy link
Member

@tuancoltech tuancoltech commented Feb 25, 2025

Issue Number

Purpose

  • Support scoped storage when sharing image files
  • Upgrade compileSdk/targetSdk to 35

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features
    • Updated the Android platform support for enhanced performance and compatibility.
    • Improved image sharing and file access, enabling secure and reliable media handling.
    • Introduced enhanced configurations that streamline image file management for smoother integration with external applications.
    • Added a new method for retrieving image files based on image IDs.
    • Introduced a new XML configuration for defining image file paths.

@tuancoltech tuancoltech self-assigned this Feb 25, 2025
@tuancoltech tuancoltech requested a review from a team as a code owner February 25, 2025 07:17
Copy link

coderabbitai bot commented Feb 25, 2025

Walkthrough

This pull request updates the project’s Android SDK configuration and enhances image file handling. The build configuration now targets SDK version 35 instead of 34. The ImageContentProvider is reconfigured in the AndroidManifest.xml to allow URI permissions, supported by a new XML file defining file paths. Additionally, a new method is introduced in the ImageContentProvider to open image files by validating the URI, querying for the image, and returning a file descriptor. An overloaded method in FileHelper enables retrieval of files using an Image object with necessary null checks.

Changes

File(s) Change Summary
app/build.gradle Updated compileSdk and targetSdkVersion from 34 to 35.
app/src/main/AndroidManifest.xml, app/src/main/res/xml/image_file_path.xml Modified ImageContentProvider configuration: added android:grantUriPermissions attribute, nested <meta-data> element for file provider paths, and introduced a new XML file defining the image file storage path.
app/src/main/java/ai/elimu/content_provider/provider/ImageContentProvider.java Added a new openFile(Uri uri, String mode) method that validates the URI, queries for an Image, retrieves the corresponding file, and returns a ParcelFileDescriptor; also added necessary imports and a logging TAG.
app/src/main/java/ai/elimu/content_provider/util/FileHelper.java Introduced an overloaded getImageFile(Image image, Context context) method with null checks and @RestrictTo annotation to support file retrieval for an Image object.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ImageContentProvider
    participant ImageDao
    participant FileHelper
    participant FS as File System

    Client->>ImageContentProvider: openFile(uri, mode)
    ImageContentProvider->>ImageContentProvider: Validate URI segments
    ImageContentProvider->>ImageDao: Query Image using file ID
    ImageDao-->>ImageContentProvider: Return Image object
    ImageContentProvider->>FileHelper: getImageFile(image, context)
    FileHelper->>FS: Locate file based on image properties
    FS-->>FileHelper: File exists or not
    FileHelper-->>ImageContentProvider: Return File or null
    alt File exists
        ImageContentProvider->>Client: Return ParcelFileDescriptor (read-only)
    else File not found
        ImageContentProvider->>Client: Throw FileNotFoundException
    end
Loading
✨ 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: 2

🧹 Nitpick comments (3)
app/src/main/java/ai/elimu/content_provider/provider/ImageContentProvider.java (3)

32-32: TAG should be private static final.

Following Android logging conventions, the TAG variable should be declared as private static final for consistency and to enable compiler optimizations.

-    private String TAG = ImageContentProvider.class.getName();
+    private static final String TAG = ImageContentProvider.class.getName();

160-181: Consider the mode parameter.

The openFile method accepts a mode parameter but always uses MODE_READ_ONLY regardless of the provided mode. Either document this behavior or consider supporting different modes based on the parameter value.

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+        // This implementation only supports read-only mode regardless of the mode parameter
        List<String> segments = uri.getPathSegments();
        if (segments.size() < 2) {
            throw new FileNotFoundException("Invalid URI: " + uri);
        }
        String fileId = segments.get(1);

        RoomDb roomDb = RoomDb.getDatabase(getContext());
        ImageDao imageDao = roomDb.imageDao();
        Image image = imageDao.load(Long.parseLong(fileId));
        
        if (image == null) {
            Log.e(TAG, "Image not found with ID: " + fileId);
            throw new FileNotFoundException("Image not found with ID: " + fileId);
        }

        File imageFile = FileHelper.getImageFile(image, getContext());
        if (imageFile == null) {
            throw new FileNotFoundException("File not found!");
        }
        if (!imageFile.exists()) {
            Log.e(TAG, "imageFile doesn't exist: " + imageFile.getAbsolutePath());
            throw new FileNotFoundException("File not found: " + imageFile.getAbsolutePath());
        }
        return ParcelFileDescriptor.open(imageFile, ParcelFileDescriptor.MODE_READ_ONLY);
    }

164-165: Add a more specific error message.

The error message for invalid URI should indicate what the expected format is, to help with debugging.

    if (segments.size() < 2) {
-        throw new FileNotFoundException("Invalid URI: " + uri);
+        throw new FileNotFoundException("Invalid URI: " + uri + ". Expected format: content://" + AUTHORITY + "/" + TABLE_IMAGES + "/{id}");
    }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a2ef20c and 66f20ed.

📒 Files selected for processing (5)
  • app/build.gradle (1 hunks)
  • app/src/main/AndroidManifest.xml (1 hunks)
  • app/src/main/java/ai/elimu/content_provider/provider/ImageContentProvider.java (3 hunks)
  • app/src/main/java/ai/elimu/content_provider/util/FileHelper.java (2 hunks)
  • app/src/main/res/xml/image_file_path.xml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/res/xml/image_file_path.xml
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build (windows-latest, 21)
  • GitHub Check: build (windows-latest, 17)
🔇 Additional comments (3)
app/build.gradle (1)

5-5: Android SDK version update looks good.

Updating to target and compile with SDK 35 is an appropriate change to ensure the app remains current with the latest Android platform capabilities, which is necessary for supporting modern storage access methods like scoped storage.

Also applies to: 11-11

app/src/main/AndroidManifest.xml (1)

55-60: Properly configured for URI-based file sharing.

The changes to enable grantUriPermissions and define file paths via the FILE_PROVIDER_PATHS meta-data are necessary for supporting scoped storage access. This configuration follows Android's recommended patterns for secure file sharing.

app/src/main/java/ai/elimu/content_provider/util/FileHelper.java (1)

30-39: Good implementation with proper scope restrictions.

The new overloaded method maintains the same structure and null checks as the original method. The use of @RestrictTo annotation is a good practice to control access to this method.


RoomDb roomDb = RoomDb.getDatabase(getContext());
ImageDao imageDao = roomDb.imageDao();
Image image = imageDao.load(Long.parseLong(fileId));
Copy link

@coderabbitai coderabbitai bot Feb 25, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Validate fileId to avoid NumberFormatException.

The code is trying to parse a potentially non-numeric string to a Long, which could throw a NumberFormatException. Add validation or exception handling.

    String fileId = segments.get(1);

+    long imageId;
+    try {
+        imageId = Long.parseLong(fileId);
+    } catch (NumberFormatException e) {
+        Log.e(TAG, "Failed to parse image ID: " + fileId, e);
+        throw new FileNotFoundException("Invalid image ID format: " + fileId);
+    }

    RoomDb roomDb = RoomDb.getDatabase(getContext());
    ImageDao imageDao = roomDb.imageDao();
-    Image image = imageDao.load(Long.parseLong(fileId));
+    Image image = imageDao.load(imageId);
📝 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
Image image = imageDao.load(Long.parseLong(fileId));
String fileId = segments.get(1);
long imageId;
try {
imageId = Long.parseLong(fileId);
} catch (NumberFormatException e) {
Log.e(TAG, "Failed to parse image ID: " + fileId, e);
throw new FileNotFoundException("Invalid image ID format: " + fileId);
}
RoomDb roomDb = RoomDb.getDatabase(getContext());
ImageDao imageDao = roomDb.imageDao();
Image image = imageDao.load(imageId);

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai This was fixed!

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@tuancoltech Thank you for implementing the fix! I've verified that the NumberFormatException is now properly handled with the try-catch block. This is a good improvement to make the code more robust against invalid inputs. Well done!


🏁 Script executed:

#!/bin/bash
# Check the current implementation of the openFile method in the ImageContentProvider class
cat app/src/main/java/ai/elimu/content_provider/provider/ImageContentProvider.java | grep -A 30 "public ParcelFileDescriptor openFile"

Length of output: 1425


@tuancoltech Thank you for implementing the fix! I've verified that the NumberFormatException is now properly handled with the try-catch block. This is a good improvement to make the code more robust against invalid inputs. Well done!


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

Comment on lines 160 to 181
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
if (segments.size() < 2) {
throw new FileNotFoundException("Invalid URI: " + uri);
}
String fileId = segments.get(1);

RoomDb roomDb = RoomDb.getDatabase(getContext());
ImageDao imageDao = roomDb.imageDao();
Image image = imageDao.load(Long.parseLong(fileId));

File imageFile = FileHelper.getImageFile(image, getContext());
if (imageFile == null) {
throw new FileNotFoundException("File not found!");
}
if (!imageFile.exists()) {
Log.e(TAG, "imageFile doesn't exist: " + imageFile.getAbsolutePath());
throw new FileNotFoundException("File not found: " + imageFile.getAbsolutePath());
}
return ParcelFileDescriptor.open(imageFile, ParcelFileDescriptor.MODE_READ_ONLY);
}
Copy link

@coderabbitai coderabbitai bot Feb 25, 2025

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 Image object to prevent potential NullPointerException.

If the image is not found in the database, a NullPointerException will occur when calling FileHelper.getImageFile(image, getContext()). Add a null check before accessing the image.

    Image image = imageDao.load(Long.parseLong(fileId));

+    if (image == null) {
+        Log.e(TAG, "Image not found with ID: " + fileId);
+        throw new FileNotFoundException("Image not found with ID: " + fileId);
+    }
    
    File imageFile = FileHelper.getImageFile(image, getContext());
📝 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 ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
if (segments.size() < 2) {
throw new FileNotFoundException("Invalid URI: " + uri);
}
String fileId = segments.get(1);
RoomDb roomDb = RoomDb.getDatabase(getContext());
ImageDao imageDao = roomDb.imageDao();
Image image = imageDao.load(Long.parseLong(fileId));
File imageFile = FileHelper.getImageFile(image, getContext());
if (imageFile == null) {
throw new FileNotFoundException("File not found!");
}
if (!imageFile.exists()) {
Log.e(TAG, "imageFile doesn't exist: " + imageFile.getAbsolutePath());
throw new FileNotFoundException("File not found: " + imageFile.getAbsolutePath());
}
return ParcelFileDescriptor.open(imageFile, ParcelFileDescriptor.MODE_READ_ONLY);
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
if (segments.size() < 2) {
throw new FileNotFoundException("Invalid URI: " + uri);
}
String fileId = segments.get(1);
RoomDb roomDb = RoomDb.getDatabase(getContext());
ImageDao imageDao = roomDb.imageDao();
Image image = imageDao.load(Long.parseLong(fileId));
if (image == null) {
Log.e(TAG, "Image not found with ID: " + fileId);
throw new FileNotFoundException("Image not found with ID: " + fileId);
}
File imageFile = FileHelper.getImageFile(image, getContext());
if (imageFile == null) {
throw new FileNotFoundException("File not found!");
}
if (!imageFile.exists()) {
Log.e(TAG, "imageFile doesn't exist: " + imageFile.getAbsolutePath());
throw new FileNotFoundException("File not found: " + imageFile.getAbsolutePath());
}
return ParcelFileDescriptor.open(imageFile, ParcelFileDescriptor.MODE_READ_ONLY);
}

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai Isn't the null check already there?

Copy link

Choose a reason for hiding this comment

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

@tuancoltech There isn't a null check for the Image object itself after loading it from the database. The current code has two checks:

  1. if (imageFile == null) - This checks if FileHelper.getImageFile() returned null
  2. if (!imageFile.exists()) - This checks if the file actually exists on disk

However, there's no check between retrieving the image from the database and passing it to the FileHelper. If an image with the requested ID doesn't exist in the database, imageDao.load() would return null, and passing that null to FileHelper.getImageFile() could potentially cause a NullPointerException.

Adding the suggested null check would handle this case explicitly with a more descriptive error message.

Copy link
Member Author

Choose a reason for hiding this comment

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

@coderabbitai This was fixed!

Copy link

Choose a reason for hiding this comment

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

@tuancoltech That's great! I can see you've not only added the null check for the Image object but also improved the error handling with more informative messages and a try-catch block for parsing the ID. Nice work on making the code more robust!


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

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 (4)
app/src/main/java/ai/elimu/content_provider/provider/ImageContentProvider.java (4)

32-32: Make TAG a static final constant.

The TAG variable should follow Android logging conventions.

-    private String TAG = ImageContentProvider.class.getName();
+    private static final String TAG = ImageContentProvider.class.getName();

168-170: Add a null check for getContext().

Similar to what's done in the query method, you should check if getContext() returns null.

+        Context context = getContext();
+        if (context == null) {
+            throw new FileNotFoundException("Content provider context is null");
+        }
-        RoomDb roomDb = RoomDb.getDatabase(getContext());
+        RoomDb roomDb = RoomDb.getDatabase(context);
         ImageDao imageDao = roomDb.imageDao();

161-161: Document mode parameter usage.

The 'mode' parameter is received but not used in the method. Either document why it's ignored or implement proper handling for different modes.

-    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+    /**
+     * Opens a file for reading based on the provided URI.
+     * Note: This implementation only supports read-only mode regardless of the mode parameter.
+     *
+     * @param uri The URI to the file
+     * @param mode The mode to open the file with (only read mode is supported)
+     * @return A ParcelFileDescriptor for the file
+     * @throws FileNotFoundException If the file cannot be found
+     */
+    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {

193-193: Consider adding permission checks.

Before returning a file descriptor, consider checking if the caller has the necessary permissions to access the file.

+        // Check if caller has permissions to access this file
+        Context context = getContext();
+        if (context != null) {
+            context.enforceCallingPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE, "No permission to read image files");
+        }
         return ParcelFileDescriptor.open(imageFile, ParcelFileDescriptor.MODE_READ_ONLY);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 66f20ed and 1fbebcb.

📒 Files selected for processing (1)
  • app/src/main/java/ai/elimu/content_provider/provider/ImageContentProvider.java (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build (windows-latest, 21)
  • GitHub Check: build (windows-latest, 17)
  • GitHub Check: build (macos-latest, 17)
🔇 Additional comments (3)
app/src/main/java/ai/elimu/content_provider/provider/ImageContentProvider.java (3)

160-194: The openFile implementation looks good overall.

You've properly implemented the openFile method with thorough error handling including:

  • URI validation
  • Safe parsing of the image ID with exception handling
  • Null checks for the Image object
  • File existence validation

This implementation supports scoped storage when sharing images as intended.


172-177: Good implementation of NumberFormatException handling.

This correctly addresses the previous review comment about validating fileId to avoid NumberFormatException.


181-183: Good implementation of null check for Image object.

This correctly addresses the previous review comment about adding a null check for the Image object to prevent potential NullPointerException.

@tuancoltech
Copy link
Member Author

@coderabbitai Please review again.

@tuancoltech tuancoltech requested a review from jo-elimu February 25, 2025 07:48
@tuancoltech
Copy link
Member Author

@jo-elimu Please review.

Copy link

coderabbitai bot commented Feb 25, 2025

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the full diffs of the changed files
git diff --staged

# If the above doesn't work, try without --staged
git diff

# List the files changed in this PR
git diff --name-only HEAD~1 HEAD

# Get the complete content of the key changed files
echo "=== ImageContentProvider.java ==="
cat app/src/main/java/ai/elimu/content_provider/provider/ImageContentProvider.java

echo "=== FileHelper.java ==="
cat app/src/main/java/ai/elimu/content_provider/util/FileHelper.java

echo "=== AndroidManifest.xml ==="
cat app/src/main/AndroidManifest.xml

echo "=== image_file_path.xml ==="
cat app/src/main/res/xml/image_file_path.xml

echo "=== build.gradle ==="
cat app/build.gradle

# Check if there are any related issues
echo "=== Finding related issue #148 ==="
gh issue view 148

Length of output: 21225


@tuancoltech I've reviewed your PR that implements support for scoped storage when sharing images. The implementation looks good and follows Android best practices:

  1. You've correctly updated the AndroidManifest.xml with android:grantUriPermissions="true" and added the necessary FILE_PROVIDER_PATHS metadata
  2. The new image_file_path.xml properly defines the external-files-path for pictures
  3. Your implementation of openFile() in ImageContentProvider handles URI validation, error cases, and file retrieval well
  4. The new overloaded getImageFile() method in FileHelper is appropriately restricted and maintains consistent behavior

This properly addresses issue #148 by implementing the FileProvider pattern for secure file sharing via content:// URIs instead of direct file paths, which is required for Android 11+.

Is there anything specific about the implementation you'd like me to review in more detail?

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Member

@jo-elimu jo-elimu left a comment

Choose a reason for hiding this comment

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

@tuancoltech There are merge conflicts 👇

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

Successfully merging this pull request may close these issues.

Suport SDK version 35 Support scoped storage
2 participants