Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ dependencies {
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.navigation.compose)
implementation(libs.compose.icons.lucide)
// Chrome Custom Tabs — keeps explorer-link round-trip inside the app's
// task so the OEM auth gate doesn't re-engage on return (#138).
implementation(libs.androidx.browser)
debugImplementation(libs.androidx.ui.tooling)

// Hilt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,11 +278,7 @@ fun ActivityScreen(
retryDialogTx = failed
},
onOpenExplorer = { url ->
try {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (e: android.content.ActivityNotFoundException) {
// No browser available — silently ignore
}
com.rjnr.pocketnode.ui.util.openInBrowser(context, url)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,8 @@ fun HomeScreen(
val onLookupBlockHeight: (() -> Unit)? = uiState.address.takeIf { it.isNotBlank() }?.let { addr ->
{
val url = buildExplorerAddressUrl(addr, uiState.currentNetwork)
try {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (_: android.content.ActivityNotFoundException) {
// Custom Tabs first, falls back to ACTION_VIEW under the hood (#138).
if (!com.rjnr.pocketnode.ui.util.openInBrowser(context, url)) {
scope.launch {
snackbarHostState.showSnackbar("No browser available to open the explorer")
}
Expand Down Expand Up @@ -362,11 +361,7 @@ fun HomeScreen(
}
},
onOpenExplorer = { url ->
try {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (_: android.content.ActivityNotFoundException) {
// No browser available
}
com.rjnr.pocketnode.ui.util.openInBrowser(context, url)
},
onRetry = { tx ->
selectedTransaction = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -821,16 +821,9 @@ fun TransactionStatusDialog(
} else {
"https://explorer.nervos.org/transaction/"
}
try {
context.startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("$explorerBase$txHash")
)
)
} catch (e: android.content.ActivityNotFoundException) {
// No browser available — silently ignore
}
com.rjnr.pocketnode.ui.util.openInBrowser(
context, "$explorerBase$txHash",
)
},
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 0.dp)
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,7 @@ fun SettingsScreen(
val url = com.rjnr.pocketnode.ui.screens.home.buildExplorerAddressUrl(
addr, uiState.currentNetwork
)
try {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
} catch (_: android.content.ActivityNotFoundException) {
if (!com.rjnr.pocketnode.ui.util.openInBrowser(context, url)) {
scope.launch {
snackbarHostState.showSnackbar("No browser available to open the explorer")
}
Expand Down Expand Up @@ -534,8 +532,9 @@ private fun SettingsScreenUI(
title = "Open Source",
badgeText = "Github",
onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(GITHUB_URL))
context.startActivity(intent)
// Custom Tabs keeps the GitHub round-trip inside the app
// task, same reasoning as the explorer launches (#138).
com.rjnr.pocketnode.ui.util.openInBrowser(context, GITHUB_URL)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.rjnr.pocketnode.ui.util

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent

/**
* Open [url] in a Chrome Custom Tab when one is available, falling back to
* [Intent.ACTION_VIEW] otherwise.
*
* Why Custom Tabs over `ACTION_VIEW`:
* - The Custom Tab runs inside Pocket Node's task instead of starting Chrome's
* own task. The OS treats the round-trip as part of Pocket Node, so the auth
* gate does not re-engage on return and the user is not bounced through PIN
* re-entry.
* - Process death by aggressive memory managers (Tecno / Infinix / MIUI) is
* much less likely because the Custom Tab and host app share a task.
* - Browser back arrow drops the user straight back where they were.
*
* Returns `true` if either path launched successfully, `false` if neither did
* (rare: a stripped ROM with no browser at all). Callers can surface a
* snackbar in that case.
*
* Issue: #138
*/
fun openInBrowser(context: Context, url: String): Boolean {
val uri = Uri.parse(url)
return try {
CustomTabsIntent.Builder()
.setShowTitle(true)
.setUrlBarHidingEnabled(false)
.build()
.launchUrl(context, uri)
true
} catch (_: ActivityNotFoundException) {
// No Custom Tabs provider AND no fallback browser — Custom Tabs lib
// resolves through ACTION_VIEW under the hood when no provider exists,
// so reaching here means the device truly has no web browser.
false
} catch (_: Throwable) {
// Defensive: if the Custom Tabs launch crashes for any other reason
// (e.g. a misconfigured browser), try plain ACTION_VIEW one more time.
try {
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
true
} catch (_: ActivityNotFoundException) {
false
}
}
}
2 changes: 2 additions & 0 deletions android/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ lucide = "1.1.0"
mockk = "1.13.16"
coroutinesTest = "1.10.1"
paging = "3.3.6"
androidxBrowser = "1.8.0"

[libraries]
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serializationJson" }
Expand Down Expand Up @@ -77,6 +78,7 @@ camerax-view = { group = "androidx.camera", name = "camera-view", version.ref =
accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanistPermissions" }
zxing-core = { module = "com.google.zxing:core", version.ref = "zxing" }
compose-icons-lucide = { group = "com.composables", name = "icons-lucide", version.ref = "lucide" }
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" }

# Testing
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }
Expand Down
Loading