Skip to content

bug(android): stat fails with OS-PLUG-FILE-0013 on MediaStore content:// URIs whose datetaken is NULL (e.g. PDFs) #88

Open
ionic-team/ion-android-filesystem
#12

Description

@junwen-k

Capacitor Version

Installed Dependencies:
  @capacitor/core: 8.5.0
  @capacitor/android: 8.5.0
  @capacitor/ios: 8.5.0
  @capacitor/cli: 8.5.0
  @capacitor/filesystem: 8.1.3  (io.ionic.libs:ionfilesystem-android 1.1.0)

Platform(s)

Android. Reproduced on a stock Android 16 emulator (Google APIs image) and on a Huawei MatePad running Android 12.

Current Behavior

Filesystem.stat() rejects with OS-PLUG-FILE-0013 for a MediaStore content:// URI that points at anything other than a photo or video, such as a PDF in Downloads. The native cause is a Kotlin null check:

java.lang.NullPointerException: getString(...) must not be null

The row itself is fine: it has _display_name, _size and date_added. The only thing missing is datetaken, which MediaStore leaves NULL for non-media files.

Some file managers hand these URIs to other apps. Huawei's Files app, for example, sends content://media/external/file/<id> when you tap "Open with". As a result, stat fails for every PDF opened that way.

Expected Behavior

stat resolves with the file's name, size, mtime and ctime. When datetaken is NULL, the created time falls back to date_added, which the library already lists as its second choice.

Code Reproduction

  1. Put any PDF in the device's Downloads folder, then look at its MediaStore row:
    adb shell content query --uri content://media/external/file \
      --projection _id:_display_name:datetaken:date_added --where "\"_display_name LIKE '%.pdf'\""
    # Row: 0 _id=18, _display_name=Autumn Leaves.pdf, datetaken=NULL, date_added=1789320521
  2. Receive that URI in a Capacitor app with a read grant. For example, add an ACTION_VIEW intent filter for application/pdf, open the file from a file manager, and pick the URL up from App.addListener('appUrlOpen', …). Then call:
    await Filesystem.stat({ path: 'content://media/external/file/18' });
    // rejects: OS-PLUG-FILE-0013

Other Technical Details

The bug is in ion-android-filesystem, in IONFILEContentHelper.kt:

  • getFileMetadata queries with a null projection. It then reads the name, the size, the last-modified time and the created time from the same cursor.
  • getCreatedTimestampForContentUri asks getColumnIndexForNames(listOf(DATE_TAKEN, DATE_ADDED)) for a column. That function returns the first column that exists, and on a MediaStore cursor datetaken always exists, so it is always chosen.
  • The code then calls cursor.getString(columnIndex).toLongOrNull(). Kotlin types getString's platform return as non-null here, so the NULL datetaken throws before toLongOrNull() runs. The NPE is then mapped to OS-PLUG-FILE-0013.
  • getLastModifiedTimestampForContentUri hits the same throw through its fallback. getSizeForContentUri has the same pattern for a NULL _size.
  • This is not only a MediaStore problem. OpenableColumns.SIZE, DocumentsContract.Document.COLUMN_SIZE and COLUMN_LAST_MODIFIED are all documented as null when unknown. So a document provider serving a remote file can fail in the same way.

main and 1.1.1-beta.1 contain the same code.

The fix PR for ion-android-filesystem is ionic-team/ion-android-filesystem#12. It adds a small Cursor.getLongForNames helper that skips a column that is NULL for the current row. It also adds a DATE_TAKEN column to the test content provider, so the existing metadata tests cover the NULL case. Once that is released, this plugin needs a dependency bump.

Additional Context

  • Workaround: query OpenableColumns.DISPLAY_NAME directly from a small app-side plugin.
  • Earlier reports: these have the same symptom but a different cause. Both come from before stat used ion-android-filesystem. In bug: Filesystem.stat() crashes app with content urls capacitor-plugins#308 (2020), stat did not support content:// at all. In Filesystem.stat() with content:// capacitor#6988 (2023), a Google Photos URI crashed stat, and the discussion was never answered. I found no report against the current implementation.
  • A second, separate issue: MediaStore's date_added and date_modified are in seconds, but datetaken and DocumentsContract's last_modified are in milliseconds. So, even with the fix, ctime/mtime for these rows come back 1000× too small. I have kept that out of the fix PR to keep it focused, and can open it separately.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions