add token based security; improve stability;

This commit is contained in:
Fabian Freund
2026-02-11 07:24:27 +01:00
parent 5cdeee6526
commit 8e25814a67
5 changed files with 173 additions and 85 deletions
@@ -152,7 +152,8 @@ object GlobalComponents {
newComponents.core.engine.warmUp() newComponents.core.engine.warmUp()
if (previousCustomTabs.isNotEmpty()) { fun restorePreviousCustomTabs() {
if (previousCustomTabs.isEmpty()) return
for (tab in previousCustomTabs) { for (tab in previousCustomTabs) {
val existing = newComponents.core.store.state.findCustomTab(tab.id) val existing = newComponents.core.store.state.findCustomTab(tab.id)
if (existing == null) { if (existing == null) {
@@ -164,7 +165,14 @@ object GlobalComponents {
} }
if (mode == ComponentsMode.FULL) { if (mode == ComponentsMode.FULL) {
restoreBrowserState(newComponents) val restoreJob = restoreBrowserState(newComponents)
if (previousCustomTabs.isNotEmpty()) {
restoreJob.invokeOnCompletion {
GlobalScope.launch(Dispatchers.Main) {
restorePreviousCustomTabs()
}
}
}
restoreDownloads(newComponents) restoreDownloads(newComponents)
try { try {
@@ -205,6 +213,8 @@ object GlobalComponents {
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
newComponents.core.fileUploadsDirCleaner.cleanUploadsDirectory() newComponents.core.fileUploadsDirCleaner.cleanUploadsDirectory()
} }
} else {
restorePreviousCustomTabs()
} }
} }
@@ -13,10 +13,12 @@ object PwaConstants {
// Intent extras keys for PWA metadata // Intent extras keys for PWA metadata
const val EXTRA_PWA_PROFILE_UUID = "pwa_profile_uuid" const val EXTRA_PWA_PROFILE_UUID = "pwa_profile_uuid"
const val EXTRA_PWA_CONTEXT_ID = "pwa_context_id" const val EXTRA_PWA_CONTEXT_ID = "pwa_context_id"
const val EXTRA_PWA_TOKEN = "pwa_token"
// Profile and file paths // Profile and file paths
const val CURRENT_PROFILE_FILE = "weblibre_profiles/current_profile" const val CURRENT_PROFILE_FILE = "weblibre_profiles/current_profile"
const val PROFILE_MAPPING_PREFS = "pwa_profile_mapping" const val PROFILE_MAPPING_PREFS = "pwa_profile_mapping"
const val PROFILE_MAPPING_TOKEN_PREFIX = "token_"
// Component initialization timeouts // Component initialization timeouts
const val COMPONENT_INIT_TIMEOUT_MS = 10000L const val COMPONENT_INIT_TIMEOUT_MS = 10000L
@@ -9,6 +9,7 @@ package eu.weblibre.flutter_mozilla_components.activities
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import eu.weblibre.flutter_mozilla_components.ExternalAppBrowserFragment import eu.weblibre.flutter_mozilla_components.ExternalAppBrowserFragment
@@ -33,6 +34,24 @@ import mozilla.components.support.base.log.logger.Logger
* Uses an empty taskAffinity so Custom Tabs appear as a separate task from the main app. * Uses an empty taskAffinity so Custom Tabs appear as a separate task from the main app.
*/ */
class ExternalAppBrowserActivity : AppCompatActivity() { class ExternalAppBrowserActivity : AppCompatActivity() {
companion object {
private const val TAG = "ExternalAppBrowserActivity"
const val EXTRA_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
const val EXTRA_WEB_APP_MANIFEST_URL = "web_app_manifest_url"
fun createIntent(
context: Context,
customTabSessionId: String,
webAppManifestUrl: String? = null,
): Intent {
return Intent(context, ExternalAppBrowserActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
putExtra(EXTRA_CUSTOM_TAB_SESSION_ID, customTabSessionId)
webAppManifestUrl?.let { putExtra(EXTRA_WEB_APP_MANIFEST_URL, it) }
}
}
}
private val logger = Logger("ExternalAppBrowserActivity") private val logger = Logger("ExternalAppBrowserActivity")
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
@@ -48,8 +67,9 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
val sessionId = customTabSessionId val sessionId = customTabSessionId
if (sessionId == null) { if (sessionId == null) {
Log.e(TAG, "No custom tab session ID provided")
logger.error("No custom tab session ID provided, finishing.") logger.error("No custom tab session ID provided, finishing.")
finish() fallbackToMainActivity()
return return
} }
@@ -87,8 +107,9 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
// Timeout reached // Timeout reached
if (isActive) { if (isActive) {
Log.e(TAG, "Timeout waiting for components")
logger.error("Timeout waiting for components after ${PwaConstants.COMPONENT_INIT_TIMEOUT_MS}ms") logger.error("Timeout waiting for components after ${PwaConstants.COMPONENT_INIT_TIMEOUT_MS}ms")
finish() fallbackToMainActivity()
} }
} }
} }
@@ -102,8 +123,9 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
// Verify session exists // Verify session exists
if (components.core.store.state.findCustomTab(sessionId) == null) { if (components.core.store.state.findCustomTab(sessionId) == null) {
Log.e(TAG, "Custom tab session $sessionId not found in store")
logger.error("Custom tab session $sessionId not found in store, finishing.") logger.error("Custom tab session $sessionId not found in store, finishing.")
finish() fallbackToMainActivity()
return return
} }
@@ -124,11 +146,25 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
val sessionId = customTabSessionId ?: return val sessionId = customTabSessionId ?: return
val components = GlobalComponents.components ?: return val components = GlobalComponents.components ?: return
if (components.core.store.state.findCustomTab(sessionId) == null) { if (components.core.store.state.findCustomTab(sessionId) == null) {
Log.w(TAG, "Custom tab session $sessionId gone on resume")
logger.debug("Custom tab session $sessionId gone, finishing activity.") logger.debug("Custom tab session $sessionId gone, finishing activity.")
finish() fallbackToMainActivity()
} }
} }
private fun fallbackToMainActivity() {
val mainIntent = Intent().apply {
setClassName(this@ExternalAppBrowserActivity, "eu.weblibre.gecko.MainActivity")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
webAppManifestUrl?.let {
action = Intent.ACTION_VIEW
data = android.net.Uri.parse(it)
}
}
startActivity(mainIntent)
finish()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
@@ -168,20 +204,4 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
} }
} }
companion object {
const val EXTRA_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id"
const val EXTRA_WEB_APP_MANIFEST_URL = "web_app_manifest_url"
fun createIntent(
context: Context,
customTabSessionId: String,
webAppManifestUrl: String? = null,
): Intent {
return Intent(context, ExternalAppBrowserActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
putExtra(EXTRA_CUSTOM_TAB_SESSION_ID, customTabSessionId)
webAppManifestUrl?.let { putExtra(EXTRA_WEB_APP_MANIFEST_URL, it) }
}
}
}
} }
@@ -8,22 +8,23 @@ package eu.weblibre.flutter_mozilla_components.activities
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log
import eu.weblibre.flutter_mozilla_components.Components
import eu.weblibre.flutter_mozilla_components.GlobalComponents import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.PwaConstants import eu.weblibre.flutter_mozilla_components.PwaConstants
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.feature.customtabs.CustomTabIntentProcessor import mozilla.components.feature.customtabs.CustomTabIntentProcessor
import mozilla.components.feature.intent.ext.getSessionId import mozilla.components.feature.intent.ext.getSessionId
import mozilla.components.feature.pwa.intent.WebAppIntentProcessor import mozilla.components.feature.pwa.intent.WebAppIntentProcessor
import mozilla.components.support.base.log.logger.Logger import mozilla.components.browser.state.state.ExternalAppType
import java.io.File import java.io.File
/** /**
@@ -37,17 +38,18 @@ import java.io.File
* if the current profile differs from the installation profile. * if the current profile differs from the installation profile.
*/ */
class IntentReceiverActivity : Activity() { class IntentReceiverActivity : Activity() {
companion object {
private const val TAG = "IntentReceiverActivity"
}
private val logger = Logger("IntentReceiverActivity")
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
private var pendingIntent: Intent? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val intent = intent?.let { Intent(it) } ?: Intent() val intent = intent?.let { Intent(it) } ?: Intent()
logger.debug("onCreate: action=${intent.action} data=${intent.dataString}") Log.d(TAG, "onCreate: action=${intent.action} data=${intent.dataString}")
// Strip flags that could interfere with task management // Strip flags that could interfere with task management
intent.flags = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv() intent.flags = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv()
@@ -68,9 +70,8 @@ class IntentReceiverActivity : Activity() {
return return
} }
logger.warn("Components not initialized, waiting for initialization...") Log.w(TAG, "Components not initialized, routing directly to MainActivity")
pendingIntent = intent handleRegularIntent(intent)
waitForComponentsWithTimeout()
return return
} }
@@ -81,16 +82,21 @@ class IntentReceiverActivity : Activity() {
// Check if this is our custom PWA intent with profile metadata // Check if this is our custom PWA intent with profile metadata
val profileUuid = intent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) val profileUuid = intent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID)
val contextId = intent.getStringExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID) val contextId = intent.getStringExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID)
val token = intent.getStringExtra(PwaConstants.EXTRA_PWA_TOKEN)
if (profileUuid != null) { if (profileUuid != null) {
logger.debug("PWA intent with profile metadata: profileUuid=$profileUuid, contextId=$contextId") if (isTrustedPwaLaunch(intent, profileUuid, token)) {
handlePwaIntent(intent, profileUuid, contextId) Log.d(TAG, "Trusted PWA intent with profile metadata: profileUuid=$profileUuid, contextId=$contextId")
return handlePwaIntent(intent, profileUuid, contextId)
return
}
Log.w(TAG, "Ignoring untrusted PWA profile metadata on VIEW intent")
} }
// Fall back to standard intent processors for Custom Tabs and legacy PWAs // Fall back to standard intent processors for Custom Tabs and legacy PWAs
val components = GlobalComponents.components val components = GlobalComponents.components
?: run { ?: run {
logger.error("Components became null during routing") Log.e(TAG, "Components became null during routing")
handleRegularIntent(intent) handleRegularIntent(intent)
return return
} }
@@ -110,13 +116,14 @@ class IntentReceiverActivity : Activity() {
) )
for ((name, processor) in processors) { for ((name, processor) in processors) {
logger.debug("Trying $name processor...") Log.d(TAG, "Trying $name processor...")
try { try {
val result = processor.process(intent) val result = processor.process(intent)
logger.debug("$name processor result: $result") Log.d(TAG, "$name processor result: $result")
if (result) { if (result) {
val sessionId = intent.getSessionId() val sessionId = intent.getSessionId()
logger.debug("$name session ID from intent: $sessionId") ?: resolveSessionIdFromStore(name, intent, components)
Log.d(TAG, "$name session ID from intent: $sessionId")
if (sessionId != null) { if (sessionId != null) {
val externalIntent = ExternalAppBrowserActivity.createIntent( val externalIntent = ExternalAppBrowserActivity.createIntent(
context = this, context = this,
@@ -127,18 +134,79 @@ class IntentReceiverActivity : Activity() {
finish() finish()
return return
} else { } else {
logger.warn("$name processor succeeded but no session ID in intent!") Log.w(TAG, "$name processor succeeded but no session ID in intent!")
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Error in $name processor", e) Log.e(TAG, "Error in $name processor", e)
} }
} }
logger.debug("No processor matched, routing to MainActivity") Log.d(TAG, "No processor matched, routing to MainActivity")
handleRegularIntent(intent) handleRegularIntent(intent)
} }
private fun isTrustedPwaLaunch(intent: Intent, profileUuid: String, token: String?): Boolean {
val url = intent.dataString ?: return false
val action = intent.action
val hasTrustedAction = action == Intent.ACTION_VIEW || action == "mozilla.components.feature.pwa.VIEW_PWA"
if (!hasTrustedAction || token.isNullOrEmpty()) {
return false
}
val prefs = applicationContext.getSharedPreferences(
PwaConstants.PROFILE_MAPPING_PREFS,
Context.MODE_PRIVATE,
)
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${url}::${profileUuid}"
val storedToken = prefs.getString(tokenKey, null)
if (storedToken == null || storedToken != token) {
Log.w(TAG, "PWA token mismatch for $url")
return false
}
return true
}
private fun resolveSessionIdFromStore(
processorName: String,
intent: Intent,
components: Components,
): String? {
val customTabs = components.core.store.state.customTabs
if (customTabs.isEmpty()) {
return null
}
return when (processorName) {
"PWA" -> {
val targetUrl = intent.dataString
val pwaTab = customTabs.lastOrNull { tab ->
val appType = tab.config.externalAppType
val isPwa = appType == ExternalAppType.PROGRESSIVE_WEB_APP ||
appType == ExternalAppType.TRUSTED_WEB_ACTIVITY
if (!isPwa) {
return@lastOrNull false
}
if (targetUrl == null) {
true
} else {
tab.content.url == targetUrl || tab.content.webAppManifest?.startUrl == targetUrl
}
} ?: customTabs.lastOrNull { tab ->
val appType = tab.config.externalAppType
appType == ExternalAppType.PROGRESSIVE_WEB_APP ||
appType == ExternalAppType.TRUSTED_WEB_ACTIVITY
}
pwaTab?.id
}
else -> customTabs.lastOrNull()?.id
}
}
/** /**
* Handles PWA intents with profile and context metadata. * Handles PWA intents with profile and context metadata.
* Checks if current profile matches and shows dialog if different. * Checks if current profile matches and shows dialog if different.
@@ -150,7 +218,7 @@ class IntentReceiverActivity : Activity() {
) { ) {
val url = intent.dataString val url = intent.dataString
if (url == null) { if (url == null) {
logger.error("PWA intent has no URL") Log.e(TAG, "PWA intent has no URL")
handleRegularIntent(intent) handleRegularIntent(intent)
return return
} }
@@ -158,10 +226,10 @@ class IntentReceiverActivity : Activity() {
val currentProfileUuid = getCurrentProfileUuid() val currentProfileUuid = getCurrentProfileUuid()
if (currentProfileUuid != null && currentProfileUuid != profileUuid) { if (currentProfileUuid != null && currentProfileUuid != profileUuid) {
logger.debug("Profile mismatch: current=$currentProfileUuid, expected=$profileUuid") Log.d(TAG, "Profile mismatch: current=$currentProfileUuid, expected=$profileUuid")
showProfileMismatchDialog(url, contextId) showProfileMismatchDialog(url, contextId)
} else { } else {
logger.debug("Profile match or indeterminate, launching PWA with contextId=$contextId") Log.d(TAG, "Profile match or indeterminate, launching PWA with contextId=$contextId")
launchPwaWithContext(url, contextId) launchPwaWithContext(url, contextId)
} }
} }
@@ -180,7 +248,7 @@ class IntentReceiverActivity : Activity() {
null null
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Failed to read current profile UUID", e) Log.e(TAG, "Failed to read current profile UUID", e)
null null
} }
} }
@@ -202,11 +270,11 @@ class IntentReceiverActivity : Activity() {
.setTitle("PWA Profile Mismatch") .setTitle("PWA Profile Mismatch")
.setMessage(message) .setMessage(message)
.setPositiveButton("Open Anyway") { _, _ -> .setPositiveButton("Open Anyway") { _, _ ->
logger.debug("User chose to open PWA despite profile mismatch") Log.d(TAG, "User chose to open PWA despite profile mismatch")
launchPwaWithContext(url, contextId) launchPwaWithContext(url, contextId)
} }
.setNegativeButton("Cancel") { _, _ -> .setNegativeButton("Cancel") { _, _ ->
logger.debug("User cancelled PWA launch due to profile mismatch") Log.d(TAG, "User cancelled PWA launch due to profile mismatch")
finish() finish()
} }
.setOnCancelListener { .setOnCancelListener {
@@ -221,7 +289,7 @@ class IntentReceiverActivity : Activity() {
private fun launchPwaWithContext(url: String, contextId: String?) { private fun launchPwaWithContext(url: String, contextId: String?) {
val components = GlobalComponents.components val components = GlobalComponents.components
?: run { ?: run {
logger.error("Components not available for PWA launch") Log.e(TAG, "Components not available for PWA launch")
handleRegularIntent(intent) handleRegularIntent(intent)
return return
} }
@@ -238,7 +306,7 @@ class IntentReceiverActivity : Activity() {
manifest = manifest manifest = manifest
) )
logger.debug("Created PWA session: contextId=$contextId, sessionId=$sessionId") Log.d(TAG, "Created PWA session: contextId=$contextId, sessionId=$sessionId")
val externalIntent = ExternalAppBrowserActivity.createIntent( val externalIntent = ExternalAppBrowserActivity.createIntent(
context = this@IntentReceiverActivity, context = this@IntentReceiverActivity,
@@ -248,7 +316,7 @@ class IntentReceiverActivity : Activity() {
startActivity(externalIntent) startActivity(externalIntent)
finish() finish()
} catch (e: Exception) { } catch (e: Exception) {
logger.error("Failed to launch PWA with context", e) Log.e(TAG, "Failed to launch PWA with context", e)
handleRegularIntent(intent) handleRegularIntent(intent)
} }
} }
@@ -288,37 +356,6 @@ class IntentReceiverActivity : Activity() {
return tab.id return tab.id
} }
/**
* Waits for GlobalComponents to be initialized with a timeout.
* Once components are ready, shows branded loading screen before routing.
* Falls back to MainActivity if timeout is reached (10 seconds).
*/
private fun waitForComponentsWithTimeout() {
coroutineScope.launch {
var elapsedMs = 0L
while (isActive && elapsedMs < PwaConstants.COMPONENT_INIT_TIMEOUT_MS) {
if (GlobalComponents.components != null) {
logger.debug("Components initialized after ${elapsedMs}ms")
pendingIntent?.let { routeIntent(it) }
pendingIntent = null
return@launch
}
delay(PwaConstants.COMPONENT_INIT_CHECK_INTERVAL_MS)
elapsedMs += PwaConstants.COMPONENT_INIT_CHECK_INTERVAL_MS
}
if (isActive) {
logger.warn("Timeout waiting for components after ${PwaConstants.COMPONENT_INIT_TIMEOUT_MS}ms, falling back to MainActivity")
pendingIntent?.let { intent ->
handleRegularIntent(intent)
}
pendingIntent = null
}
}
}
private fun handleRegularIntent(intent: Intent) { private fun handleRegularIntent(intent: Intent) {
val mainActivityIntent = Intent(intent).apply { val mainActivityIntent = Intent(intent).apply {
setClassName(this@IntentReceiverActivity, "eu.weblibre.gecko.MainActivity") setClassName(this@IntentReceiverActivity, "eu.weblibre.gecko.MainActivity")
@@ -37,6 +37,7 @@ import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import java.io.File import java.io.File
import java.security.MessageDigest import java.security.MessageDigest
import java.util.UUID
/** /**
* Implementation of GeckoPwaApi that provides PWA install and query functionality. * Implementation of GeckoPwaApi that provides PWA install and query functionality.
@@ -52,6 +53,12 @@ class GeckoPwaApiImpl(
} }
private val logger = Logger("GeckoPwaApiImpl") private val logger = Logger("GeckoPwaApiImpl")
private val appPrefs by lazy {
context.applicationContext.getSharedPreferences(
PwaConstants.PROFILE_MAPPING_PREFS,
Context.MODE_PRIVATE,
)
}
private val components by lazy { private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" } requireNotNull(GlobalComponents.components) { "Components not initialized" }
@@ -138,6 +145,7 @@ class GeckoPwaApiImpl(
val (iconBitmap, isMaskable) = loadPwaIcon(manifest) val (iconBitmap, isMaskable) = loadPwaIcon(manifest)
val shortcutId = generateShortcutId(manifest.startUrl) val shortcutId = generateShortcutId(manifest.startUrl)
val launchToken = generateAndStoreLaunchToken(manifest.startUrl, profileUuid)
val appName = manifest.shortName ?: manifest.name ?: "Web App" val appName = manifest.shortName ?: manifest.name ?: "Web App"
@@ -146,6 +154,7 @@ class GeckoPwaApiImpl(
data = Uri.parse(manifest.startUrl) data = Uri.parse(manifest.startUrl)
putExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID, profileUuid) putExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID, profileUuid)
putExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID, contextId) putExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID, contextId)
putExtra(PwaConstants.EXTRA_PWA_TOKEN, launchToken)
} }
val shortcut = ShortcutInfo.Builder(context, shortcutId).apply { val shortcut = ShortcutInfo.Builder(context, shortcutId).apply {
@@ -247,15 +256,25 @@ class GeckoPwaApiImpl(
} }
private fun storeProfileMapping(startUrl: String, profileUuid: String) { private fun storeProfileMapping(startUrl: String, profileUuid: String) {
context.getSharedPreferences(PwaConstants.PROFILE_MAPPING_PREFS, Context.MODE_PRIVATE) appPrefs.edit()
.edit()
.putString(startUrl, profileUuid) .putString(startUrl, profileUuid)
.apply() .apply()
} }
private fun generateAndStoreLaunchToken(startUrl: String, profileUuid: String): String {
val token = UUID.randomUUID().toString()
val tokenKey = "${PwaConstants.PROFILE_MAPPING_TOKEN_PREFIX}${startUrl}::${profileUuid}"
val committed = appPrefs.edit()
.putString(tokenKey, token)
.commit()
if (!committed) {
logger.warn("Failed to persist PWA launch token for $startUrl")
}
return token
}
private fun getProfileMapping(startUrl: String): String? { private fun getProfileMapping(startUrl: String): String? {
return context.getSharedPreferences(PwaConstants.PROFILE_MAPPING_PREFS, Context.MODE_PRIVATE) return appPrefs.getString(startUrl, null)
.getString(startUrl, null)
} }
private fun getCurrentProfileUuid(): String? { private fun getCurrentProfileUuid(): String? {