improved loading time pwa/ct; removed splash screens
This commit is contained in:
+139
-39
@@ -9,20 +9,26 @@ package eu.weblibre.flutter_mozilla_components
|
||||
import android.content.Context
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonCollection
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.BounceTrackingProtectionMode
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.ContentBlocking
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.QueryParameterStripping
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
|
||||
import eu.weblibre.flutter_mozilla_components.api.GeckoViewportApiImpl
|
||||
import eu.weblibre.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl
|
||||
import eu.weblibre.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
|
||||
import mozilla.components.browser.state.action.CustomTabListAction
|
||||
import mozilla.components.browser.state.selector.findCustomTab
|
||||
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
|
||||
import mozilla.components.support.base.facts.Facts
|
||||
@@ -31,14 +37,21 @@ import mozilla.components.support.base.log.Log
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import mozilla.components.support.base.log.sink.AndroidLogSink
|
||||
import mozilla.components.support.webextensions.WebExtensionSupport
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object GlobalComponents {
|
||||
private var _components: Components? = null
|
||||
private var currentMode: ComponentsMode? = null
|
||||
|
||||
val components: Components?
|
||||
get() = _components
|
||||
|
||||
enum class ComponentsMode {
|
||||
FULL,
|
||||
EXTERNAL,
|
||||
}
|
||||
|
||||
// Pull-to-refresh setting
|
||||
var pullToRefreshEnabled: Boolean = true
|
||||
set(value) {
|
||||
@@ -103,10 +116,19 @@ object GlobalComponents {
|
||||
extensionEvents: BrowserExtensionEvents,
|
||||
logLevel: Log.Priority,
|
||||
contentBlocking: ContentBlocking,
|
||||
addonCollection: AddonCollection?
|
||||
addonCollection: AddonCollection?,
|
||||
mode: ComponentsMode = ComponentsMode.FULL,
|
||||
) {
|
||||
Logger.debug("Creating new components")
|
||||
|
||||
val previousComponents = _components
|
||||
val previousMode = currentMode
|
||||
val previousCustomTabs = if (previousMode == ComponentsMode.EXTERNAL) {
|
||||
previousComponents?.core?.store?.state?.customTabs.orEmpty()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val newComponents = Components(
|
||||
applicationContext,
|
||||
flutterEvents,
|
||||
@@ -120,6 +142,7 @@ object GlobalComponents {
|
||||
extensionEvents,
|
||||
)
|
||||
_components = newComponents
|
||||
currentMode = mode
|
||||
|
||||
//newComponents.crashReporter.install(applicationContext)
|
||||
|
||||
@@ -129,46 +152,123 @@ object GlobalComponents {
|
||||
|
||||
newComponents.core.engine.warmUp()
|
||||
|
||||
restoreBrowserState(newComponents)
|
||||
restoreDownloads(newComponents)
|
||||
|
||||
try {
|
||||
GlobalPlacesDependencyProvider.initialize(newComponents.core.historyStorage)
|
||||
|
||||
GlobalAddonDependencyProvider.initialize(
|
||||
newComponents.core.addonManager,
|
||||
newComponents.core.addonUpdater,
|
||||
)
|
||||
|
||||
WebExtensionSupport.initialize(
|
||||
newComponents.core.engine,
|
||||
newComponents.core.store,
|
||||
onNewTabOverride = { _, engineSession, url ->
|
||||
newComponents.useCases.tabsUseCases.addTab(
|
||||
url,
|
||||
selectTab = true,
|
||||
engineSession = engineSession
|
||||
if (previousCustomTabs.isNotEmpty()) {
|
||||
for (tab in previousCustomTabs) {
|
||||
val existing = newComponents.core.store.state.findCustomTab(tab.id)
|
||||
if (existing == null) {
|
||||
newComponents.core.store.dispatch(
|
||||
CustomTabListAction.AddCustomTabAction(tab)
|
||||
)
|
||||
},
|
||||
onCloseTabOverride = { _, sessionId ->
|
||||
newComponents.useCases.tabsUseCases.removeTab(sessionId)
|
||||
},
|
||||
onSelectTabOverride = { _, sessionId ->
|
||||
newComponents.useCases.tabsUseCases.selectTab(sessionId)
|
||||
},
|
||||
onUpdatePermissionRequest = newComponents.core.addonUpdater::onUpdatePermissionRequest,
|
||||
onExtensionsLoaded = { extensions ->
|
||||
newComponents.core.addonUpdater.registerForFutureUpdates(extensions)
|
||||
newComponents.core.supportedAddonsChecker.registerForChecks()
|
||||
},
|
||||
)
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
// Web extension support is only available for engine gecko
|
||||
Logger.error("Failed to initialize web extension support", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
newComponents.core.fileUploadsDirCleaner.cleanUploadsDirectory()
|
||||
if (mode == ComponentsMode.FULL) {
|
||||
restoreBrowserState(newComponents)
|
||||
restoreDownloads(newComponents)
|
||||
|
||||
try {
|
||||
GlobalPlacesDependencyProvider.initialize(newComponents.core.historyStorage)
|
||||
|
||||
GlobalAddonDependencyProvider.initialize(
|
||||
newComponents.core.addonManager,
|
||||
newComponents.core.addonUpdater,
|
||||
)
|
||||
|
||||
WebExtensionSupport.initialize(
|
||||
newComponents.core.engine,
|
||||
newComponents.core.store,
|
||||
onNewTabOverride = { _, engineSession, url ->
|
||||
newComponents.useCases.tabsUseCases.addTab(
|
||||
url,
|
||||
selectTab = true,
|
||||
engineSession = engineSession
|
||||
)
|
||||
},
|
||||
onCloseTabOverride = { _, sessionId ->
|
||||
newComponents.useCases.tabsUseCases.removeTab(sessionId)
|
||||
},
|
||||
onSelectTabOverride = { _, sessionId ->
|
||||
newComponents.useCases.tabsUseCases.selectTab(sessionId)
|
||||
},
|
||||
onUpdatePermissionRequest = newComponents.core.addonUpdater::onUpdatePermissionRequest,
|
||||
onExtensionsLoaded = { extensions ->
|
||||
newComponents.core.addonUpdater.registerForFutureUpdates(extensions)
|
||||
newComponents.core.supportedAddonsChecker.registerForChecks()
|
||||
},
|
||||
)
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
// Web extension support is only available for engine gecko
|
||||
Logger.error("Failed to initialize web extension support", e)
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
newComponents.core.fileUploadsDirCleaner.cleanUploadsDirectory()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun ensureExternalComponents(
|
||||
baseContext: Context,
|
||||
logLevel: Log.Priority = Log.Priority.WARN,
|
||||
): Boolean {
|
||||
if (_components != null) {
|
||||
return true
|
||||
}
|
||||
|
||||
val profileFolder = resolveExternalProfileFolder(baseContext) ?: return false
|
||||
val profileContext = ProfileContext(baseContext.applicationContext, profileFolder)
|
||||
val messenger = NoopBinaryMessenger()
|
||||
|
||||
val selectionActionEvents = GeckoSelectionActionEvents(messenger)
|
||||
val selectionActionDelegate = DefaultSelectionActionDelegate(selectionActionEvents) { actions ->
|
||||
val processTextAction = "android.intent.action.PROCESS_TEXT"
|
||||
val withoutProcessText = actions.filter { it != processTextAction }.toTypedArray()
|
||||
val processTextActions = actions.filter { it == processTextAction }.toTypedArray()
|
||||
withoutProcessText + processTextActions
|
||||
}
|
||||
|
||||
val contentBlocking = ContentBlocking(
|
||||
queryParameterStripping = QueryParameterStripping.DISABLED,
|
||||
queryParameterStrippingAllowList = "",
|
||||
queryParameterStrippingStripList = "",
|
||||
bounceTrackingProtectionMode = BounceTrackingProtectionMode.DISABLED,
|
||||
)
|
||||
|
||||
setUp(
|
||||
applicationContext = profileContext,
|
||||
flutterEvents = GeckoStateEvents(messenger),
|
||||
readerViewController = ReaderViewController(messenger),
|
||||
selectionAction = selectionActionDelegate,
|
||||
addonEvents = GeckoAddonEvents(messenger),
|
||||
tabContentEvents = GeckoTabContentEvents(messenger),
|
||||
extensionEvents = BrowserExtensionEvents(messenger),
|
||||
logLevel = logLevel,
|
||||
contentBlocking = contentBlocking,
|
||||
addonCollection = null,
|
||||
mode = ComponentsMode.EXTERNAL,
|
||||
)
|
||||
|
||||
engineSettingsApi = GeckoEngineSettingsApiImpl()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun resolveExternalProfileFolder(baseContext: Context): String? {
|
||||
return try {
|
||||
val profileFile = File(baseContext.filesDir, PwaConstants.CURRENT_PROFILE_FILE)
|
||||
if (!profileFile.exists()) return null
|
||||
val profileUuid = profileFile.readText().trim().ifEmpty { return null }
|
||||
|
||||
val relativePath =
|
||||
"${PwaConstants.PROFILES_DIR_NAME}/${PwaConstants.PROFILE_DIR_PREFIX}$profileUuid"
|
||||
val profileDir = File(baseContext.filesDir, relativePath)
|
||||
if (!profileDir.exists()) return null
|
||||
|
||||
relativePath
|
||||
} catch (e: Exception) {
|
||||
Logger.error("Failed to resolve external profile folder", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 io.flutter.plugin.common.BinaryMessenger
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class NoopBinaryMessenger : BinaryMessenger {
|
||||
override fun send(channel: String, message: ByteBuffer?) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
override fun send(
|
||||
channel: String,
|
||||
message: ByteBuffer?,
|
||||
callback: BinaryMessenger.BinaryReply?
|
||||
) {
|
||||
callback?.reply(null)
|
||||
}
|
||||
|
||||
override fun setMessageHandler(
|
||||
channel: String,
|
||||
handler: BinaryMessenger.BinaryMessageHandler?
|
||||
) {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -7,17 +7,16 @@
|
||||
package eu.weblibre.flutter_mozilla_components
|
||||
|
||||
object PwaConstants {
|
||||
const val PROFILES_DIR_NAME = "weblibre_profiles"
|
||||
const val PROFILE_DIR_PREFIX = "profile-"
|
||||
|
||||
// Intent extras keys for PWA metadata
|
||||
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
@@ -1,96 +0,0 @@
|
||||
/*
|
||||
* 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")
|
||||
}
|
||||
}
|
||||
+5
-30
@@ -9,14 +9,12 @@ package eu.weblibre.flutter_mozilla_components.activities
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import eu.weblibre.flutter_mozilla_components.ExternalAppBrowserFragment
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.PwaConstants
|
||||
import eu.weblibre.flutter_mozilla_components.R
|
||||
import eu.weblibre.flutter_mozilla_components.ui.LoadingScreenManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -38,7 +36,6 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
|
||||
private val logger = Logger("ExternalAppBrowserActivity")
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private var loadingScreenManager: LoadingScreenManager? = null
|
||||
|
||||
private val customTabSessionId: String?
|
||||
get() = intent?.getStringExtra(EXTRA_CUSTOM_TAB_SESSION_ID)
|
||||
@@ -61,8 +58,12 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
|
||||
val components = GlobalComponents.components
|
||||
if (components == null) {
|
||||
if (GlobalComponents.ensureExternalComponents(applicationContext)) {
|
||||
showFragment(sessionId)
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug("Components not yet initialized, waiting...")
|
||||
showLoading()
|
||||
waitForComponents(sessionId)
|
||||
return
|
||||
}
|
||||
@@ -70,25 +71,6 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
showFragment(sessionId)
|
||||
}
|
||||
|
||||
private fun showLoading() {
|
||||
val container = findViewById<FrameLayout>(R.id.container)
|
||||
loadingScreenManager = LoadingScreenManager.forActivity(this, container)
|
||||
|
||||
val url = webAppManifestUrl ?: ""
|
||||
val shortcutId = intent?.getStringExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID)
|
||||
|
||||
if (url.isNotEmpty()) {
|
||||
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 {
|
||||
loadingScreenManager?.showLoadingForIntent(Intent())
|
||||
}
|
||||
}
|
||||
|
||||
private fun waitForComponents(sessionId: String) {
|
||||
coroutineScope.launch {
|
||||
var elapsedMs = 0L
|
||||
@@ -132,7 +114,6 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.container, fragment)
|
||||
.runOnCommit { loadingScreenManager?.hideLoading() }
|
||||
.commit()
|
||||
}
|
||||
|
||||
@@ -154,10 +135,6 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
// Cancel any pending coroutines
|
||||
coroutineScope.cancel()
|
||||
|
||||
// Clean up loading screen manager
|
||||
loadingScreenManager?.cleanup()
|
||||
loadingScreenManager = null
|
||||
|
||||
// Only clean up when the activity is actually finishing (user closed it),
|
||||
// not when the system temporarily destroys it (e.g. switching to main app).
|
||||
if (isFinishing) {
|
||||
@@ -199,13 +176,11 @@ 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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-29
@@ -10,10 +10,8 @@ import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.FrameLayout
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.PwaConstants
|
||||
import eu.weblibre.flutter_mozilla_components.ui.LoadingScreenManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -43,7 +41,6 @@ class IntentReceiverActivity : Activity() {
|
||||
private val logger = Logger("IntentReceiverActivity")
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
private var pendingIntent: Intent? = null
|
||||
private var loadingScreenManager: LoadingScreenManager? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -62,16 +59,17 @@ class IntentReceiverActivity : Activity() {
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
coroutineScope.cancel()
|
||||
loadingScreenManager?.cleanup()
|
||||
loadingScreenManager = null
|
||||
}
|
||||
|
||||
private fun processIntent(intent: Intent) {
|
||||
val components = GlobalComponents.components
|
||||
if (components == null) {
|
||||
if (GlobalComponents.components == null) {
|
||||
if (GlobalComponents.ensureExternalComponents(applicationContext)) {
|
||||
routeIntent(intent)
|
||||
return
|
||||
}
|
||||
|
||||
logger.warn("Components not initialized, waiting for initialization...")
|
||||
pendingIntent = intent
|
||||
showLoadingIndicator(intent)
|
||||
waitForComponentsWithTimeout()
|
||||
return
|
||||
}
|
||||
@@ -246,7 +244,6 @@ class IntentReceiverActivity : Activity() {
|
||||
context = this@IntentReceiverActivity,
|
||||
customTabSessionId = sessionId,
|
||||
webAppManifestUrl = url,
|
||||
pwaShortcutId = intent.getStringExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID),
|
||||
)
|
||||
startActivity(externalIntent)
|
||||
finish()
|
||||
@@ -291,26 +288,6 @@ class IntentReceiverActivity : Activity() {
|
||||
return tab.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a branded loading screen immediately based on intent type.
|
||||
* This avoids showing a minimal spinner and shows proper placeholders right away.
|
||||
*/
|
||||
private fun showLoadingIndicator(intent: Intent) {
|
||||
// Create a container layout
|
||||
val container = FrameLayout(this).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||
}
|
||||
setContentView(container)
|
||||
|
||||
// Initialize loading screen manager and show branded screen immediately
|
||||
loadingScreenManager = LoadingScreenManager.forActivity(this, container)
|
||||
loadingScreenManager?.showLoadingForIntent(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for GlobalComponents to be initialized with a timeout.
|
||||
* Once components are ready, shows branded loading screen before routing.
|
||||
|
||||
-5
@@ -17,7 +17,6 @@ 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
|
||||
@@ -140,9 +139,6 @@ class GeckoPwaApiImpl(
|
||||
|
||||
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 {
|
||||
@@ -150,7 +146,6 @@ class GeckoPwaApiImpl(
|
||||
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 shortcut = ShortcutInfo.Builder(context, shortcutId).apply {
|
||||
|
||||
+4
-1
@@ -13,9 +13,12 @@ import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
|
||||
|
||||
class CustomTabsService : AbstractCustomTabsService() {
|
||||
private val components by lazy {
|
||||
if (GlobalComponents.components == null) {
|
||||
GlobalComponents.ensureExternalComponents(applicationContext)
|
||||
}
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override val customTabsServiceStore: CustomTabsServiceStore by lazy { components.core.customTabsStore }
|
||||
override val engine: Engine by lazy { components.core.engine }
|
||||
}
|
||||
}
|
||||
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
/*
|
||||
* 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.ui
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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
|
||||
|
||||
/**
|
||||
* 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 var currentLoadingView: View? = 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 {
|
||||
fun forActivity(activity: Activity, container: FrameLayout) =
|
||||
LoadingScreenManager(activity, container)
|
||||
|
||||
fun isPwaIntent(intent: Intent?) =
|
||||
intent?.hasExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) == true
|
||||
|
||||
fun hasCachedSplashData(intent: Intent?) =
|
||||
intent?.hasExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the loading screen immediately based on intent analysis.
|
||||
* Can be called before components are initialized.
|
||||
*/
|
||||
fun showLoadingForIntent(intent: Intent) {
|
||||
cleanup()
|
||||
|
||||
val view = LayoutInflater.from(themedContext).inflate(
|
||||
R.layout.loading_screen, container, false
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fun hideLoading(animate: Boolean = true) {
|
||||
currentLoadingView?.let { view ->
|
||||
if (animate) {
|
||||
view.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(200)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
.start()
|
||||
} else {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
currentLoadingView?.let { container.removeView(it) }
|
||||
currentLoadingView = null
|
||||
}
|
||||
|
||||
private fun applyThemeColor(view: View, themeColor: Int, nameView: TextView, statusView: TextView) {
|
||||
view.setBackgroundColor(themeColor)
|
||||
|
||||
val isDark = ColorUtils.calculateLuminance(themeColor) < 0.5
|
||||
val primaryTextColor = if (isDark) Color.WHITE else Color.BLACK
|
||||
val secondaryTextColor = ColorUtils.setAlphaComponent(primaryTextColor, 0xB3)
|
||||
|
||||
nameView.setTextColor(primaryTextColor)
|
||||
statusView.setTextColor(secondaryTextColor)
|
||||
}
|
||||
|
||||
private fun extractDomain(url: String): String {
|
||||
return try {
|
||||
android.net.Uri.parse(url).host ?: url
|
||||
} catch (_: Exception) {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +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/. -->
|
||||
|
||||
<!-- 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>
|
||||
@@ -27,8 +27,4 @@
|
||||
<string name="mozac_feature_customtabs_menu_button">Menu</string>
|
||||
<string name="mozac_feature_customtabs_security_indicator">Security</string>
|
||||
|
||||
<!-- Loading screen strings -->
|
||||
<string name="pwa_loading">Opening…</string>
|
||||
<string name="pwa_icon_description">App icon</string>
|
||||
<string name="website_icon_description">Website icon</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user