diff --git a/filekit-core/src/androidHostTest/kotlin/io/github/vinceglb/filekit/PlatformFileAndroidTest.kt b/filekit-core/src/androidHostTest/kotlin/io/github/vinceglb/filekit/PlatformFileAndroidTest.kt index b55dd958..981a68d7 100644 --- a/filekit-core/src/androidHostTest/kotlin/io/github/vinceglb/filekit/PlatformFileAndroidTest.kt +++ b/filekit-core/src/androidHostTest/kotlin/io/github/vinceglb/filekit/PlatformFileAndroidTest.kt @@ -1,15 +1,30 @@ package io.github.vinceglb.filekit +import android.net.Uri +import io.github.vinceglb.filekit.exceptions.FileKitException +import io.github.vinceglb.filekit.exceptions.FileKitUriPathNotSupportedException import io.github.vinceglb.filekit.mimeType.MimeType +import org.junit.Before import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertIsNot +import kotlin.test.assertTrue @RunWith(RobolectricTestRunner::class) @Config(sdk = [36]) class PlatformFileAndroidTest { + @Before + fun setup() { + // Initialize FileKit with Robolectric's application context + FileKit.manualFileKitCoreInitialization(RuntimeEnvironment.getApplication()) + } + private val resourceDirectory = FileKit.projectDir / "src/nonWebTest/resources" private val textFile = resourceDirectory / "hello.txt" private val imageFile = resourceDirectory / "compose-logo.png" @@ -39,4 +54,41 @@ class PlatformFileAndroidTest { actual = resourceDirectory.mimeType(), ) } + + // Issue #415: Test `/` operator on FileWrapper (should work as before) + @Test + fun testDivOperatorOnFileWrapper() { + val base = PlatformFile("/tmp/test") + val child = base / "child.txt" + + assertIs(child.androidFile) + assertEquals("/tmp/test/child.txt", child.path) + } + + // Issue #415: Test `/` operator on UriWrapper does NOT throw FileKitUriPathNotSupportedException + // Note: In Robolectric, DocumentFile.fromTreeUri() returns null (no real SAF support), + // so this test verifies that the ORIGINAL bug (FileKitUriPathNotSupportedException) is fixed. + // Full integration testing requires a real Android device with SAF support. + @Test + fun testDivOperatorOnUriWrapper_noLongerThrowsPathNotSupportedException() { + // Create a Uri-based PlatformFile (tree Uri format used by directory pickers) + val uri = Uri.parse("content://com.android.externalstorage.documents/tree/primary%3ADocuments") + val base = PlatformFile(uri) + + // Before the fix, this would throw FileKitUriPathNotSupportedException + // because PlatformFile(base, child) called base.toKotlinxIoPath() which throws for UriWrapper. + // After the fix, it uses DocumentFile API instead, which fails in Robolectric + // with a generic FileKitException (not FileKitUriPathNotSupportedException). + val exception = assertFailsWith { + base / "backup.zip" + } + + // Verify it's NOT the old exception type (the bug we fixed) + assertIsNot(exception) + // The error message should be about DocumentFile access, not Path conversion + assertTrue( + exception.message?.contains("Could not access Uri as directory") == true || + exception.message?.contains("Could not create child file") == true, + ) + } } diff --git a/filekit-core/src/jvmAndNativeMain/kotlin/io/github/vinceglb/filekit/PlatformFile.jvmAndNative.kt b/filekit-core/src/jvmAndNativeMain/kotlin/io/github/vinceglb/filekit/PlatformFile.jvmAndNative.kt index 1174710c..083531a4 100644 --- a/filekit-core/src/jvmAndNativeMain/kotlin/io/github/vinceglb/filekit/PlatformFile.jvmAndNative.kt +++ b/filekit-core/src/jvmAndNativeMain/kotlin/io/github/vinceglb/filekit/PlatformFile.jvmAndNative.kt @@ -18,6 +18,9 @@ public actual val PlatformFile.path: String public actual fun PlatformFile(path: String): PlatformFile = PlatformFile(Path(path)) +public actual fun PlatformFile(base: PlatformFile, child: String): PlatformFile = + PlatformFile(base.toKotlinxIoPath() / child) + public actual fun PlatformFile.isRegularFile(): Boolean = withScopedAccess { SystemFileSystem.metadataOrNull(toKotlinxIoPath())?.isRegularFile ?: false } diff --git a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFile.nonWeb.kt b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFile.nonWeb.kt index 5ef0a706..6b3f2d75 100644 --- a/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFile.nonWeb.kt +++ b/filekit-core/src/nonWebMain/kotlin/io/github/vinceglb/filekit/PlatformFile.nonWeb.kt @@ -1,6 +1,5 @@ package io.github.vinceglb.filekit -import io.github.vinceglb.filekit.utils.div import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.withContext @@ -38,8 +37,7 @@ public expect fun PlatformFile(path: String): PlatformFile * @param child The child path string. * @return A [PlatformFile] instance representing the combined path. */ -public fun PlatformFile(base: PlatformFile, child: String): PlatformFile = - PlatformFile(base.toKotlinxIoPath() / child) +public expect fun PlatformFile(base: PlatformFile, child: String): PlatformFile /** * Converts this [PlatformFile] to a [Path]. diff --git a/sample/shared/src/commonMain/kotlin/io/github/vinceglb/filekit/sample/shared/ui/screens/debug/DebugScreen.kt b/sample/shared/src/commonMain/kotlin/io/github/vinceglb/filekit/sample/shared/ui/screens/debug/DebugScreen.kt index 983b02d6..c6ba9cec 100644 --- a/sample/shared/src/commonMain/kotlin/io/github/vinceglb/filekit/sample/shared/ui/screens/debug/DebugScreen.kt +++ b/sample/shared/src/commonMain/kotlin/io/github/vinceglb/filekit/sample/shared/ui/screens/debug/DebugScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -23,9 +24,11 @@ import io.github.vinceglb.filekit.sample.shared.ui.components.AppScreenHeader import io.github.vinceglb.filekit.sample.shared.ui.components.AppScreenHeaderButtonState import io.github.vinceglb.filekit.sample.shared.ui.icons.LucideIcons import io.github.vinceglb.filekit.sample.shared.ui.icons.MessageCircleCode +import io.github.vinceglb.filekit.sample.shared.ui.screens.directorypicker.rememberDirectoryPickerLauncher import io.github.vinceglb.filekit.sample.shared.ui.theme.AppMaxWidth import io.github.vinceglb.filekit.sample.shared.ui.theme.AppTheme import io.github.vinceglb.filekit.sample.shared.util.plus +import kotlinx.coroutines.launch @Composable internal fun DebugRoute( @@ -51,6 +54,15 @@ private fun DebugScreen( files = file?.let(::listOf) ?: emptyList() } + val scope = rememberCoroutineScope() + val folderPicker = rememberDirectoryPickerLauncher(directory = null) { folder -> + scope.launch { + folder?.let { + debugPlatformTest(folder) + } + } + } + Scaffold( topBar = { AppPickerTopBar( @@ -75,7 +87,8 @@ private fun DebugScreen( primaryButtonState = buttonState, onPrimaryButtonClick = { buttonState = AppScreenHeaderButtonState.Loading - picker.launch() + // picker.launch() + folderPicker.launch() }, modifier = Modifier.sizeIn(maxWidth = AppMaxWidth), ) @@ -94,6 +107,8 @@ private fun DebugScreen( } } +internal expect suspend fun debugPlatformTest(folder: PlatformFile) + @Preview @Composable private fun DebugScreenPreview() { diff --git a/sample/shared/src/nonWebMain/kotlin/io/github/vinceglb/filekit/sample/shared/ui/screens/debug/DebugScreen.nonWeb.kt b/sample/shared/src/nonWebMain/kotlin/io/github/vinceglb/filekit/sample/shared/ui/screens/debug/DebugScreen.nonWeb.kt index 5218964a..a870f2f8 100644 --- a/sample/shared/src/nonWebMain/kotlin/io/github/vinceglb/filekit/sample/shared/ui/screens/debug/DebugScreen.nonWeb.kt +++ b/sample/shared/src/nonWebMain/kotlin/io/github/vinceglb/filekit/sample/shared/ui/screens/debug/DebugScreen.nonWeb.kt @@ -1,2 +1,10 @@ -package io.github.vinceglb.filekit.sample.shared.ui.screens.debug +package io.github.vinceglb.filekit.sample.shared.ui.screens.debug +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.div +import io.github.vinceglb.filekit.writeString + +internal actual suspend fun debugPlatformTest(folder: PlatformFile) { + val file = folder / "debug-test-file.txt" + file.writeString("Vince") +}