Add QR pairing (terminal QR, in-app scanner, iris://pair deep link)
CI / Kotlin tests (android host + desktop) (push) Successful in 8m5s
CI / Gateway plugin tests (push) Successful in 9m47s

This commit is contained in:
ARIA committed 2026-08-22 22:43:13 +02:00
1 parent 27dc7917f2
commit 7a6d922d12
63 files changed
+2073 -630

No files matched your search

@@ -8,6 +8,9 @@
<!-- M5: ntfy listener foreground service (dataSync type on API 34). -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- QR pairing (docs/20): the in-app scanner reads the camera. -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:label="Iris"
@@ -38,8 +41,22 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="iris" android:host="chat" />
</intent-filter>
<!-- QR pairing (docs/20): iris://pair deep link (scanned QR / link). -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="iris" android:host="pair" />
</intent-filter>
</activity>
<!-- QR pairing (docs/20): full-screen scanner launched from the
Connect screen. Not exported; started only by our own app. -->
<activity
android:name="iris.platform.QrScanActivity"
android:exported="false"
android:screenOrientation="portrait" />
<!-- M4: serve cached media/documents to other apps (ACTION_VIEW). -->
<provider
android:name="androidx.core.content.FileProvider"
@@ -16,11 +16,16 @@ import iris.platform.AndroidEnv
import iris.platform.AndroidSecureStore
import iris.platform.AppBridge
import iris.platform.syncNtfyListener
import iris.util.PairLink
class MainActivity : ComponentActivity() {
private val deepLinkChatId = mutableStateOf<String?>(null)
private val deepLinkThreadId = mutableStateOf<String?>(null)
// QR pairing (docs/20): a parsed iris://pair link to prefill the Connect
// screen with.
private val pendingPair = mutableStateOf<PairLink?>(null)
private val notificationPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { /* result ignored */ }
@@ -37,7 +42,13 @@ class MainActivity : ComponentActivity() {
setContent {
val chatId by deepLinkChatId
val threadId by deepLinkThreadId
IrisApp(store = store, deepLinkChatId = chatId, deepLinkThreadId = threadId)
val pair by pendingPair
IrisApp(
store = store,
deepLinkChatId = chatId,
deepLinkThreadId = threadId,
deepLinkPair = pair,
)
}
}
@@ -64,6 +75,14 @@ class MainActivity : ComponentActivity() {
* extras or an iris://chat/<id>?thread=<tid> URI). */
private fun handleDeepLink(intent: Intent?) {
val data = intent?.data
// QR pairing (docs/20): iris://pair?host=…&port=…&secure=…&token=… —
// the whole payload is in the query string; parse it with the shared
// PairLink parser.
if (data?.scheme == "iris" && data.host == "pair") {
val link = PairLink.parse(data.toString())
if (link != null) pendingPair.value = link
return
}
val chatId =
intent?.getStringExtra("chat_id")
?: data?.pathSegments?.firstOrNull()