improve splash screens
This commit is contained in:
+4
@@ -11,9 +11,13 @@ object PwaConstants {
|
||||
const val EXTRA_PWA_PROFILE_UUID = "pwa_profile_uuid"
|
||||
const val EXTRA_PWA_CONTEXT_ID = "pwa_context_id"
|
||||
|
||||
// Shortcut ID for linking to cached manifest + icon on disk
|
||||
const val EXTRA_PWA_SHORTCUT_ID = "pwa_shortcut_id"
|
||||
|
||||
// Profile and file paths
|
||||
const val CURRENT_PROFILE_FILE = "weblibre_profiles/current_profile"
|
||||
const val PROFILE_MAPPING_PREFS = "pwa_profile_mapping"
|
||||
const val PWA_CACHE_DIR = "pwa_cache"
|
||||
|
||||
// Component initialization timeouts
|
||||
const val COMPONENT_INIT_TIMEOUT_MS = 10000L
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package eu.weblibre.flutter_mozilla_components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.Log
|
||||
import mozilla.components.concept.engine.manifest.WebAppManifest
|
||||
import mozilla.components.concept.engine.manifest.WebAppManifestParser
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
/**
|
||||
* Simple disk cache for PWA splash screen data (manifest + icon).
|
||||
* Keyed by shortcut ID so home screen shortcuts can instantly display
|
||||
* branded splash screens without waiting for components to initialize.
|
||||
*
|
||||
* Cache layout:
|
||||
* <filesDir>/pwa_cache/<shortcutId>/manifest.json
|
||||
* <filesDir>/pwa_cache/<shortcutId>/icon.png
|
||||
*/
|
||||
object PwaSplashCache {
|
||||
|
||||
private const val TAG = "PwaSplashCache"
|
||||
private val parser = WebAppManifestParser()
|
||||
|
||||
private const val MANIFEST_FILE = "manifest.json"
|
||||
private const val ICON_FILE = "icon.png"
|
||||
|
||||
/**
|
||||
* Saves manifest JSON and icon bitmap to disk for the given shortcut ID.
|
||||
*/
|
||||
fun save(context: Context, shortcutId: String, manifest: WebAppManifest, icon: Bitmap?) {
|
||||
try {
|
||||
val dir = cacheDir(context, shortcutId)
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
|
||||
val json = parser.serialize(manifest).toString()
|
||||
File(dir, MANIFEST_FILE).writeText(json)
|
||||
|
||||
icon?.let { bitmap ->
|
||||
FileOutputStream(File(dir, ICON_FILE)).use { out ->
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 90, out)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to cache splash data for $shortcutId", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads cached manifest for the given shortcut ID, or null if not cached.
|
||||
*/
|
||||
fun loadManifest(context: Context, shortcutId: String): WebAppManifest? {
|
||||
return try {
|
||||
val file = File(cacheDir(context, shortcutId), MANIFEST_FILE)
|
||||
if (!file.exists()) return null
|
||||
|
||||
val json = file.readText()
|
||||
when (val result = parser.parse(json)) {
|
||||
is WebAppManifestParser.Result.Success -> result.manifest
|
||||
is WebAppManifestParser.Result.Failure -> {
|
||||
Log.e(TAG, "Failed to parse cached manifest for $shortcutId", result.exception)
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load cached manifest for $shortcutId", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads cached icon bitmap for the given shortcut ID, or null if not cached.
|
||||
*/
|
||||
fun loadIcon(context: Context, shortcutId: String): Bitmap? {
|
||||
return try {
|
||||
val file = File(cacheDir(context, shortcutId), ICON_FILE)
|
||||
if (!file.exists()) return null
|
||||
BitmapFactory.decodeFile(file.absolutePath)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load cached icon for $shortcutId", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun cacheDir(context: Context, shortcutId: String): File {
|
||||
// Always use applicationContext.filesDir to avoid profile-scoped context paths
|
||||
return File(context.applicationContext.filesDir, "${PwaConstants.PWA_CACHE_DIR}/$shortcutId")
|
||||
}
|
||||
}
|
||||
+12
-52
@@ -12,7 +12,6 @@ import android.os.Bundle
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import eu.weblibre.flutter_mozilla_components.Components
|
||||
import eu.weblibre.flutter_mozilla_components.ExternalAppBrowserFragment
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.PwaConstants
|
||||
@@ -25,7 +24,6 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import mozilla.components.browser.state.selector.findCustomTab
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
@@ -75,20 +73,18 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
private fun showLoading() {
|
||||
val container = findViewById<FrameLayout>(R.id.container)
|
||||
loadingScreenManager = LoadingScreenManager.forActivity(this, container)
|
||||
|
||||
// Show branded placeholder immediately based on available data
|
||||
// If we have a manifest URL, it's likely a PWA
|
||||
|
||||
val url = webAppManifestUrl ?: ""
|
||||
val shortcutId = intent?.getStringExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID)
|
||||
|
||||
if (url.isNotEmpty()) {
|
||||
// Try to show PWA placeholder
|
||||
loadingScreenManager?.showLoadingForIntent(
|
||||
Intent().apply {
|
||||
data = android.net.Uri.parse(url)
|
||||
putExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID, "placeholder")
|
||||
}
|
||||
)
|
||||
val loadingIntent = Intent().apply {
|
||||
data = android.net.Uri.parse(url)
|
||||
putExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID, "placeholder")
|
||||
shortcutId?.let { putExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID, it) }
|
||||
}
|
||||
loadingScreenManager?.showLoadingForIntent(loadingIntent)
|
||||
} else {
|
||||
// Show Custom Tab placeholder
|
||||
loadingScreenManager?.showLoadingForIntent(Intent())
|
||||
}
|
||||
}
|
||||
@@ -98,12 +94,7 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
var elapsedMs = 0L
|
||||
|
||||
while (isActive && elapsedMs < PwaConstants.COMPONENT_INIT_TIMEOUT_MS) {
|
||||
val components = GlobalComponents.components
|
||||
if (components != null) {
|
||||
// Enhance the existing loading screen with actual data
|
||||
enhanceLoadingScreen(components, sessionId)
|
||||
// Brief delay to show the enhanced loading screen
|
||||
delay(200)
|
||||
if (GlobalComponents.components != null) {
|
||||
showFragment(sessionId)
|
||||
return@launch
|
||||
}
|
||||
@@ -120,39 +111,6 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the existing loading screen with actual data once components are ready.
|
||||
*/
|
||||
private fun enhanceLoadingScreen(components: Components, sessionId: String) {
|
||||
val session = components.core.store.state.findCustomTab(sessionId) ?: return
|
||||
val url = session.content.url
|
||||
val manifestUrl = webAppManifestUrl
|
||||
|
||||
loadingScreenManager?.let { manager ->
|
||||
when (session.config.externalAppType) {
|
||||
mozilla.components.browser.state.state.ExternalAppType.PROGRESSIVE_WEB_APP,
|
||||
mozilla.components.browser.state.state.ExternalAppType.TRUSTED_WEB_ACTIVITY -> {
|
||||
// Enhance PWA loading with manifest data
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val manifest = manifestUrl?.let { manifestUrl ->
|
||||
components.core.webAppManifestStorage.loadManifest(manifestUrl)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
manifest?.let {
|
||||
manager.enhancePwaLoading(it, components.core.icons, coroutineScope)
|
||||
} ?: manager.enhanceCustomTabLoading(url, components.core.icons, coroutineScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Enhance Custom Tab loading with favicon
|
||||
manager.enhanceCustomTabLoading(url, components.core.icons, coroutineScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFragment(sessionId: String) {
|
||||
val components = GlobalComponents.components ?: run {
|
||||
logger.error("Components still null after waiting, finishing.")
|
||||
@@ -241,11 +199,13 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
context: Context,
|
||||
customTabSessionId: String,
|
||||
webAppManifestUrl: String? = null,
|
||||
pwaShortcutId: 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) }
|
||||
pwaShortcutId?.let { putExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-34
@@ -246,6 +246,7 @@ class IntentReceiverActivity : Activity() {
|
||||
context = this@IntentReceiverActivity,
|
||||
customTabSessionId = sessionId,
|
||||
webAppManifestUrl = url,
|
||||
pwaShortcutId = intent.getStringExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID),
|
||||
)
|
||||
startActivity(externalIntent)
|
||||
finish()
|
||||
@@ -310,33 +311,6 @@ class IntentReceiverActivity : Activity() {
|
||||
loadingScreenManager?.showLoadingForIntent(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the existing loading screen with actual data once components are initialized.
|
||||
* This updates the placeholder with real manifest/icon data.
|
||||
*/
|
||||
private fun enhanceLoadingScreen(intent: Intent) {
|
||||
val components = GlobalComponents.components ?: return
|
||||
val url = intent.dataString ?: return
|
||||
|
||||
loadingScreenManager?.let { manager ->
|
||||
when {
|
||||
// PWA intent - enhance with manifest data
|
||||
LoadingScreenManager.isPwaIntent(intent) -> {
|
||||
coroutineScope.launch {
|
||||
val manifest = components.core.webAppManifestStorage.loadManifest(url)
|
||||
manifest?.let {
|
||||
manager.enhancePwaLoading(it, components.core.icons, coroutineScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Custom Tab - enhance with favicon
|
||||
else -> {
|
||||
manager.enhanceCustomTabLoading(url, components.core.icons, coroutineScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for GlobalComponents to be initialized with a timeout.
|
||||
* Once components are ready, shows branded loading screen before routing.
|
||||
@@ -349,13 +323,7 @@ class IntentReceiverActivity : Activity() {
|
||||
while (isActive && elapsedMs < PwaConstants.COMPONENT_INIT_TIMEOUT_MS) {
|
||||
if (GlobalComponents.components != null) {
|
||||
logger.debug("Components initialized after ${elapsedMs}ms")
|
||||
pendingIntent?.let { intent ->
|
||||
// Enhance the existing loading screen with actual data
|
||||
enhanceLoadingScreen(intent)
|
||||
// Small delay to show the enhanced loading screen (200ms)
|
||||
delay(200)
|
||||
routeIntent(intent)
|
||||
}
|
||||
pendingIntent?.let { routeIntent(it) }
|
||||
pendingIntent = null
|
||||
return@launch
|
||||
}
|
||||
|
||||
+13
-5
@@ -17,6 +17,7 @@ import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.PwaConstants
|
||||
import eu.weblibre.flutter_mozilla_components.PwaSplashCache
|
||||
import eu.weblibre.flutter_mozilla_components.activities.IntentReceiverActivity
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.ExternalApplicationResource
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPwaApi
|
||||
@@ -135,19 +136,26 @@ class GeckoPwaApiImpl(
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
val (iconBitmap, isMaskable) = loadPwaIcon(manifest)
|
||||
|
||||
val shortcutId = generateShortcutId(manifest.startUrl)
|
||||
|
||||
// Cache manifest + icon to disk for instant branded splash at launch
|
||||
PwaSplashCache.save(context, shortcutId, manifest, iconBitmap)
|
||||
|
||||
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)
|
||||
putExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID, profileUuid)
|
||||
putExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID, contextId)
|
||||
putExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID, shortcutId)
|
||||
}
|
||||
|
||||
val (iconBitmap, isMaskable) = loadPwaIcon(manifest)
|
||||
|
||||
val shortcutId = generateShortcutId(manifest.startUrl)
|
||||
val shortcut = ShortcutInfo.Builder(context, shortcutId).apply {
|
||||
setShortLabel(manifest.shortName ?: manifest.name ?: "Web App")
|
||||
setLongLabel(manifest.name ?: manifest.shortName ?: "Web App")
|
||||
setShortLabel(appName)
|
||||
setLongLabel(manifest.name ?: appName)
|
||||
setIntent(shortcutIntent)
|
||||
|
||||
if (iconBitmap != null) {
|
||||
|
||||
+60
-221
@@ -8,203 +8,90 @@ package eu.weblibre.flutter_mozilla_components.ui
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import eu.weblibre.flutter_mozilla_components.PwaConstants
|
||||
import eu.weblibre.flutter_mozilla_components.PwaSplashCache
|
||||
import eu.weblibre.flutter_mozilla_components.R
|
||||
import mozilla.components.browser.icons.BrowserIcons
|
||||
import mozilla.components.browser.icons.IconRequest
|
||||
import mozilla.components.concept.engine.manifest.WebAppManifest
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
/**
|
||||
* Manager for displaying branded loading screens during PWA and Custom Tab initialization.
|
||||
* Handles loading of icons, theming, and animations.
|
||||
* Displays a loading screen during PWA and Custom Tab initialization.
|
||||
*
|
||||
* - PWA with cached data: shows cached icon, app name, and theme color instantly from disk.
|
||||
* - PWA without cache / Custom Tab: shows WebLibre logo with domain name.
|
||||
*
|
||||
* No component dependencies — everything is resolved from intent extras and disk cache.
|
||||
*/
|
||||
class LoadingScreenManager private constructor(
|
||||
private val activity: Activity,
|
||||
private val container: FrameLayout
|
||||
) {
|
||||
private val logger = Logger("LoadingScreenManager")
|
||||
private var currentLoadingView: View? = null
|
||||
private var pulseAnimator: ObjectAnimator? = null
|
||||
|
||||
// Material3 theme wrapper for activities that use non-Material themes (e.g. AppCompat.Translucent)
|
||||
private val themedContext: Context
|
||||
get() = ContextThemeWrapper(activity, com.google.android.material.R.style.Theme_Material3_DayNight_NoActionBar)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates a LoadingScreenManager for the given activity.
|
||||
* The container should be the root view where loading screens will be added.
|
||||
*/
|
||||
fun forActivity(activity: Activity, container: FrameLayout): LoadingScreenManager {
|
||||
return LoadingScreenManager(activity, container)
|
||||
}
|
||||
fun forActivity(activity: Activity, container: FrameLayout) =
|
||||
LoadingScreenManager(activity, container)
|
||||
|
||||
/**
|
||||
* Detects if an intent is for a PWA based on the extras.
|
||||
*/
|
||||
fun isPwaIntent(intent: Intent?): Boolean {
|
||||
return intent?.hasExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) == true
|
||||
}
|
||||
fun isPwaIntent(intent: Intent?) =
|
||||
intent?.hasExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) == true
|
||||
|
||||
/**
|
||||
* Extracts URL from an intent.
|
||||
*/
|
||||
fun extractUrl(intent: Intent?): String? {
|
||||
return intent?.dataString
|
||||
}
|
||||
fun hasCachedSplashData(intent: Intent?) =
|
||||
intent?.hasExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the appropriate loading screen immediately based on intent analysis.
|
||||
* This can be called before components are initialized.
|
||||
*
|
||||
* @param intent The intent to analyze for type detection
|
||||
* Shows the loading screen immediately based on intent analysis.
|
||||
* Can be called before components are initialized.
|
||||
*/
|
||||
fun showLoadingForIntent(intent: Intent) {
|
||||
when {
|
||||
isPwaIntent(intent) -> showPwaPlaceholder(intent)
|
||||
else -> showCustomTabPlaceholder(intent)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a PWA placeholder loading screen immediately (before components are ready).
|
||||
* Uses URL to extract domain as temporary app name.
|
||||
*/
|
||||
private fun showPwaPlaceholder(intent: Intent) {
|
||||
cleanup()
|
||||
|
||||
val view = LayoutInflater.from(activity).inflate(
|
||||
R.layout.pwa_loading_screen,
|
||||
container,
|
||||
false
|
||||
|
||||
val view = LayoutInflater.from(themedContext).inflate(
|
||||
R.layout.loading_screen, container, false
|
||||
)
|
||||
|
||||
// Extract URL and use domain as temporary name
|
||||
val url = intent.dataString
|
||||
val domain = url?.let { extractDomain(it) } ?: "Web App"
|
||||
|
||||
// Set temporary app name (will be replaced with actual name once manifest loads)
|
||||
val nameView = view.findViewById<TextView>(R.id.pwa_name)
|
||||
nameView.text = domain
|
||||
|
||||
// Show WebLibre logo as placeholder (already set in XML layout)
|
||||
val iconView = view.findViewById<ImageView>(R.id.pwa_icon)
|
||||
iconView.alpha = 0.5f
|
||||
|
||||
// Start pulsing animation
|
||||
startPulseAnimation(view)
|
||||
|
||||
|
||||
val iconView = view.findViewById<ImageView>(R.id.loading_icon)
|
||||
val nameView = view.findViewById<TextView>(R.id.loading_name)
|
||||
val statusView = view.findViewById<TextView>(R.id.loading_status)
|
||||
|
||||
val shortcutId = intent.getStringExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID)
|
||||
val manifest = shortcutId?.let { PwaSplashCache.loadManifest(activity, it) }
|
||||
|
||||
if (manifest != null) {
|
||||
// PWA with cached data — branded splash
|
||||
nameView.text = manifest.shortName ?: manifest.name ?: "Web App"
|
||||
|
||||
val cachedIcon = PwaSplashCache.loadIcon(activity, shortcutId)
|
||||
if (cachedIcon != null) {
|
||||
iconView.setImageBitmap(cachedIcon)
|
||||
}
|
||||
|
||||
manifest.themeColor?.let { themeColor ->
|
||||
applyThemeColor(view, themeColor, nameView, statusView)
|
||||
}
|
||||
} else {
|
||||
// Custom Tab or uncached PWA — WebLibre logo + domain
|
||||
val url = intent.dataString
|
||||
nameView.text = url?.let { extractDomain(it) } ?: "Web App"
|
||||
}
|
||||
|
||||
container.addView(view)
|
||||
currentLoadingView = view
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a Custom Tab placeholder loading screen immediately (before components are ready).
|
||||
*/
|
||||
private fun showCustomTabPlaceholder(intent: Intent) {
|
||||
cleanup()
|
||||
|
||||
val view = LayoutInflater.from(activity).inflate(
|
||||
R.layout.custom_tab_loading_screen,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
// Extract and display domain
|
||||
val url = intent.dataString ?: return
|
||||
val domainView = view.findViewById<TextView>(R.id.custom_tab_domain)
|
||||
domainView.text = extractDomain(url)
|
||||
|
||||
container.addView(view)
|
||||
currentLoadingView = view
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the current loading screen with actual PWA data once components are ready.
|
||||
* This updates the placeholder with real manifest data.
|
||||
*/
|
||||
fun enhancePwaLoading(
|
||||
manifest: WebAppManifest,
|
||||
browserIcons: BrowserIcons,
|
||||
coroutineScope: CoroutineScope
|
||||
) {
|
||||
currentLoadingView?.let { view ->
|
||||
// Apply theme colors if available
|
||||
manifest.themeColor?.let { colorInt ->
|
||||
view.setBackgroundColor(colorInt)
|
||||
|
||||
val isDark = ColorUtils.calculateLuminance(colorInt) < 0.5
|
||||
val primaryTextColor = if (isDark) Color.WHITE else Color.BLACK
|
||||
val secondaryTextColor = ColorUtils.setAlphaComponent(primaryTextColor, 0xB3)
|
||||
|
||||
view.findViewById<TextView>(R.id.pwa_name)?.setTextColor(primaryTextColor)
|
||||
view.findViewById<TextView>(R.id.pwa_status)?.setTextColor(secondaryTextColor)
|
||||
}
|
||||
|
||||
// Update app name
|
||||
val nameView = view.findViewById<TextView>(R.id.pwa_name)
|
||||
val appName = manifest.shortName ?: manifest.name
|
||||
if (appName != null && nameView.text != appName) {
|
||||
nameView.text = appName
|
||||
}
|
||||
|
||||
// Load the PWA icon asynchronously and update
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
loadPwaIcon(manifest, browserIcons)?.let { bitmap ->
|
||||
withContext(Dispatchers.Main) {
|
||||
val iconView = view.findViewById<ImageView>(R.id.pwa_icon)
|
||||
iconView.alpha = 1.0f
|
||||
iconView.setImageBitmap(bitmap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhances the Custom Tab loading screen with favicon once components are ready.
|
||||
*/
|
||||
fun enhanceCustomTabLoading(
|
||||
url: String,
|
||||
browserIcons: BrowserIcons,
|
||||
coroutineScope: CoroutineScope
|
||||
) {
|
||||
currentLoadingView?.let { view ->
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val iconRequest = IconRequest(
|
||||
url = url,
|
||||
size = IconRequest.Size.DEFAULT
|
||||
)
|
||||
val iconResult = browserIcons.loadIcon(iconRequest).await()
|
||||
iconResult?.bitmap?.let { bitmap ->
|
||||
withContext(Dispatchers.Main) {
|
||||
val iconView = view.findViewById<ImageView>(R.id.custom_tab_icon)
|
||||
iconView.setImageBitmap(bitmap)
|
||||
iconView.alpha = 1.0f
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.debug("Failed to load favicon for $url")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the loading screen with an optional fade-out animation.
|
||||
*/
|
||||
fun hideLoading(animate: Boolean = true) {
|
||||
currentLoadingView?.let { view ->
|
||||
if (animate) {
|
||||
@@ -223,74 +110,26 @@ class LoadingScreenManager private constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the loading view and animations.
|
||||
*/
|
||||
fun cleanup() {
|
||||
pulseAnimator?.cancel()
|
||||
pulseAnimator = null
|
||||
|
||||
currentLoadingView?.let { view ->
|
||||
container.removeView(view)
|
||||
}
|
||||
currentLoadingView?.let { container.removeView(it) }
|
||||
currentLoadingView = null
|
||||
}
|
||||
|
||||
private fun startPulseAnimation(view: View) {
|
||||
val pulseView = view.findViewById<View>(R.id.pwa_icon_pulse)
|
||||
?: return
|
||||
|
||||
pulseView.alpha = 0.0f
|
||||
pulseAnimator = ObjectAnimator.ofFloat(pulseView, "alpha", 0.0f, 0.3f, 0.0f).apply {
|
||||
duration = 1500
|
||||
repeatCount = ObjectAnimator.INFINITE
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
start()
|
||||
}
|
||||
}
|
||||
private fun applyThemeColor(view: View, themeColor: Int, nameView: TextView, statusView: TextView) {
|
||||
view.setBackgroundColor(themeColor)
|
||||
|
||||
private suspend fun loadPwaIcon(
|
||||
manifest: WebAppManifest,
|
||||
browserIcons: BrowserIcons
|
||||
): Bitmap? {
|
||||
return 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()
|
||||
val isDark = ColorUtils.calculateLuminance(themeColor) < 0.5
|
||||
val primaryTextColor = if (isDark) Color.WHITE else Color.BLACK
|
||||
val secondaryTextColor = ColorUtils.setAlphaComponent(primaryTextColor, 0xB3)
|
||||
|
||||
iconResource?.let { icon ->
|
||||
val iconRequest = IconRequest(
|
||||
url = manifest.startUrl,
|
||||
size = IconRequest.Size.LAUNCHER,
|
||||
resources = listOf(
|
||||
IconRequest.Resource(
|
||||
url = icon.src,
|
||||
type = IconRequest.Resource.Type.MANIFEST_ICON,
|
||||
sizes = icon.sizes?.map { size ->
|
||||
mozilla.components.concept.engine.manifest.Size(size.width, size.height)
|
||||
} ?: emptyList(),
|
||||
mimeType = icon.type,
|
||||
maskable = icon.purpose.contains(WebAppManifest.Icon.Purpose.MASKABLE)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val result = browserIcons.loadIcon(iconRequest).await()
|
||||
result?.bitmap
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to load PWA icon", e)
|
||||
null
|
||||
}
|
||||
nameView.setTextColor(primaryTextColor)
|
||||
statusView.setTextColor(secondaryTextColor)
|
||||
}
|
||||
|
||||
private fun extractDomain(url: String): String {
|
||||
return try {
|
||||
val uri = android.net.Uri.parse(url)
|
||||
uri.host ?: url
|
||||
} catch (e: Exception) {
|
||||
android.net.Uri.parse(url).host ?: url
|
||||
} catch (_: Exception) {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!-- Pulsing ripple effect for PWA loading animation -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="?attr/colorPrimary" />
|
||||
</shape>
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!-- Minimal loading screen for Custom Tabs - keeps focus on content -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/custom_tab_loading_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Small domain favicon or generic icon -->
|
||||
<ImageView
|
||||
android:id="@+id/custom_tab_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:contentDescription="@string/website_icon_description"
|
||||
android:alpha="0.6"
|
||||
android:src="@drawable/ic_launcher_foreground" />
|
||||
|
||||
<!-- Domain/URL being loaded -->
|
||||
<TextView
|
||||
android:id="@+id/custom_tab_domain"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:paddingHorizontal="32dp" />
|
||||
|
||||
<!-- Simple linear progress indicator -->
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/custom_tab_progress"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:trackThickness="2dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<!-- Shared loading screen for PWA and Custom Tab launch.
|
||||
PWA: shows cached icon + app name + theme color.
|
||||
Custom Tab: shows WebLibre logo + domain name. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/loading_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/loading_icon"
|
||||
android:layout_width="96dp"
|
||||
android:layout_height="96dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_launcher_foreground" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loading_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="2"
|
||||
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/loading_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/pwa_loading"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary" />
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/loading_progress"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:indeterminate="true"
|
||||
app:indicatorSize="32dp"
|
||||
app:trackThickness="3dp" />
|
||||
|
||||
</LinearLayout>
|
||||
-88
@@ -1,88 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/pwa_loading_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/colorBackground">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp">
|
||||
|
||||
<!-- App Icon Container with subtle shadow/elevation -->
|
||||
<FrameLayout
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="120dp"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<!-- Icon background for maskable icons -->
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/pwa_icon_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
app:shapeAppearanceOverlay="@style/RoundedIconShape" />
|
||||
|
||||
<!-- App Icon -->
|
||||
<ImageView
|
||||
android:id="@+id/pwa_icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="@string/pwa_icon_description"
|
||||
android:src="@drawable/ic_launcher_foreground" />
|
||||
|
||||
<!-- Pulsing animation overlay -->
|
||||
<View
|
||||
android:id="@+id/pwa_icon_pulse"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/pulse_ripple"
|
||||
android:alpha="0.0" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- App Name -->
|
||||
<TextView
|
||||
android:id="@+id/pwa_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:gravity="center"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<!-- Subtitle/Status -->
|
||||
<TextView
|
||||
android:id="@+id/pwa_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center"
|
||||
android:text="@string/pwa_loading" />
|
||||
|
||||
<!-- Progress indicator at bottom -->
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
android:id="@+id/pwa_progress"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:indeterminate="true"
|
||||
app:indicatorSize="32dp"
|
||||
app:trackThickness="3dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -7,10 +7,4 @@
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowAnimationStyle">@android:style/Animation</item>
|
||||
</style>
|
||||
|
||||
<!-- Shape style for rounded icon backgrounds (PWA loading) -->
|
||||
<style name="RoundedIconShape">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">28%</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user