improved loading time pwa/ct; removed splash screens
This commit is contained in:
@@ -20,10 +20,10 @@
|
|||||||
package eu.weblibre.gecko
|
package eu.weblibre.gecko
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
|
||||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.embedding.engine.FlutterEngineCache
|
import io.flutter.embedding.engine.FlutterEngineCache
|
||||||
|
import io.flutter.embedding.engine.dart.DartExecutor
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class MainActivity: FlutterFragmentActivity() {
|
class MainActivity: FlutterFragmentActivity() {
|
||||||
@@ -41,8 +41,21 @@ class MainActivity: FlutterFragmentActivity() {
|
|||||||
channel.invokeMethod("onTrimMemory", level)
|
channel.invokeMethod("onTrimMemory", level)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCachedEngineId(): String {
|
override fun provideFlutterEngine(context: Context): FlutterEngine {
|
||||||
return "engine_id"
|
val cache = FlutterEngineCache.getInstance()
|
||||||
|
val cachedEngine = cache.get("engine_id")
|
||||||
|
if (cachedEngine != null) {
|
||||||
|
return cachedEngine
|
||||||
|
}
|
||||||
|
|
||||||
|
val flutterEngine = FlutterEngine(context.applicationContext)
|
||||||
|
flutterEngine.navigationChannel.setInitialRoute("/")
|
||||||
|
flutterEngine.dartExecutor.executeDartEntrypoint(
|
||||||
|
DartExecutor.DartEntrypoint.createDefault()
|
||||||
|
)
|
||||||
|
cache.put("engine_id", flutterEngine)
|
||||||
|
|
||||||
|
return flutterEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldDestroyEngineWithHost(): Boolean {
|
override fun shouldDestroyEngineWithHost(): Boolean {
|
||||||
|
|||||||
@@ -19,47 +19,10 @@
|
|||||||
*/
|
*/
|
||||||
package eu.weblibre.gecko
|
package eu.weblibre.gecko
|
||||||
|
|
||||||
import android.app.ActivityManager
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
|
||||||
import io.flutter.embedding.engine.FlutterEngineCache
|
|
||||||
import io.flutter.embedding.engine.dart.DartExecutor
|
|
||||||
|
|
||||||
class MyApplication : Application() {
|
class MyApplication : Application() {
|
||||||
private fun getProcessNameCompat(): String {
|
|
||||||
// Try to get process name from ActivityManager
|
|
||||||
val pid = android.os.Process.myPid()
|
|
||||||
val manager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
||||||
|
|
||||||
for (processInfo in manager.runningAppProcesses) {
|
|
||||||
if (processInfo.pid == pid) {
|
|
||||||
return processInfo.processName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to package name if process name cannot be determined
|
|
||||||
return packageName
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
val processName = getProcessNameCompat()
|
|
||||||
// Only initialize Flutter engine in the main process
|
|
||||||
if (processName == packageName) {
|
|
||||||
// Initialize the Flutter engine
|
|
||||||
val flutterEngine = FlutterEngine(this)
|
|
||||||
|
|
||||||
// Pre-warm the Flutter engine with your initial route
|
|
||||||
flutterEngine.navigationChannel.setInitialRoute("/")
|
|
||||||
flutterEngine.dartExecutor.executeDartEntrypoint(
|
|
||||||
DartExecutor.DartEntrypoint.createDefault()
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cache the Flutter engine
|
|
||||||
FlutterEngineCache.getInstance().put("engine_id", flutterEngine)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+139
-39
@@ -9,20 +9,26 @@ package eu.weblibre.flutter_mozilla_components
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.AddonCollection
|
import eu.weblibre.flutter_mozilla_components.pigeons.AddonCollection
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
|
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.ContentBlocking
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
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.GeckoStateEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
|
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.pigeons.ReaderViewController
|
||||||
import eu.weblibre.flutter_mozilla_components.api.GeckoViewportApiImpl
|
import eu.weblibre.flutter_mozilla_components.api.GeckoViewportApiImpl
|
||||||
import eu.weblibre.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl
|
import eu.weblibre.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl
|
||||||
|
import eu.weblibre.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
|
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.concept.engine.selection.SelectionActionDelegate
|
||||||
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
|
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
|
||||||
import mozilla.components.support.base.facts.Facts
|
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.logger.Logger
|
||||||
import mozilla.components.support.base.log.sink.AndroidLogSink
|
import mozilla.components.support.base.log.sink.AndroidLogSink
|
||||||
import mozilla.components.support.webextensions.WebExtensionSupport
|
import mozilla.components.support.webextensions.WebExtensionSupport
|
||||||
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
object GlobalComponents {
|
object GlobalComponents {
|
||||||
private var _components: Components? = null
|
private var _components: Components? = null
|
||||||
|
private var currentMode: ComponentsMode? = null
|
||||||
|
|
||||||
val components: Components?
|
val components: Components?
|
||||||
get() = _components
|
get() = _components
|
||||||
|
|
||||||
|
enum class ComponentsMode {
|
||||||
|
FULL,
|
||||||
|
EXTERNAL,
|
||||||
|
}
|
||||||
|
|
||||||
// Pull-to-refresh setting
|
// Pull-to-refresh setting
|
||||||
var pullToRefreshEnabled: Boolean = true
|
var pullToRefreshEnabled: Boolean = true
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -103,10 +116,19 @@ object GlobalComponents {
|
|||||||
extensionEvents: BrowserExtensionEvents,
|
extensionEvents: BrowserExtensionEvents,
|
||||||
logLevel: Log.Priority,
|
logLevel: Log.Priority,
|
||||||
contentBlocking: ContentBlocking,
|
contentBlocking: ContentBlocking,
|
||||||
addonCollection: AddonCollection?
|
addonCollection: AddonCollection?,
|
||||||
|
mode: ComponentsMode = ComponentsMode.FULL,
|
||||||
) {
|
) {
|
||||||
Logger.debug("Creating new components")
|
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(
|
val newComponents = Components(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
flutterEvents,
|
flutterEvents,
|
||||||
@@ -120,6 +142,7 @@ object GlobalComponents {
|
|||||||
extensionEvents,
|
extensionEvents,
|
||||||
)
|
)
|
||||||
_components = newComponents
|
_components = newComponents
|
||||||
|
currentMode = mode
|
||||||
|
|
||||||
//newComponents.crashReporter.install(applicationContext)
|
//newComponents.crashReporter.install(applicationContext)
|
||||||
|
|
||||||
@@ -129,46 +152,123 @@ object GlobalComponents {
|
|||||||
|
|
||||||
newComponents.core.engine.warmUp()
|
newComponents.core.engine.warmUp()
|
||||||
|
|
||||||
restoreBrowserState(newComponents)
|
if (previousCustomTabs.isNotEmpty()) {
|
||||||
restoreDownloads(newComponents)
|
for (tab in previousCustomTabs) {
|
||||||
|
val existing = newComponents.core.store.state.findCustomTab(tab.id)
|
||||||
try {
|
if (existing == null) {
|
||||||
GlobalPlacesDependencyProvider.initialize(newComponents.core.historyStorage)
|
newComponents.core.store.dispatch(
|
||||||
|
CustomTabListAction.AddCustomTabAction(tab)
|
||||||
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) {
|
if (mode == ComponentsMode.FULL) {
|
||||||
newComponents.core.fileUploadsDirCleaner.cleanUploadsDirectory()
|
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
|
package eu.weblibre.flutter_mozilla_components
|
||||||
|
|
||||||
object PwaConstants {
|
object PwaConstants {
|
||||||
|
const val PROFILES_DIR_NAME = "weblibre_profiles"
|
||||||
|
const val PROFILE_DIR_PREFIX = "profile-"
|
||||||
|
|
||||||
// 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"
|
||||||
|
|
||||||
// Shortcut ID for linking to cached manifest + icon on disk
|
|
||||||
const val EXTRA_PWA_SHORTCUT_ID = "pwa_shortcut_id"
|
|
||||||
|
|
||||||
// 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 PWA_CACHE_DIR = "pwa_cache"
|
|
||||||
|
|
||||||
// Component initialization timeouts
|
// Component initialization timeouts
|
||||||
const val COMPONENT_INIT_TIMEOUT_MS = 10000L
|
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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.FrameLayout
|
|
||||||
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
|
||||||
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 eu.weblibre.flutter_mozilla_components.R
|
import eu.weblibre.flutter_mozilla_components.R
|
||||||
import eu.weblibre.flutter_mozilla_components.ui.LoadingScreenManager
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -38,7 +36,6 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private val logger = Logger("ExternalAppBrowserActivity")
|
private val logger = Logger("ExternalAppBrowserActivity")
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||||
private var loadingScreenManager: LoadingScreenManager? = null
|
|
||||||
|
|
||||||
private val customTabSessionId: String?
|
private val customTabSessionId: String?
|
||||||
get() = intent?.getStringExtra(EXTRA_CUSTOM_TAB_SESSION_ID)
|
get() = intent?.getStringExtra(EXTRA_CUSTOM_TAB_SESSION_ID)
|
||||||
@@ -61,8 +58,12 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val components = GlobalComponents.components
|
val components = GlobalComponents.components
|
||||||
if (components == null) {
|
if (components == null) {
|
||||||
|
if (GlobalComponents.ensureExternalComponents(applicationContext)) {
|
||||||
|
showFragment(sessionId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug("Components not yet initialized, waiting...")
|
logger.debug("Components not yet initialized, waiting...")
|
||||||
showLoading()
|
|
||||||
waitForComponents(sessionId)
|
waitForComponents(sessionId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -70,25 +71,6 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
|||||||
showFragment(sessionId)
|
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) {
|
private fun waitForComponents(sessionId: String) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
var elapsedMs = 0L
|
var elapsedMs = 0L
|
||||||
@@ -132,7 +114,6 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.container, fragment)
|
.replace(R.id.container, fragment)
|
||||||
.runOnCommit { loadingScreenManager?.hideLoading() }
|
|
||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,10 +135,6 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
|||||||
// Cancel any pending coroutines
|
// Cancel any pending coroutines
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
|
|
||||||
// Clean up loading screen manager
|
|
||||||
loadingScreenManager?.cleanup()
|
|
||||||
loadingScreenManager = null
|
|
||||||
|
|
||||||
// Only clean up when the activity is actually finishing (user closed it),
|
// 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).
|
// not when the system temporarily destroys it (e.g. switching to main app).
|
||||||
if (isFinishing) {
|
if (isFinishing) {
|
||||||
@@ -199,13 +176,11 @@ class ExternalAppBrowserActivity : AppCompatActivity() {
|
|||||||
context: Context,
|
context: Context,
|
||||||
customTabSessionId: String,
|
customTabSessionId: String,
|
||||||
webAppManifestUrl: String? = null,
|
webAppManifestUrl: String? = null,
|
||||||
pwaShortcutId: String? = null,
|
|
||||||
): Intent {
|
): Intent {
|
||||||
return Intent(context, ExternalAppBrowserActivity::class.java).apply {
|
return Intent(context, ExternalAppBrowserActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
|
||||||
putExtra(EXTRA_CUSTOM_TAB_SESSION_ID, customTabSessionId)
|
putExtra(EXTRA_CUSTOM_TAB_SESSION_ID, customTabSessionId)
|
||||||
webAppManifestUrl?.let { putExtra(EXTRA_WEB_APP_MANIFEST_URL, it) }
|
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.app.AlertDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.FrameLayout
|
|
||||||
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 eu.weblibre.flutter_mozilla_components.ui.LoadingScreenManager
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
@@ -43,7 +41,6 @@ class IntentReceiverActivity : Activity() {
|
|||||||
private val logger = Logger("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
|
private var pendingIntent: Intent? = null
|
||||||
private var loadingScreenManager: LoadingScreenManager? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -62,16 +59,17 @@ class IntentReceiverActivity : Activity() {
|
|||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
coroutineScope.cancel()
|
coroutineScope.cancel()
|
||||||
loadingScreenManager?.cleanup()
|
|
||||||
loadingScreenManager = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processIntent(intent: Intent) {
|
private fun processIntent(intent: Intent) {
|
||||||
val components = GlobalComponents.components
|
if (GlobalComponents.components == null) {
|
||||||
if (components == null) {
|
if (GlobalComponents.ensureExternalComponents(applicationContext)) {
|
||||||
|
routeIntent(intent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
logger.warn("Components not initialized, waiting for initialization...")
|
logger.warn("Components not initialized, waiting for initialization...")
|
||||||
pendingIntent = intent
|
pendingIntent = intent
|
||||||
showLoadingIndicator(intent)
|
|
||||||
waitForComponentsWithTimeout()
|
waitForComponentsWithTimeout()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -246,7 +244,6 @@ class IntentReceiverActivity : Activity() {
|
|||||||
context = this@IntentReceiverActivity,
|
context = this@IntentReceiverActivity,
|
||||||
customTabSessionId = sessionId,
|
customTabSessionId = sessionId,
|
||||||
webAppManifestUrl = url,
|
webAppManifestUrl = url,
|
||||||
pwaShortcutId = intent.getStringExtra(PwaConstants.EXTRA_PWA_SHORTCUT_ID),
|
|
||||||
)
|
)
|
||||||
startActivity(externalIntent)
|
startActivity(externalIntent)
|
||||||
finish()
|
finish()
|
||||||
@@ -291,26 +288,6 @@ class IntentReceiverActivity : Activity() {
|
|||||||
return tab.id
|
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.
|
* Waits for GlobalComponents to be initialized with a timeout.
|
||||||
* Once components are ready, shows branded loading screen before routing.
|
* 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 androidx.core.content.getSystemService
|
||||||
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 eu.weblibre.flutter_mozilla_components.PwaSplashCache
|
|
||||||
import eu.weblibre.flutter_mozilla_components.activities.IntentReceiverActivity
|
import eu.weblibre.flutter_mozilla_components.activities.IntentReceiverActivity
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.ExternalApplicationResource
|
import eu.weblibre.flutter_mozilla_components.pigeons.ExternalApplicationResource
|
||||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPwaApi
|
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoPwaApi
|
||||||
@@ -140,9 +139,6 @@ class GeckoPwaApiImpl(
|
|||||||
|
|
||||||
val shortcutId = generateShortcutId(manifest.startUrl)
|
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 appName = manifest.shortName ?: manifest.name ?: "Web App"
|
||||||
|
|
||||||
val shortcutIntent = Intent(context, IntentReceiverActivity::class.java).apply {
|
val shortcutIntent = Intent(context, IntentReceiverActivity::class.java).apply {
|
||||||
@@ -150,7 +146,6 @@ 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_SHORTCUT_ID, shortcutId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val shortcut = ShortcutInfo.Builder(context, shortcutId).apply {
|
val shortcut = ShortcutInfo.Builder(context, shortcutId).apply {
|
||||||
|
|||||||
+4
-1
@@ -13,9 +13,12 @@ import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
|
|||||||
|
|
||||||
class CustomTabsService : AbstractCustomTabsService() {
|
class CustomTabsService : AbstractCustomTabsService() {
|
||||||
private val components by lazy {
|
private val components by lazy {
|
||||||
|
if (GlobalComponents.components == null) {
|
||||||
|
GlobalComponents.ensureExternalComponents(applicationContext)
|
||||||
|
}
|
||||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||||
}
|
}
|
||||||
|
|
||||||
override val customTabsServiceStore: CustomTabsServiceStore by lazy { components.core.customTabsStore }
|
override val customTabsServiceStore: CustomTabsServiceStore by lazy { components.core.customTabsStore }
|
||||||
override val engine: Engine by lazy { components.core.engine }
|
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_menu_button">Menu</string>
|
||||||
<string name="mozac_feature_customtabs_security_indicator">Security</string>
|
<string name="mozac_feature_customtabs_security_indicator">Security</string>
|
||||||
|
|
||||||
<!-- Loading screen strings -->
|
</resources>
|
||||||
<string name="pwa_loading">Opening…</string>
|
|
||||||
<string name="pwa_icon_description">App icon</string>
|
|
||||||
<string name="website_icon_description">Website icon</string>
|
|
||||||
</resources>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user