Add mobile file access sources

This commit is contained in:
2026-07-03 00:24:25 +02:00
parent 3da576a94e
commit 3cdbf7e8f9
11 changed files with 272 additions and 29 deletions

View File

@@ -50,8 +50,7 @@ class CoreRepository(
}
suspend fun sharePath(path: String, transferName: String, senderName: String) = runCore {
val active = requireCore()
val result = active.shareFiles(
shareSources(
sources = listOf(
ShareSource(
kind = SourceKind.PATH,
@@ -60,14 +59,54 @@ class CoreRepository(
isDirectory = false,
),
),
metadata = ShareMetadataInput(
transferId = nextTransferId(),
transferName = transferName.ifBlank { null },
senderName = senderName.ifBlank { null },
),
transferName = transferName,
senderName = senderName,
)
}
suspend fun shareFileDescriptor(
fd: Int,
displayName: String,
transferName: String,
senderName: String,
) = runCore {
// The fd is borrowed from platform code. Rust duplicates it before
// starting the import, so Android may close the ParcelFileDescriptor
// once this suspend call returns.
shareSources(
sources = listOf(
ShareSource(
kind = SourceKind.FILE_DESCRIPTOR,
value = fd.toString(),
displayName = displayName.ifBlank { "transfer" },
isDirectory = false,
),
),
transferName = transferName,
senderName = senderName,
)
}
suspend fun shareSecurityScopedFileUrl(
fileUrl: String,
displayName: String,
transferName: String,
senderName: String,
) = runCore {
// The iOS actual for withPlatformPathAccess starts and stops the
// security-scoped URL lease around this entire shareFiles call.
shareSources(
sources = listOf(
ShareSource(
kind = SourceKind.IOS_SECURITY_SCOPED_URL,
value = fileUrl,
displayName = displayName.ifBlank { fileUrl.substringAfterLast('/').ifBlank { "transfer" } },
isDirectory = false,
),
),
transferName = transferName,
senderName = senderName,
)
_state.update { it.copy(lastShare = result, error = null) }
refreshStatus()
}
suspend fun inspectTicket(ticket: String) = runCore {
@@ -80,6 +119,17 @@ class CoreRepository(
refreshStatus()
}
suspend fun receiveIntoSecurityScopedDirectory(
ticket: String,
outputDirectoryUrl: String,
receiverName: String,
) = runCore {
withPlatformPathAccess(SourceKind.IOS_SECURITY_SCOPED_URL, outputDirectoryUrl) {
requireCore().receive(ticket, outputDirectoryUrl, receiverName.ifBlank { null })
}
refreshStatus()
}
suspend fun cancel(transferId: ULong) = runCore {
requireCore().cancelTransfer(transferId)
refreshStatus()
@@ -98,6 +148,39 @@ class CoreRepository(
private fun requireCore(): VnidropCore =
core ?: error("Initialize the core first.")
private suspend fun shareSources(
sources: List<ShareSource>,
transferName: String,
senderName: String,
) {
withPlatformPathAccess(sources) {
val result = requireCore().shareFiles(
sources = sources,
metadata = ShareMetadataInput(
transferId = nextTransferId(),
transferName = transferName.ifBlank { null },
senderName = senderName.ifBlank { null },
),
)
_state.update { it.copy(lastShare = result, error = null) }
}
refreshStatus()
}
private suspend fun <T> withPlatformPathAccess(
sources: List<ShareSource>,
index: Int = 0,
block: suspend () -> T,
): T {
if (index >= sources.size) {
return block()
}
val source = sources[index]
return withPlatformPathAccess(source.kind, source.value) {
withPlatformPathAccess(sources, index + 1, block)
}
}
private fun refreshStatus() {
val status = core?.status()
_state.update {

View File

@@ -0,0 +1,13 @@
package com.vnidrop.app.core
import uniffi.vnidrop.SourceKind
// Platform file handles have different lifetime rules. Desktop paths need no
// extra work, Android fd sources are duplicated immediately by Rust, and iOS
// security-scoped URLs must remain leased while Rust performs the blocking
// import/export call.
internal expect suspend fun <T> withPlatformPathAccess(
kind: SourceKind,
value: String,
block: suspend () -> T,
): T