mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
feat(browser): name the browser the hand-off tile will open
"Other browser" says what it is not. When the system will name a default, the tile now says "Open in Chrome" — which is also the answer to "where does this go?", the question the tile exists to answer. Knowing the name took a `<queries>` entry. Android 11+ package visibility answers `resolveActivity` with nothing for a scheme the manifest does not declare, so the app could see every payment scheme it had asked about and not https. One entry alongside those, read-only: it names the target, it does not change where anything goes. `defaultBrowserName` is null whenever a name would be a promise we cannot keep — no default set (the system resolves to its own picker, which would otherwise be labelled "Android System"), the only handler is Amethyst, or visibility still hides it. The tile falls back to "Open in browser" and the hand-off keeps going through a chooser either way, so a wrong answer here costs a label and never a mis-launch. Only the full-screen browser passes a name so far; the embedded tab still shows the generic label. Verified on an SM-T220: the tile reads "Open in Chrome", on one line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
17423a200d
commit
dc5932a812
@@ -17,6 +17,17 @@
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
</intent>
|
||||
|
||||
<!-- So the browser's hand-off tile can name the browser it would open.
|
||||
Android 11+ package visibility answers resolveActivity with nothing
|
||||
for a scheme that is not declared here, so without this the tile can
|
||||
only ever say "Open in browser". Read-only: it names the target, the
|
||||
hand-off itself still goes through a chooser. -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
|
||||
<!-- NIP-A3 payment targets. Android 11+ package visibility means
|
||||
queryIntentActivities returns NOTHING for a scheme not declared here,
|
||||
so without these the zap picker's pay-to chip is invisible on every
|
||||
|
||||
@@ -5374,6 +5374,7 @@
|
||||
<string name="browser_pill_add_home">Add to Home</string>
|
||||
<string name="browser_pill_desktop">Desktop site</string>
|
||||
<string name="browser_pill_other_browser">Open in browser</string>
|
||||
<string name="browser_pill_other_browser_named">Open in %1$s</string>
|
||||
<string name="browser_pill_full_screen">Full screen</string>
|
||||
<string name="browser_pill_left_site">You left %1$s</string>
|
||||
<string name="browser_pill_back_to_app">Back to app</string>
|
||||
|
||||
+7
-2
@@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.commons.resources.browser_pill_close
|
||||
import com.vitorpamplona.amethyst.commons.resources.browser_pill_decision_allowed
|
||||
import com.vitorpamplona.amethyst.commons.resources.browser_pill_decision_blocked
|
||||
import com.vitorpamplona.amethyst.commons.resources.browser_pill_left_site
|
||||
import com.vitorpamplona.amethyst.commons.resources.browser_pill_other_browser_named
|
||||
import com.vitorpamplona.amethyst.commons.resources.browser_pill_permission_state
|
||||
import com.vitorpamplona.amethyst.commons.resources.browser_pill_privacy
|
||||
import com.vitorpamplona.amethyst.commons.resources.browser_pill_site_settings_none
|
||||
@@ -365,10 +366,14 @@ private fun TileGrid(
|
||||
actions.chunked(columns).forEach { rowActions ->
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
rowActions.forEach { action ->
|
||||
// The hand-off tile names the browser it will actually open when the
|
||||
// system will name one; "Open in browser" is what is left when the
|
||||
// answer is a chooser. See BrowserPillUi.defaultBrowserName.
|
||||
val named = ui.defaultBrowserName?.takeIf { action == Action.OPEN_IN_BROWSER_APP }
|
||||
ActionTile(
|
||||
symbol = pillSymbolFor(action) ?: MaterialSymbols.Info,
|
||||
label = stringRes(pillTileLabelFor(action)),
|
||||
description = stringRes(pillLabelFor(action)),
|
||||
label = named?.let { stringRes(Res.string.browser_pill_other_browser_named, it) } ?: stringRes(pillTileLabelFor(action)),
|
||||
description = named?.let { stringRes(Res.string.browser_pill_other_browser_named, it) } ?: stringRes(pillLabelFor(action)),
|
||||
onClick = { onAction(action) },
|
||||
selected =
|
||||
when (action) {
|
||||
|
||||
+7
@@ -44,6 +44,13 @@ data class BrowserPillUi(
|
||||
val consoleErrors: Int = 0,
|
||||
/** Answers this site already has (camera / mic / location), for the site-settings summary. */
|
||||
val sitePermissions: Map<BrowserSitePermission, BrowserSitePermission.Decision> = emptyMap(),
|
||||
/**
|
||||
* The default browser's name, when the system will name one and it is not us.
|
||||
*
|
||||
* Null means the hand-off shows a chooser, and the tile has to stay "Open in browser":
|
||||
* no default is set, the device hides it, or the only handler is Amethyst itself.
|
||||
*/
|
||||
val defaultBrowserName: String? = null,
|
||||
) {
|
||||
val security: BrowserChrome.Security get() = BrowserChrome.security(chrome)
|
||||
val host: String get() = BrowserChrome.displayHost(chrome.url)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.napplethost
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ResolveInfo
|
||||
import androidx.core.net.toUri
|
||||
|
||||
/**
|
||||
* Who "open in a browser" would actually hand the page to.
|
||||
*
|
||||
* Only used to say so on the tile. The hand-off itself still goes through a chooser, so a wrong
|
||||
* or missing answer here costs a label, never a mis-launch.
|
||||
*/
|
||||
object DefaultBrowser {
|
||||
/** A probe URL: the scheme is what selects browsers, the host is never contacted. */
|
||||
private val PROBE = Intent(Intent.ACTION_VIEW, "https://example.com".toUri()).addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
|
||||
/**
|
||||
* The default browser's app label, or null when the tile should stay generic.
|
||||
*
|
||||
* Null in three cases that all mean the same thing to a reader — you are going to get a
|
||||
* chooser, so do not promise a name:
|
||||
* - no default is set, and the system resolves to its own picker;
|
||||
* - the only handler is Amethyst, so "open in a browser" means anything but us;
|
||||
* - package visibility hides it. Android 11+ answers `resolveActivity` with nothing unless
|
||||
* the manifest declares a matching `<queries>` entry, which is why one exists for
|
||||
* http/https alongside the payment schemes.
|
||||
*/
|
||||
fun label(context: Context): String? =
|
||||
runCatching {
|
||||
val pm = context.packageManager
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val match: ResolveInfo = pm.resolveActivity(PROBE, PackageManager.MATCH_DEFAULT_ONLY) ?: return null
|
||||
val pkg = match.activityInfo?.packageName ?: return null
|
||||
// The system picker resolves for everything; naming it would be a lie.
|
||||
if (pkg == context.packageName || isResolver(match)) return null
|
||||
match.loadLabel(pm).toString().takeIf { it.isNotBlank() }
|
||||
}.getOrNull()
|
||||
|
||||
/**
|
||||
* Whether this is Android's own chooser rather than a browser.
|
||||
*
|
||||
* `resolveActivity` returns the resolver when several apps match and none is default; it
|
||||
* reports `exported=false` from the `android` package, which is the cheap way to tell.
|
||||
*/
|
||||
private fun isResolver(info: ResolveInfo): Boolean = info.activityInfo?.packageName == "android"
|
||||
}
|
||||
+1
@@ -1337,6 +1337,7 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
torOn = if (proxyPort > 0) useTor else null,
|
||||
),
|
||||
isFavorite = intent.getBooleanExtra(EXTRA_IS_FAVORITE, false),
|
||||
defaultBrowserName = DefaultBrowser.label(this),
|
||||
),
|
||||
listener = chromeListener,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user