feat(receive): add adaptive receive flow and document opening

This commit is contained in:
2026-07-12 04:15:21 +02:00
parent 757966379b
commit 996cc0a5ab
30 changed files with 1203 additions and 92 deletions

View File

@@ -10,10 +10,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.vnidrop.app.core.rememberFileSystemService
import com.vnidrop.app.notifications.rememberAndroidLocalNotificationService
import com.vnidrop.app.feature.receive.ExternalInvitationController
import java.net.NetworkInterface
@Composable
fun rememberAndroidAppDependencies(activity: ComponentActivity): AppDependencies {
fun rememberAndroidAppDependencies(activity: ComponentActivity, externalInvitations: ExternalInvitationController): AppDependencies {
val context = activity.applicationContext
val fileSystemService = rememberFileSystemService()
val notificationService = rememberAndroidLocalNotificationService(activity)
@@ -28,6 +29,7 @@ fun rememberAndroidAppDependencies(activity: ComponentActivity): AppDependencies
deviceInfoProvider = AndroidDeviceInfoProvider(context),
fileSystemService = fileSystemService,
localNotificationService = notificationService,
externalInvitations = externalInvitations,
)
}
}

View File

@@ -0,0 +1,86 @@
package com.vnidrop.app.feature.receive
import android.nfc.NfcAdapter
import android.nfc.tech.Ndef
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.codescanner.GmsBarcodeScannerOptions
import com.google.mlkit.vision.codescanner.GmsBarcodeScanning
@Composable
actual fun rememberReceiveInvitationActions(): ReceiveInvitationActions {
val context = LocalContext.current
val activity = context as? ComponentActivity
var fileResult by remember { mutableStateOf<((Result<String>) -> Unit)?>(null) }
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
val callback = fileResult.also { fileResult = null } ?: return@rememberLauncherForActivityResult
if (uri == null) return@rememberLauncherForActivityResult
callback(runCatching {
val bytes = context.contentResolver.openInputStream(uri)?.use { it.readNBytes(MaxInvitationBytes + 1) }
?: error("The invitation could not be opened")
require(bytes.size <= MaxInvitationBytes) { "The invitation is too large" }
bytes.decodeToString()
})
}
val nfcAdapter = remember(activity) { activity?.let(NfcAdapter::getDefaultAdapter) }
return remember(activity, filePicker, nfcAdapter) {
object : ReceiveInvitationActions {
override val fileAvailability = ReceiveMethodAvailability.Available
override val qrAvailability = if (activity != null) ReceiveMethodAvailability.Available else ReceiveMethodAvailability.Unavailable
override val nfcAvailability = if (nfcAdapter?.isEnabled == true) ReceiveMethodAvailability.Available else ReceiveMethodAvailability.Unavailable
override fun pickInvitation(onResult: (Result<String>) -> Unit) {
fileResult = onResult
filePicker.launch(arrayOf(InvitationMimeType, "application/octet-stream", "text/plain"))
}
override fun scanQrCode(onResult: (Result<String>) -> Unit) {
val host = activity ?: return onResult(Result.failure(UnsupportedOperationException("QR scanning is unavailable")))
val options = GmsBarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.enableAutoZoom()
.build()
GmsBarcodeScanning.getClient(host, options).startScan()
.addOnSuccessListener { barcode ->
val value = barcode.rawValue
onResult(if (value.isNullOrBlank()) Result.failure(IllegalArgumentException("The QR code is empty")) else Result.success(value))
}
.addOnFailureListener { onResult(Result.failure(it)) }
}
override fun readNfcInvitation(onResult: (Result<String>) -> Unit) {
val host = activity ?: return onResult(Result.failure(UnsupportedOperationException("NFC is unavailable")))
val adapter = nfcAdapter?.takeIf { it.isEnabled }
?: return onResult(Result.failure(UnsupportedOperationException("NFC is unavailable")))
adapter.enableReaderMode(host, { tag ->
val result = runCatching {
val ndef = Ndef.get(tag) ?: error("This NFC tag does not contain an invitation")
ndef.connect()
try {
val record = ndef.ndefMessage?.records?.firstOrNull { record ->
record.tnf == android.nfc.NdefRecord.TNF_MIME_MEDIA && record.type.decodeToString() == InvitationMimeType
} ?: error("This NFC tag does not contain a VniDrop invitation")
record.payload.decodeToString()
} finally { ndef.close() }
}
host.runOnUiThread {
adapter.disableReaderMode(host)
onResult(result)
}
}, NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_NFC_F or NfcAdapter.FLAG_READER_NFC_V, null)
}
override fun cancel() {
activity?.let { nfcAdapter?.disableReaderMode(it) }
}
}
}
}