Repository navigation
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/<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
- 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
- Receive that URI in a Capacitor app with a read grant. For example, add an
ACTION_VIEWintent filter forapplication/pdf, open the file from a file manager, and pick the URL up fromApp.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:
getFileMetadataqueries with a null projection. It then reads the name, the size, the last-modified time and the created time from the same cursor.getCreatedTimestampForContentUriasksgetColumnIndexForNames(listOf(DATE_TAKEN, DATE_ADDED))for a column. That function returns the first column that exists, and on a MediaStore cursordatetakenalways exists, so it is always chosen.- The code then calls
cursor.getString(columnIndex).toLongOrNull(). Kotlin typesgetString's platform return as non-null here, so the NULLdatetakenthrows beforetoLongOrNull()runs. The NPE is then mapped toOS-PLUG-FILE-0013. getLastModifiedTimestampForContentUrihits the same throw through its fallback.getSizeForContentUrihas the same pattern for a NULL_size.- This is not only a MediaStore problem.
OpenableColumns.SIZE,DocumentsContract.Document.COLUMN_SIZEandCOLUMN_LAST_MODIFIEDare 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_NAMEdirectly from a small app-side plugin. - Earlier reports: these have the same symptom but a different cause. Both come from before
statusedion-android-filesystem. In bug: Filesystem.stat() crashes app with content urls capacitor-plugins#308 (2020),statdid not supportcontent://at all. In Filesystem.stat() with content:// capacitor#6988 (2023), a Google Photos URI crashedstat, and the discussion was never answered. I found no report against the current implementation. - A second, separate issue: MediaStore's
date_addedanddate_modifiedare in seconds, butdatetakenandDocumentsContract'slast_modifiedare in milliseconds. So, even with the fix,ctime/mtimefor 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