skiptools / skiptools/skip-kit
Android `withDocumentPicker` may crash on real devices due to `URL.appendingPathComponent` NPE
Nobody has claimed this yet.
- Dominant language
- Swift
- Stars
- 10
- Forks
- 9
- PR merge metrics
- No merged PRs in 30d
Description
Body
We are seeing a real-device Android crash when importing files with SkipKit's withDocumentPicker.
Symptoms
After selecting a file from the system picker, the app immediately crashes on the main thread before our own import/validation logic runs.
This is reproducible for us with PDF files from Downloads on a real Android device. We did not reliably reproduce it on emulator.
Stack trace
java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=1062580886, result=-1, data=Intent { dat=content://com.android.providers.downloads.documents/document/msf:146 flg=0x43 }} to activity {app.coutto.jade/jade.module.MainActivity}: java.lang.NullPointerException。整体的log:04-16 23:46:58.618 W/HwActivityTaskManagerServiceEx( 1387): appSwitch from: com.android.documentsui to: app.coutto.jade
04-16 23:46:58.674 E/AndroidRuntime(10066): FATAL EXCEPTION: main
04-16 23:46:58.674 E/AndroidRuntime(10066): Process: app.coutto.jade, PID: 10066
04-16 23:46:58.674 E/AndroidRuntime(10066): java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=1062580886, result=-1, data=Intent { dat=content://com.android.providers.downloads.documents/document/msf:146 flg=0x43 }} to activity {app.coutto.jade/jade.module.MainActivity}: java.lang.NullPointerException
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.app.ActivityThread.deliverResults(ActivityThread.java:6027)
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.app.ActivityThread.handleSendResult(ActivityThread.java:6068)
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:51)
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:149)
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:103)
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2706)
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.os.Handler.dispatchMessage(Handler.java:109)
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.os.Looper.loop(Looper.java:228)
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.app.ActivityThread.main(ActivityThread.java:9105)
04-16 23:46:58.674 E/AndroidRuntime(10066): at java.lang.reflect.Method.invoke(Native Method)
04-16 23:46:58.674 E/AndroidRuntime(10066): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:614)
04-16 23:46:58.674 E/AndroidRuntime(10066): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1129)
04-16 23:46:58.674 E/AndroidRuntime(10066): Caused by: java.lang.NullPointerException
04-16 23:46:58.674 E/AndroidRuntime(10066): at skip.foundation.URL._appendingPathComponent(URL.kt:322)
04-16 23:46:58.674 E/AndroidRuntime(10066): at skip.foundation.URL.appendingPathComponent(URL.kt:325)
04-16 23:46:58.674 E/AndroidRuntime(10066): at jade.ui.View_JadeDocumentPickerKt.withJadeDocumentPicker$lambda$0$0$0(View+JadeDocumentPicker.kt:63)
04-16 23:46:58.674 E/AndroidRuntime(10066): at jade.ui.View_JadeDocumentPickerKt.$r8$lambda$0XlgswPBrP8Xoc9uug2MGsF6uXQ(Unknown Source:0)
04-16 23:46:58.674 E/AndroidRuntime(10066): at jade.ui.View_JadeDocumentPickerKt$$ExternalSyntheticLambda0.invoke(D8$$SyntheticClass:0)
04-16 23:46:58.674 E/AndroidRuntime(10066): at androidx.activity.compose.ActivityResultRegistryKt.rememberLauncherForActivityResult$lambda$4$0$0(ActivityResultRegistry.kt:104)
04-16 23:46:58.674 E/AndroidRuntime(10066): at androidx.activity.compose.ActivityResultRegistryKt.$r8$lambda$3CvmGOvkwZLY6ksF4ULs20ma2UE(Unknown Source:0)
04-16 23:46:58.674 E/AndroidRuntime(10066): at androidx.activity.compose.ActivityResultRegistryKt$$ExternalSyntheticLambda2.onActivityResult(D8$$SyntheticClass:0)
04-16 23:46:58.674 E/AndroidRuntime(10066): at androidx.activity.result.ActivityResultRegistry.doDispatch(ActivityResultRegistry.kt:350)
04-16 23:46:58.674 E/AndroidRuntime(10066): at androidx.activity.result.ActivityResultRegistry.dispatchResult(ActivityResultRegistry.kt:311)
04-16 23:46:58.674 E/AndroidRuntime(10066): at androidx.activity.ComponentActivity.onActivityResult(ComponentActivity.kt:788)
04-16 23:46:58.674 E/AndroidRuntime(10066): at androidx.fragment.app.FragmentActivity.onActivityResult(FragmentActivity.java:152)
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.app.Activity.dispatchActivityResult(Activity.java:8735)
04-16 23:46:58.674 E/AndroidRuntime(10066): at android.app.ActivityThread.deliverResults(ActivityThread.java:6020)
04-16 23:46:58.674 E/AndroidRuntime(10066): ... 11 more
04-16 23:46:58.679 W/ActivityTaskManager( 1387): finishTopCrashedActivityLocked Force finishing activity app.coutto.jade/jade.module.MainActivity
04-16 23:46:58.680 W/HwActivityTaskManagerServiceEx( 1387): setResumedActivityUncheckLocked start call, from: ActivityRecord{cd771ce u0 app.coutto.jade/jade.module.MainActivity t392 f}}, to: ActivityRecord{f5a29a7 u0 com.huawei.android.launcher/.unihome.UniHomeLauncher t2}
04-16 23:46:58.680 W/HwActivityTaskManagerServiceEx( 1387): appSwitch from: app.coutto.jade to: com.huawei.android.launcher
04-16 23:46:58.716 W/InputDispatcher( 1387): Attempted to unregister already unregistered input channel '8df915f app.coutto.jade/jade.module.MainActivity (server)'
04-16 23:46:59.181 W/ActivityTaskManager( 1387): Activity top resumed state loss timeout for ActivityRecord{cd771ce u0 app.coutto.jade/jade.module.MainActivity t-1 f}}
04-16 23:47:04.700 E/SWAP_AppModel( 1387): getAppNowScore, app not exist: app.coutto.jade
Likely cause
The Android implementation of withDocumentPicker copies the selected document into cache and builds the destination path with URL.appendingPathComponent(...).
That eventually reaches:
return components_0.url(relativeTo = baseURL)!!
in SkipFoundation.URL._appendingPathComponent, which can return null on Android and then crash with NPE.
So this looks like an Android path construction issue inside SkipKit / SkipFoundation, not an app-level file parsing issue.
Minimal fix suggestion
In the Android cache-copy path, avoid URL.appendingPathComponent(...) entirely and use JVM file APIs instead:
let safeFilename = selectedFilename.wrappedValue ?? "imported_file"
let outputFile = java.io.File(storageDir, safeFilename)
let destinationFileURL = URL(platformValue: outputFile.toURI())
This avoids the problematic SkipFoundation.URL path construction on Android.
Additional hardening that would help
- avoid
selectedFilename.wrappedValue! - avoid
getColumnIndexOrThrow(...) - fall back to
resolver.getType(uri) - fall back to
uri.lastPathSegmentwhenDISPLAY_NAMEis missing - handle
openInputStream(uri)returningnull
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at skip.foundation.URL._appendingPathComponent in URL.kt:322 and the Android picker callback at View+JadeDocumentPicker.kt:63, then reproduce with a PDF from Downloads on a real Android device. Trace the cache-copy destination construction and verify that importing the document completes without the reported NPE, including the missing-name and null-stream cases described in the issue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin, swift
- Domain
- mobile-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100