ionic-team / ionic-team/capacitor-filesystem
bug(android): stat fails with OS-PLUG-FILE-0013 on MediaStore content:// URIs whose datetaken is NULL (e.g. PDFs)
- Dominant language
- TypeScript
- Stars
- 6
- Forks
- 15
- Avg merge
- 14m
- Merged PRs (30d)
- 1
Description
## 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/` 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:
```sh
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:
```ts
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 ionic-team/capacitor-plugins#308 (2020), `stat` did not support `content://` at all. In ionic-team/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.
Contributor guide
Research direction
Start with IONFILEContentHelper.kt and ionic-team/ion-android-filesystem#12, then inspect the plugin's existing metadata tests and dependency declaration. After the upstream fix is released, update the dependency and run the metadata tests; done means stat handles content:// documents whose metadata columns are NULL.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin, typescript
- Domain
- mobile-dev
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100