Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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<AndroidFile.FileWrapper>(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<FileKitException> {
base / "backup.zip"
}

// Verify it's NOT the old exception type (the bug we fixed)
assertIsNot<FileKitUriPathNotSupportedException>(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,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Provide Android actual for PlatformFile(base, child)

The new expect fun PlatformFile(base, child) is declared in nonWebMain, but Android depends on nonWebMain (see build-logic/convention/ConfigureKotlinMultiplatform.kt where androidMain depends on nonWebMain) and there is no Android actual implementation. As a result, Android builds will fail with an “expected/actual mismatch” when compiling the Android target, and the / operator on PlatformFile can’t be resolved for Android. This breaks ./gradlew assemble for Android consumers until an Android actual is added.

Useful? React with 👍 / 👎.


/**
* Converts this [PlatformFile] to a [Path].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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)
}
Comment on lines +58 to +62

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore button state after directory picker completes

The debug screen now launches the directory picker, but the callback only runs debugPlatformTest and never resets buttonState or updates files. Since onPrimaryButtonClick still sets buttonState to Loading, the UI remains stuck in the loading state after any pick/cancel, and the results list stays empty. This is a regression from the previous file picker behavior and makes the debug UI appear broken after one use.

Useful? React with 👍 / 👎.

}
}

Scaffold(
topBar = {
AppPickerTopBar(
Expand All @@ -75,7 +87,8 @@ private fun DebugScreen(
primaryButtonState = buttonState,
onPrimaryButtonClick = {
buttonState = AppScreenHeaderButtonState.Loading
picker.launch()
// picker.launch()
folderPicker.launch()
},
modifier = Modifier.sizeIn(maxWidth = AppMaxWidth),
)
Expand All @@ -94,6 +107,8 @@ private fun DebugScreen(
}
}

internal expect suspend fun debugPlatformTest(folder: PlatformFile)

@Preview
@Composable
private fun DebugScreenPreview() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
Loading