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

@@ -14,14 +14,21 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@android:style/Theme.Material.NoActionBar">
<meta-data android:name="com.google.mlkit.vision.DEPENDENCIES" android:value="barcode_ui"/>
<activity
android:exported="true"
android:launchMode="singleTask"
android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="application/vnd.vnidrop.transfer"/>
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"

View File

@@ -1,16 +1,67 @@
package com.vnidrop.app
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.lifecycleScope
import com.vnidrop.app.feature.receive.ExternalInvitationController
import com.vnidrop.app.feature.receive.MaxVniDropInvitationBytes
import com.vnidrop.app.feature.receive.VniDropInvitationExtension
import com.vnidrop.app.feature.receive.VniDropInvitationMimeType
import java.nio.ByteBuffer
import java.nio.charset.CodingErrorAction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainActivity : ComponentActivity() {
private val externalInvitations = ExternalInvitationController()
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
App(rememberAndroidAppDependencies(this))
App(rememberAndroidAppDependencies(this, externalInvitations))
}
handleInvitationIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleInvitationIntent(intent)
}
private fun handleInvitationIntent(source: Intent?) {
if (source?.action != Intent.ACTION_VIEW) return
val uri = source.data
setIntent(Intent(this, MainActivity::class.java).setAction(Intent.ACTION_MAIN))
if (uri == null) {
externalInvitations.reportOpenFailure("The invitation could not be opened")
return
}
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) { readInvitation(uri, source.type) }
result.fold(externalInvitations::openInvitation) { error ->
externalInvitations.reportOpenFailure(error.message ?: "The invitation could not be opened")
}
}
}
private fun readInvitation(uri: Uri, declaredType: String?): Result<String> = runCatching {
val resolvedType = declaredType ?: contentResolver.getType(uri)
val hasExpectedName = uri.lastPathSegment?.endsWith(".$VniDropInvitationExtension", ignoreCase = true) == true
require(resolvedType == VniDropInvitationMimeType || hasExpectedName) { "This is not a VniDrop invitation" }
val bytes = contentResolver.openInputStream(uri)?.use { it.readNBytes(MaxVniDropInvitationBytes + 1) }
?: error("The invitation could not be opened")
require(bytes.size <= MaxVniDropInvitationBytes) { "The invitation is too large" }
Charsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(bytes))
.toString()
}
}