pwa improvements: custom name and context selection

This commit is contained in:
Fabian Freund
2026-04-20 12:20:55 +02:00
parent 7af0f1d4e3
commit b699ef5fba
23 changed files with 1220 additions and 439 deletions
@@ -16,11 +16,16 @@ object PwaConstants {
const val EXTRA_PWA_TOKEN = "pwa_token"
const val EXTRA_PWA_INSTALL_START_URL = "pwa_install_start_url"
const val EXTRA_SHORTCUT_TYPE = "shortcut_type"
const val EXTRA_SHORTCUT_CONTAINER_MODE = "shortcut_container_mode"
// Shortcut type values
const val SHORTCUT_TYPE_BASIC = "basic"
const val SHORTCUT_TYPE_PWA = "pwa"
// Shortcut container mode values
const val SHORTCUT_CONTAINER_MODE_SPECIFIC = "specific"
const val SHORTCUT_CONTAINER_MODE_UNASSIGNED = "unassigned"
// Profile and file paths
const val CURRENT_PROFILE_FILE = "weblibre_profiles/current_profile"
const val PROFILE_MAPPING_PREFS = "pwa_profile_mapping"
@@ -222,14 +222,22 @@ class IntentReceiverActivity : Activity() {
PwaConstants.PROFILE_MAPPING_PREFS,
Context.MODE_PRIVATE,
)
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${intentUrl}::${profileUuid}"
if (prefs.getString(tokenKey, null) == token) {
// Tokens are keyed by (url, profile, contextId) since each install
// variant gets its own token. Fall back to the legacy (url, profile)
// key for shortcuts pinned before context-scoping was introduced.
val contextId = intent.getStringExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID).orEmpty()
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${intentUrl}::${profileUuid}::${contextId}"
val legacyTokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${intentUrl}::${profileUuid}"
if (prefs.getString(tokenKey, null) == token ||
prefs.getString(legacyTokenKey, null) == token) {
return true
}
if (!installStartUrl.isNullOrEmpty() && installStartUrl != intentUrl) {
val installTokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${installStartUrl}::${profileUuid}"
if (prefs.getString(installTokenKey, null) == token) {
val installTokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${installStartUrl}::${profileUuid}::${contextId}"
val legacyInstallTokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${installStartUrl}::${profileUuid}"
if (prefs.getString(installTokenKey, null) == token ||
prefs.getString(legacyInstallTokenKey, null) == token) {
return true
}
}
@@ -52,6 +52,14 @@ class GeckoPwaApiImpl(
private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
}
private enum class ShortcutKind(
val shortcutType: String,
val idPrefix: String,
) {
PWA(PwaConstants.SHORTCUT_TYPE_PWA, "pwa"),
BASIC(PwaConstants.SHORTCUT_TYPE_BASIC, "shortcut"),
}
private val logger = Logger("GeckoPwaApiImpl")
private val appPrefs by lazy {
context.applicationContext.getSharedPreferences(
@@ -68,6 +76,7 @@ class GeckoPwaApiImpl(
tabId: String?,
profileUuid: String,
contextId: String?,
overrideAppName: String?,
callback: (Result<Boolean>) -> Unit
) {
logger.debug("installWebApp called for tabId: $tabId, profileUuid: $profileUuid, contextId: $contextId")
@@ -86,7 +95,7 @@ class GeckoPwaApiImpl(
return@launch
}
val manifest = tab.content.webAppManifest ?: run {
val baseManifest = tab.content.webAppManifest ?: run {
// Generate a synthetic manifest for sites without one
val url = tab.content.url
val title = tab.content.title.ifBlank { url }
@@ -99,17 +108,26 @@ class GeckoPwaApiImpl(
)
}
val manifest = overrideAppName?.takeIf { it.isNotBlank() }?.let { name ->
baseManifest.copy(name = name, shortName = name)
} ?: baseManifest
logger.debug("Installing web app for tab ${tab.id}: ${manifest.startUrl}")
val success = createPwaShortcut(
manifest = manifest,
profileUuid = profileUuid,
contextId = contextId,
tabFavicon = tab.content.icon,
)
if (success) {
components.core.webAppManifestStorage.saveManifest(manifest)
storeProfileMapping(manifest.startUrl, profileUuid)
// Persist the unmodified manifest so a second install
// of the same URL with a different overrideAppName or
// contextId does not clobber the first install's
// standalone-window metadata. The user-chosen label
// lives on the shortcut itself.
components.core.webAppManifestStorage.saveManifest(baseManifest)
logger.debug("Web app installation completed for tab ${tab.id}")
} else {
logger.warn("Failed to create PWA shortcut for tab ${tab.id}")
@@ -130,6 +148,7 @@ class GeckoPwaApiImpl(
manifest: WebAppManifest,
profileUuid: String,
contextId: String?,
tabFavicon: Bitmap?,
): Boolean = withContext(Dispatchers.Main) {
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
@@ -148,18 +167,30 @@ class GeckoPwaApiImpl(
return@withContext false
}
val (iconBitmap, isMaskable) = loadPwaIcon(manifest)
// Prefer a manifest icon when available; for synthetic manifests
// (no icons declared) fall back to the tab favicon so we always
// ship a shortcut icon — some launchers crash when pinning a
// shortcut without one.
val iconBitmap = loadPwaIcon(manifest)
?: loadTabFaviconBitmap(manifest.startUrl, tabFavicon)
val shortcutId = generateShortcutId(manifest.startUrl, profileUuid)
val appName = manifest.shortName ?: manifest.name ?: "Web App"
val shortcutId = resolveShortcutId(
shortcutManager = shortcutManager,
url = manifest.startUrl,
profileUuid = profileUuid,
contextId = contextId,
shortcutKind = ShortcutKind.PWA,
)
val launchToken = resolveLaunchToken(
shortcutManager = shortcutManager,
shortcutId = shortcutId,
startUrl = manifest.startUrl,
profileUuid = profileUuid,
contextId = contextId,
)
val appName = manifest.shortName ?: manifest.name ?: "Web App"
val shortcutIntent = Intent(context, IntentReceiverActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = Uri.parse(manifest.startUrl)
@@ -167,7 +198,11 @@ class GeckoPwaApiImpl(
putExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID, contextId)
putExtra(PwaConstants.EXTRA_PWA_TOKEN, launchToken)
putExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL, manifest.startUrl)
putExtra(PwaConstants.EXTRA_SHORTCUT_TYPE, PwaConstants.SHORTCUT_TYPE_PWA)
putExtra(PwaConstants.EXTRA_SHORTCUT_TYPE, ShortcutKind.PWA.shortcutType)
putExtra(
PwaConstants.EXTRA_SHORTCUT_CONTAINER_MODE,
resolveShortcutContainerMode(contextId),
)
}
val shortcut = ShortcutInfo.Builder(context, shortcutId).apply {
@@ -176,19 +211,17 @@ class GeckoPwaApiImpl(
setIntent(shortcutIntent)
if (iconBitmap != null) {
// Only use adaptive bitmap for maskable icons (designed for adaptive shapes)
// Regular icons should use createWithBitmap to display as-is
if (isMaskable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setIcon(Icon.createWithAdaptiveBitmap(iconBitmap))
} else {
setIcon(Icon.createWithBitmap(iconBitmap))
}
// Always use createWithBitmap (never createWithAdaptiveBitmap):
// Launcher3's pin-preview routes adaptive icons through
// AdaptiveIconDrawable + BitmapShader and promotes intermediates
// to HARDWARE, crashing the software preview canvas.
setIcon(Icon.createWithBitmap(iconBitmap))
}
}.build()
// Update existing shortcut intent if one exists with the same ID
// (e.g. upgrading a basic shortcut to PWA). requestPinShortcut alone
// may reuse the cached intent on some launchers.
// Update an existing install of the same kind in place. Some
// launchers reuse cached shortcut metadata unless we explicitly
// refresh the pinned record first.
updateExistingShortcut(shortcutManager, shortcut)
val success = shortcutManager.requestPinShortcut(shortcut, null)
@@ -218,53 +251,129 @@ class GeckoPwaApiImpl(
}
/**
* Generates a collision-resistant shortcut ID from URL + profile using SHA-256.
* Resolves the shortcut ID for the current install kind.
*
* New installs use a kind-specific ID so a standalone PWA and a regular
* shortcut for the same site do not overwrite each other. For minimal
* migration handling, we still reuse the legacy shared ID if a pinned
* shortcut with that ID already exists for the same kind.
*/
private fun generateShortcutId(url: String, profileUuid: String): String {
private fun resolveShortcutId(
shortcutManager: ShortcutManager,
url: String,
profileUuid: String,
contextId: String? = null,
shortcutKind: ShortcutKind,
): String {
val typedShortcutId = generateShortcutId(
url = url,
profileUuid = profileUuid,
contextId = contextId,
shortcutKind = shortcutKind,
)
if (shortcutManager.pinnedShortcuts.any { it.id == typedShortcutId }) {
return typedShortcutId
}
val legacyShortcutId = generateLegacyShortcutId(
url = url,
profileUuid = profileUuid,
contextId = contextId,
)
val matchingLegacyShortcut = shortcutManager.pinnedShortcuts.firstOrNull { shortcut ->
if (shortcut.id != legacyShortcutId) {
return@firstOrNull false
}
val shortcutIntent = shortcut.intent ?: return@firstOrNull false
shortcutIntent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) == profileUuid &&
shortcutIntent.getStringExtra(PwaConstants.EXTRA_SHORTCUT_TYPE) == shortcutKind.shortcutType
}
return matchingLegacyShortcut?.id ?: typedShortcutId
}
/**
* Generates a collision-resistant shortcut ID from (type, url, profile,
* contextId) using SHA-256. The display label is deliberately excluded
* because it is presentation, not install identity.
*/
private fun generateShortcutId(
url: String,
profileUuid: String,
contextId: String? = null,
shortcutKind: ShortcutKind,
): String {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest("$url::$profileUuid".toByteArray())
val key = if (contextId.isNullOrEmpty()) {
"${shortcutKind.shortcutType}::$url::$profileUuid"
} else {
"${shortcutKind.shortcutType}::$url::$profileUuid::$contextId"
}
val hash = digest.digest(key.toByteArray())
val hex = hash.take(16).joinToString("") { "%02x".format(it) }
return "${shortcutKind.idPrefix}_$hex"
}
/**
* Historical shared shortcut ID used before install kind became part of
* the identity. Both PWAs and basic shortcuts previously reused this ID.
*/
private fun generateLegacyShortcutId(
url: String,
profileUuid: String,
contextId: String? = null,
): String {
val digest = MessageDigest.getInstance("SHA-256")
val key = if (contextId.isNullOrEmpty()) {
"$url::$profileUuid"
} else {
"$url::$profileUuid::$contextId"
}
val hash = digest.digest(key.toByteArray())
val hex = hash.take(16).joinToString("") { "%02x".format(it) }
return "pwa_$hex"
}
private fun resolveShortcutContainerMode(contextId: String?): String {
return if (contextId.isNullOrEmpty()) {
PwaConstants.SHORTCUT_CONTAINER_MODE_UNASSIGNED
} else {
PwaConstants.SHORTCUT_CONTAINER_MODE_SPECIFIC
}
}
/**
* Loads the PWA icon from the manifest using BrowserIcons.
* Returns a pair of (bitmap, isMaskable) to determine proper icon format.
* Loads the PWA icon from the manifest using BrowserIcons. Requested at
* plain LAUNCHER size since the shortcut is set as a non-adaptive bitmap.
*/
private suspend fun loadPwaIcon(manifest: WebAppManifest): Pair<Bitmap?, Boolean> = withContext(Dispatchers.IO) {
private suspend fun loadPwaIcon(manifest: WebAppManifest): Bitmap? = withContext(Dispatchers.IO) {
try {
val iconResource = manifest.icons
.filter { it.purpose.contains(WebAppManifest.Icon.Purpose.MASKABLE) ||
it.purpose.contains(WebAppManifest.Icon.Purpose.ANY) }
.maxByOrNull { (it.sizes?.maxOf { size -> size.width * size.height } ?: 0) }
?: manifest.icons.firstOrNull()
?: return@withContext null
if (iconResource != null) {
val isMaskable = iconResource.purpose.contains(WebAppManifest.Icon.Purpose.MASKABLE)
val iconRequest = IconRequest(
url = manifest.startUrl,
size = IconRequest.Size.LAUNCHER_ADAPTIVE,
resources = listOf(
IconRequest.Resource(
url = iconResource.src,
type = IconRequest.Resource.Type.MANIFEST_ICON,
sizes = iconResource.sizes?.map { size ->
mozilla.components.concept.engine.manifest.Size(size.width, size.height)
} ?: emptyList(),
mimeType = iconResource.type,
maskable = isMaskable
)
val iconRequest = IconRequest(
url = manifest.startUrl,
size = IconRequest.Size.LAUNCHER,
resources = listOf(
IconRequest.Resource(
url = iconResource.src,
type = IconRequest.Resource.Type.MANIFEST_ICON,
sizes = iconResource.sizes?.map { size ->
mozilla.components.concept.engine.manifest.Size(size.width, size.height)
} ?: emptyList(),
mimeType = iconResource.type,
)
)
)
val iconResult = components.core.icons.loadIcon(iconRequest).await()
Pair(iconResult?.bitmap, isMaskable)
} else {
Pair(null, false)
}
components.core.icons.loadIcon(iconRequest).await()?.bitmap
} catch (e: Exception) {
logger.error("Failed to load PWA icon", e)
Pair(null, false)
null
}
}
@@ -335,16 +444,23 @@ class GeckoPwaApiImpl(
return@withContext false
}
val shortcutId = generateShortcutId(url, profileUuid)
val shortLabel = title.ifBlank { url }
val shortcutId = resolveShortcutId(
shortcutManager = shortcutManager,
url = url,
profileUuid = profileUuid,
contextId = contextId,
shortcutKind = ShortcutKind.BASIC,
)
val launchToken = resolveLaunchToken(
shortcutManager = shortcutManager,
shortcutId = shortcutId,
startUrl = url,
profileUuid = profileUuid,
contextId = contextId,
)
val shortLabel = title.ifBlank { url }
val shortcutIntent = Intent(context, IntentReceiverActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = Uri.parse(url)
@@ -352,7 +468,11 @@ class GeckoPwaApiImpl(
putExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID, contextId)
putExtra(PwaConstants.EXTRA_PWA_TOKEN, launchToken)
putExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL, url)
putExtra(PwaConstants.EXTRA_SHORTCUT_TYPE, PwaConstants.SHORTCUT_TYPE_BASIC)
putExtra(PwaConstants.EXTRA_SHORTCUT_TYPE, ShortcutKind.BASIC.shortcutType)
putExtra(
PwaConstants.EXTRA_SHORTCUT_CONTAINER_MODE,
resolveShortcutContainerMode(contextId),
)
}
val icon = loadTabIcon(url, tabIcon)
@@ -364,7 +484,7 @@ class GeckoPwaApiImpl(
icon?.let { setIcon(it) }
}.build()
// Update existing shortcut intent if one exists with the same ID
// Update an existing install of the same kind in place.
updateExistingShortcut(shortcutManager, shortcut)
val success = shortcutManager.requestPinShortcut(shortcut, null)
@@ -377,31 +497,37 @@ class GeckoPwaApiImpl(
}
/**
* Loads an icon for the shortcut from the tab's favicon or BrowserIcons.
* Loads a favicon-style bitmap for [url], preferring the in-memory tab icon
* and falling back to BrowserIcons at LAUNCHER size. Returns a live bitmap
* (caller is responsible for converting to software before use).
*/
private suspend fun loadTabIcon(url: String, tabIcon: Bitmap?): Icon? = withContext(Dispatchers.IO) {
private suspend fun loadTabFaviconBitmap(
url: String,
tabIcon: Bitmap?,
): Bitmap? = withContext(Dispatchers.IO) {
try {
// Try using the tab's existing favicon first
val bitmap = tabIcon?.takeUnless { it.isRecycled }
tabIcon?.takeUnless { it.isRecycled }
?: run {
// Fall back to loading via BrowserIcons
val iconRequest = IconRequest(
url = url,
size = IconRequest.Size.LAUNCHER,
)
components.core.icons.loadIcon(iconRequest).await()?.bitmap
}
bitmap?.takeUnless { it.isRecycled }?.let {
val bitmapCopy = it.copy(it.config ?: Bitmap.Config.ARGB_8888, false)
Icon.createWithBitmap(bitmapCopy)
}
} catch (e: Exception) {
logger.error("Failed to load tab icon", e)
logger.error("Failed to load tab favicon bitmap", e)
null
}
}
/**
* Loads an icon for the shortcut from the tab's favicon or BrowserIcons.
*/
private suspend fun loadTabIcon(url: String, tabIcon: Bitmap?): Icon? {
val bitmap = loadTabFaviconBitmap(url, tabIcon)
return bitmap?.takeUnless { it.isRecycled }?.let(Icon::createWithBitmap)
}
/**
* Extracts the scope from a URL (origin + path up to last segment).
*/
@@ -425,14 +551,51 @@ class GeckoPwaApiImpl(
coroutineScope.launch {
try {
val storage = components.core.webAppManifestStorage
val manifests = storage.loadShareableManifests(System.currentTimeMillis())
val currentProfileUuid = getCurrentProfileUuid()
val pwaManifests = manifests.filter { manifest ->
val mappedProfile = getProfileMapping(manifest.startUrl)
currentProfileUuid == null || mappedProfile == null || mappedProfile == currentProfileUuid
}.map { manifest ->
manifest.toPwaManifest()
// Pinned shortcuts are the source of truth for installs: each
// pinned shortcut carries its own label, profile, and
// contextId in its intent extras. Two installs of the same
// URL with different contextIds or labels are two distinct
// shortcuts here, even though Mozilla's manifest storage
// keys the underlying manifest by URL only. We join the
// shared manifest with per-install fields from each shortcut.
val pwaShortcuts = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.getSystemService<ShortcutManager>()?.pinnedShortcuts
?.filter { shortcut ->
val intent = shortcut.intent ?: return@filter false
intent.getStringExtra(PwaConstants.EXTRA_SHORTCUT_TYPE) ==
PwaConstants.SHORTCUT_TYPE_PWA
}
?: emptyList()
} else {
emptyList()
}
val pwaManifests = pwaShortcuts.mapNotNull { shortcut ->
val intent = shortcut.intent ?: return@mapNotNull null
val shortcutProfile =
intent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID)
if (currentProfileUuid != null &&
shortcutProfile != null &&
shortcutProfile != currentProfileUuid
) {
return@mapNotNull null
}
val installStartUrl =
intent.getStringExtra(PwaConstants.EXTRA_PWA_INSTALL_START_URL)
?: intent.dataString
?: return@mapNotNull null
val manifest = storage.loadManifest(installStartUrl)
?: return@mapNotNull null
manifest.toPwaManifest(
contextId = intent.getStringExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID),
installLabel = shortcut.shortLabel?.toString(),
)
}
logger.debug("Found ${pwaManifests.size} installed web apps")
callback(Result.success(pwaManifests))
} catch (e: Exception) {
@@ -442,19 +605,14 @@ class GeckoPwaApiImpl(
}
}
private fun storeProfileMapping(startUrl: String, profileUuid: String) {
appPrefs.edit()
.putString(startUrl, profileUuid)
.apply()
}
private fun resolveLaunchToken(
shortcutManager: ShortcutManager,
shortcutId: String,
startUrl: String,
profileUuid: String,
contextId: String?,
): String {
val storedToken = getStoredLaunchToken(startUrl, profileUuid)
val storedToken = getStoredLaunchToken(startUrl, profileUuid, contextId)
val existingShortcutToken = shortcutManager.pinnedShortcuts
.firstOrNull { shortcut -> shortcut.id == shortcutId }
?.intent
@@ -464,7 +622,7 @@ class GeckoPwaApiImpl(
?.getStringExtra(PwaConstants.EXTRA_PWA_TOKEN)
if (!existingShortcutToken.isNullOrEmpty()) {
val committed = storeLaunchToken(startUrl, profileUuid, existingShortcutToken)
val committed = storeLaunchToken(startUrl, profileUuid, contextId, existingShortcutToken)
if (!committed) {
logger.warn("Failed to persist pinned shortcut PWA token for $startUrl")
}
@@ -472,7 +630,7 @@ class GeckoPwaApiImpl(
}
if (!storedToken.isNullOrEmpty()) {
val committed = storeLaunchToken(startUrl, profileUuid, storedToken)
val committed = storeLaunchToken(startUrl, profileUuid, contextId, storedToken)
if (!committed) {
logger.warn("Failed to refresh stored PWA launch token index for $startUrl")
}
@@ -480,27 +638,40 @@ class GeckoPwaApiImpl(
}
val generatedToken = UUID.randomUUID().toString()
val committed = storeLaunchToken(startUrl, profileUuid, generatedToken)
val committed = storeLaunchToken(startUrl, profileUuid, contextId, generatedToken)
if (!committed) {
logger.warn("Failed to persist PWA launch token for $startUrl")
}
return generatedToken
}
private fun getStoredLaunchToken(startUrl: String, profileUuid: String): String? {
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}"
return appPrefs.getString(tokenKey, null)
private fun tokenKey(startUrl: String, profileUuid: String, contextId: String?): String {
// Keyed by (startUrl, profileUuid, contextId) so multiple installs of
// the same URL with different storage contexts each keep their own
// token and don't share or overwrite each other.
return "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}::${contextId.orEmpty()}"
}
private fun storeLaunchToken(startUrl: String, profileUuid: String, token: String): Boolean {
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}"
return appPrefs.edit()
.putString(tokenKey, token)
.commit()
private fun legacyTokenKey(startUrl: String, profileUuid: String): String {
return "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}"
}
private fun getProfileMapping(startUrl: String): String? {
return appPrefs.getString(startUrl, null)
private fun getStoredLaunchToken(startUrl: String, profileUuid: String, contextId: String?): String? {
return appPrefs.getString(tokenKey(startUrl, profileUuid, contextId), null)
?: if (contextId.isNullOrEmpty()) {
appPrefs.getString(legacyTokenKey(startUrl, profileUuid), null)
} else {
null
}
}
private fun storeLaunchToken(startUrl: String, profileUuid: String, contextId: String?, token: String): Boolean {
return appPrefs.edit().apply {
putString(tokenKey(startUrl, profileUuid, contextId), token)
if (contextId.isNullOrEmpty()) {
putString(legacyTokenKey(startUrl, profileUuid), token)
}
}.commit()
}
private fun getCurrentProfileUuid(): String? {
@@ -517,10 +688,16 @@ class GeckoPwaApiImpl(
}
}
private fun WebAppManifest.toPwaManifest(currentUrl: String = startUrl): PwaManifest {
private fun WebAppManifest.toPwaManifest(
currentUrl: String = startUrl,
contextId: String? = null,
installLabel: String? = null,
): PwaManifest {
return PwaManifest(
startUrl = startUrl,
currentUrl = currentUrl,
contextId = contextId,
installLabel = installLabel,
name = name,
shortName = shortName,
display = display?.name?.lowercase()?.replace("_", "-"),
@@ -5124,7 +5124,22 @@ data class PwaManifest (
* The URL of the page when the manifest was detected.
* Used for HTTPS/installability checks.
*/
val currentUrl: String
val currentUrl: String,
/**
* Storage context (container contextualIdentity or isolated `iso1_` id)
* that this install was pinned with. Only populated by
* `getInstalledWebApps` read from the pinned shortcut's intent extras,
* not from the manifest itself, because the same URL can have multiple
* installs that differ only in contextId.
*/
val contextId: String? = null,
/**
* User-chosen launcher label for this specific install (from the pinned
* shortcut's shortLabel). May differ from `name`/`shortName` because the
* underlying manifest is shared across install variants of the same URL.
* Only populated by `getInstalledWebApps`.
*/
val installLabel: String? = null
)
{
companion object {
@@ -5145,7 +5160,9 @@ data class PwaManifest (
val preferRelatedApplications = pigeonVar_list[13] as Boolean
val shareTarget = pigeonVar_list[14] as ShareTarget?
val currentUrl = pigeonVar_list[15] as String
return PwaManifest(startUrl, name, shortName, display, themeColor, backgroundColor, scope, description, icons, dir, lang, orientation, relatedApplications, preferRelatedApplications, shareTarget, currentUrl)
val contextId = pigeonVar_list[16] as String?
val installLabel = pigeonVar_list[17] as String?
return PwaManifest(startUrl, name, shortName, display, themeColor, backgroundColor, scope, description, icons, dir, lang, orientation, relatedApplications, preferRelatedApplications, shareTarget, currentUrl, contextId, installLabel)
}
}
fun toList(): List<Any?> {
@@ -5166,6 +5183,8 @@ data class PwaManifest (
preferRelatedApplications,
shareTarget,
currentUrl,
contextId,
installLabel,
)
}
override fun equals(other: Any?): Boolean {
@@ -5176,7 +5195,7 @@ data class PwaManifest (
return true
}
val other = other as PwaManifest
return GeckoPigeonUtils.deepEquals(this.startUrl, other.startUrl) && GeckoPigeonUtils.deepEquals(this.name, other.name) && GeckoPigeonUtils.deepEquals(this.shortName, other.shortName) && GeckoPigeonUtils.deepEquals(this.display, other.display) && GeckoPigeonUtils.deepEquals(this.themeColor, other.themeColor) && GeckoPigeonUtils.deepEquals(this.backgroundColor, other.backgroundColor) && GeckoPigeonUtils.deepEquals(this.scope, other.scope) && GeckoPigeonUtils.deepEquals(this.description, other.description) && GeckoPigeonUtils.deepEquals(this.icons, other.icons) && GeckoPigeonUtils.deepEquals(this.dir, other.dir) && GeckoPigeonUtils.deepEquals(this.lang, other.lang) && GeckoPigeonUtils.deepEquals(this.orientation, other.orientation) && GeckoPigeonUtils.deepEquals(this.relatedApplications, other.relatedApplications) && GeckoPigeonUtils.deepEquals(this.preferRelatedApplications, other.preferRelatedApplications) && GeckoPigeonUtils.deepEquals(this.shareTarget, other.shareTarget) && GeckoPigeonUtils.deepEquals(this.currentUrl, other.currentUrl)
return GeckoPigeonUtils.deepEquals(this.startUrl, other.startUrl) && GeckoPigeonUtils.deepEquals(this.name, other.name) && GeckoPigeonUtils.deepEquals(this.shortName, other.shortName) && GeckoPigeonUtils.deepEquals(this.display, other.display) && GeckoPigeonUtils.deepEquals(this.themeColor, other.themeColor) && GeckoPigeonUtils.deepEquals(this.backgroundColor, other.backgroundColor) && GeckoPigeonUtils.deepEquals(this.scope, other.scope) && GeckoPigeonUtils.deepEquals(this.description, other.description) && GeckoPigeonUtils.deepEquals(this.icons, other.icons) && GeckoPigeonUtils.deepEquals(this.dir, other.dir) && GeckoPigeonUtils.deepEquals(this.lang, other.lang) && GeckoPigeonUtils.deepEquals(this.orientation, other.orientation) && GeckoPigeonUtils.deepEquals(this.relatedApplications, other.relatedApplications) && GeckoPigeonUtils.deepEquals(this.preferRelatedApplications, other.preferRelatedApplications) && GeckoPigeonUtils.deepEquals(this.shareTarget, other.shareTarget) && GeckoPigeonUtils.deepEquals(this.currentUrl, other.currentUrl) && GeckoPigeonUtils.deepEquals(this.contextId, other.contextId) && GeckoPigeonUtils.deepEquals(this.installLabel, other.installLabel)
}
override fun hashCode(): Int {
@@ -5197,6 +5216,8 @@ data class PwaManifest (
result = 31 * result + GeckoPigeonUtils.deepHash(this.preferRelatedApplications)
result = 31 * result + GeckoPigeonUtils.deepHash(this.shareTarget)
result = 31 * result + GeckoPigeonUtils.deepHash(this.currentUrl)
result = 31 * result + GeckoPigeonUtils.deepHash(this.contextId)
result = 31 * result + GeckoPigeonUtils.deepHash(this.installLabel)
return result
}
}
@@ -10635,9 +10656,10 @@ interface GeckoPwaApi {
* The [tabId] identifies which tab to install from. If null, uses the selected tab.
* The [profileUuid] is the UUID of the current user profile.
* The [contextId] is the container's contextual identity (optional, null for default container).
* The [overrideAppName] customizes the installed app's displayed name and persists in the saved manifest.
* Returns true if installation was successful.
*/
fun installWebApp(tabId: String?, profileUuid: String, contextId: String?, callback: (Result<Boolean>) -> Unit)
fun installWebApp(tabId: String?, profileUuid: String, contextId: String?, overrideAppName: String?, callback: (Result<Boolean>) -> Unit)
/** Returns a list of all installed PWA manifests. */
fun getInstalledWebApps(callback: (Result<List<PwaManifest>>) -> Unit)
/**
@@ -10672,7 +10694,8 @@ interface GeckoPwaApi {
val tabIdArg = args[0] as String?
val profileUuidArg = args[1] as String
val contextIdArg = args[2] as String?
api.installWebApp(tabIdArg, profileUuidArg, contextIdArg) { result: Result<Boolean> ->
val overrideAppNameArg = args[3] as String?
api.installWebApp(tabIdArg, profileUuidArg, contextIdArg, overrideAppNameArg) { result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(GeckoPigeonUtils.wrapError(error))
@@ -5474,6 +5474,8 @@ class PwaManifest {
required this.preferRelatedApplications,
this.shareTarget,
required this.currentUrl,
this.contextId,
this.installLabel,
});
String startUrl;
@@ -5510,6 +5512,19 @@ class PwaManifest {
/// Used for HTTPS/installability checks.
String currentUrl;
/// Storage context (container contextualIdentity or isolated `iso1_…` id)
/// that this install was pinned with. Only populated by
/// `getInstalledWebApps` — read from the pinned shortcut's intent extras,
/// not from the manifest itself, because the same URL can have multiple
/// installs that differ only in contextId.
String? contextId;
/// User-chosen launcher label for this specific install (from the pinned
/// shortcut's shortLabel). May differ from `name`/`shortName` because the
/// underlying manifest is shared across install variants of the same URL.
/// Only populated by `getInstalledWebApps`.
String? installLabel;
List<Object?> _toList() {
return <Object?>[
startUrl,
@@ -5528,6 +5543,8 @@ class PwaManifest {
preferRelatedApplications,
shareTarget,
currentUrl,
contextId,
installLabel,
];
}
@@ -5553,6 +5570,8 @@ class PwaManifest {
preferRelatedApplications: result[13]! as bool,
shareTarget: result[14] as ShareTarget?,
currentUrl: result[15]! as String,
contextId: result[16] as String?,
installLabel: result[17] as String?,
);
}
@@ -5565,7 +5584,7 @@ class PwaManifest {
if (identical(this, other)) {
return true;
}
return _deepEquals(startUrl, other.startUrl) && _deepEquals(name, other.name) && _deepEquals(shortName, other.shortName) && _deepEquals(display, other.display) && _deepEquals(themeColor, other.themeColor) && _deepEquals(backgroundColor, other.backgroundColor) && _deepEquals(scope, other.scope) && _deepEquals(description, other.description) && _deepEquals(icons, other.icons) && _deepEquals(dir, other.dir) && _deepEquals(lang, other.lang) && _deepEquals(orientation, other.orientation) && _deepEquals(relatedApplications, other.relatedApplications) && _deepEquals(preferRelatedApplications, other.preferRelatedApplications) && _deepEquals(shareTarget, other.shareTarget) && _deepEquals(currentUrl, other.currentUrl);
return _deepEquals(startUrl, other.startUrl) && _deepEquals(name, other.name) && _deepEquals(shortName, other.shortName) && _deepEquals(display, other.display) && _deepEquals(themeColor, other.themeColor) && _deepEquals(backgroundColor, other.backgroundColor) && _deepEquals(scope, other.scope) && _deepEquals(description, other.description) && _deepEquals(icons, other.icons) && _deepEquals(dir, other.dir) && _deepEquals(lang, other.lang) && _deepEquals(orientation, other.orientation) && _deepEquals(relatedApplications, other.relatedApplications) && _deepEquals(preferRelatedApplications, other.preferRelatedApplications) && _deepEquals(shareTarget, other.shareTarget) && _deepEquals(currentUrl, other.currentUrl) && _deepEquals(contextId, other.contextId) && _deepEquals(installLabel, other.installLabel);
}
@override
@@ -10499,15 +10518,16 @@ class GeckoPwaApi {
/// The [tabId] identifies which tab to install from. If null, uses the selected tab.
/// The [profileUuid] is the UUID of the current user profile.
/// The [contextId] is the container's contextual identity (optional, null for default container).
/// The [overrideAppName] customizes the installed app's displayed name and persists in the saved manifest.
/// Returns true if installation was successful.
Future<bool> installWebApp(String? tabId, String profileUuid, String? contextId) async {
Future<bool> installWebApp(String? tabId, String profileUuid, String? contextId, String? overrideAppName) async {
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoPwaApi.installWebApp$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[tabId, profileUuid, contextId]);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[tabId, profileUuid, contextId, overrideAppName]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
@@ -2673,6 +2673,19 @@ class PwaManifest {
/// Used for HTTPS/installability checks.
final String currentUrl;
/// Storage context (container contextualIdentity or isolated `iso1_…` id)
/// that this install was pinned with. Only populated by
/// `getInstalledWebApps` — read from the pinned shortcut's intent extras,
/// not from the manifest itself, because the same URL can have multiple
/// installs that differ only in contextId.
final String? contextId;
/// User-chosen launcher label for this specific install (from the pinned
/// shortcut's shortLabel). May differ from `name`/`shortName` because the
/// underlying manifest is shared across install variants of the same URL.
/// Only populated by `getInstalledWebApps`.
final String? installLabel;
const PwaManifest({
required this.startUrl,
required this.currentUrl,
@@ -2690,6 +2703,8 @@ class PwaManifest {
this.relatedApplications = const [],
this.preferRelatedApplications = false,
this.shareTarget,
this.contextId,
this.installLabel,
});
}
@@ -2708,9 +2723,15 @@ abstract class GeckoPwaApi {
/// The [tabId] identifies which tab to install from. If null, uses the selected tab.
/// The [profileUuid] is the UUID of the current user profile.
/// The [contextId] is the container's contextual identity (optional, null for default container).
/// The [overrideAppName] customizes the installed app's displayed name and persists in the saved manifest.
/// Returns true if installation was successful.
@async
bool installWebApp(String? tabId, String profileUuid, String? contextId);
bool installWebApp(
String? tabId,
String profileUuid,
String? contextId,
String? overrideAppName,
);
/// Returns a list of all installed PWA manifests.
@async