app: one activity for deep links, and say what re-enrolling actually does
MainActivity had no launchMode, so every echolot:// link stacked a fresh activity with its own ViewModel. The enrolment then ran in a throwaway copy and pressing back returned to the original screen showing none of it — silent, and indistinguishable from the link not working at all. singleTask plus onNewIntent means the link reaches the screen already in front of the user. The confirmation dialog also read as nonsense when re-enrolling with the server already configured: "already enrolled with X ... enrolling with X replaces that". Naming one URL twice looks like a bug and buries the consequence that does apply — the credential is replaced, the old one stops working at once, and the device appears on the server as a second entry next to the first, which is worth revoking afterwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
30d9501507
commit
a720e84411
@@ -22,7 +22,8 @@
|
|||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true"
|
||||||
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
@@ -43,9 +43,27 @@ class MainActivity : ComponentActivity() {
|
|||||||
private val permissionLauncher =
|
private val permissionLauncher =
|
||||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { /* proceed regardless */ }
|
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { /* proceed regardless */ }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The intent currently being acted on, so a deep link that arrives while the app is running
|
||||||
|
* is seen by the screen the user is already looking at.
|
||||||
|
*
|
||||||
|
* The activity is singleTask for the same reason. As a standard activity it stacked a second
|
||||||
|
* instance per link, each with its own ViewModel: the enrolment then happened in a throwaway
|
||||||
|
* copy, and pressing back returned to the original screen showing none of it. Silent, and
|
||||||
|
* indistinguishable from the link simply not working.
|
||||||
|
*/
|
||||||
|
private val liveIntent = mutableStateOf<android.content.Intent?>(null)
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: android.content.Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
liveIntent.value = intent
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
requestRuntimePermissions()
|
requestRuntimePermissions()
|
||||||
|
liveIntent.value = intent
|
||||||
setContent {
|
setContent {
|
||||||
MaterialTheme(colorScheme = darkColorScheme()) {
|
MaterialTheme(colorScheme = darkColorScheme()) {
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
@@ -66,7 +84,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
// Both deep links land here. They are told apart by host, so a sign-in
|
// Both deep links land here. They are told apart by host, so a sign-in
|
||||||
// redirect is never mistaken for an enrolment link — one spends a token, the
|
// redirect is never mistaken for an enrolment link — one spends a token, the
|
||||||
// other completes an authorization, and confusing them would fail obscurely.
|
// other completes an authorization, and confusing them would fail obscurely.
|
||||||
val incoming = intent?.takeIf { it.action == Intent.ACTION_VIEW }?.dataString
|
val incoming = liveIntent.value?.takeIf { it.action == Intent.ACTION_VIEW }?.dataString
|
||||||
val authUri = incoming?.takeIf { it.startsWith("echolot://auth") }
|
val authUri = incoming?.takeIf { it.startsWith("echolot://auth") }
|
||||||
val enrollUri = incoming?.takeIf { it.startsWith("echolot://enroll") }
|
val enrollUri = incoming?.takeIf { it.startsWith("echolot://enroll") }
|
||||||
androidx.compose.runtime.LaunchedEffect(enrollUri) {
|
androidx.compose.runtime.LaunchedEffect(enrollUri) {
|
||||||
@@ -80,9 +98,27 @@ class MainActivity : ComponentActivity() {
|
|||||||
vm.state.pendingEnroll?.let { pending ->
|
vm.state.pendingEnroll?.let { pending ->
|
||||||
androidx.compose.material3.AlertDialog(
|
androidx.compose.material3.AlertDialog(
|
||||||
onDismissRequest = { vm.cancelEnroll() },
|
onDismissRequest = { vm.cancelEnroll() },
|
||||||
title = { androidx.compose.material3.Text("Replace this device's server?") },
|
title = {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
if (pending.sameServer) "Enroll again with this server?"
|
||||||
|
else "Replace this device's server?"
|
||||||
|
)
|
||||||
|
},
|
||||||
text = {
|
text = {
|
||||||
androidx.compose.material3.Text(
|
androidx.compose.material3.Text(
|
||||||
|
// Naming the same URL twice reads as a mistake and buries the
|
||||||
|
// one consequence that actually applies: the device is issued a
|
||||||
|
// fresh credential and shows up as a second entry.
|
||||||
|
if (pending.sameServer) {
|
||||||
|
"This device is already enrolled with " +
|
||||||
|
"${pending.currentServer}.\n\n" +
|
||||||
|
"Enrolling again replaces its credential. The old one " +
|
||||||
|
"stops working immediately, and the device appears on " +
|
||||||
|
"the server as a new entry alongside the current one — " +
|
||||||
|
"which you may want to revoke afterwards.\n\n" +
|
||||||
|
"Runs already uploaded, and runs stored on this phone, " +
|
||||||
|
"are not affected."
|
||||||
|
} else {
|
||||||
"This device is already enrolled with " +
|
"This device is already enrolled with " +
|
||||||
"${pending.currentServer}.\n\n" +
|
"${pending.currentServer}.\n\n" +
|
||||||
"Enrolling with ${pending.newServer} replaces that. Runs " +
|
"Enrolling with ${pending.newServer} replaces that. Runs " +
|
||||||
@@ -90,11 +126,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
"stops reporting to the old server and appears on the new " +
|
"stops reporting to the old server and appears on the new " +
|
||||||
"one as a new device.\n\n" +
|
"one as a new device.\n\n" +
|
||||||
"Runs stored on this phone are not affected."
|
"Runs stored on this phone are not affected."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
androidx.compose.material3.TextButton(onClick = { vm.confirmEnroll() }) {
|
androidx.compose.material3.TextButton(onClick = { vm.confirmEnroll() }) {
|
||||||
androidx.compose.material3.Text("Enroll here")
|
androidx.compose.material3.Text(
|
||||||
|
if (pending.sameServer) "Enroll again" else "Enroll here"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
|||||||
@@ -58,7 +58,10 @@ data class UiState(
|
|||||||
* device simply stops reporting. Following a link is one tap from a web page, which is not enough
|
* device simply stops reporting. Following a link is one tap from a web page, which is not enough
|
||||||
* deliberation to discard a working enrollment by accident.
|
* deliberation to discard a working enrollment by accident.
|
||||||
*/
|
*/
|
||||||
data class PendingEnroll(val link: String, val currentServer: String, val newServer: String)
|
data class PendingEnroll(val link: String, val currentServer: String, val newServer: String) {
|
||||||
|
/** Re-enrolling with the server already configured, rather than moving to a different one. */
|
||||||
|
val sameServer: Boolean get() = currentServer.trimEnd('/') == newServer.trimEnd('/')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drives one measurement run: device-tier probes (link snapshot, per-network ICMP) always run;
|
* Drives one measurement run: device-tier probes (link snapshot, per-network ICMP) always run;
|
||||||
|
|||||||
Reference in New Issue
Block a user