diff --git a/app/android/app/src/main/kotlin/eu/weblibre/gecko/MainActivity.kt b/app/android/app/src/main/kotlin/eu/weblibre/gecko/MainActivity.kt index 0f77de19..4557be63 100644 --- a/app/android/app/src/main/kotlin/eu/weblibre/gecko/MainActivity.kt +++ b/app/android/app/src/main/kotlin/eu/weblibre/gecko/MainActivity.kt @@ -20,10 +20,10 @@ package eu.weblibre.gecko import android.content.Context -import android.os.Bundle import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.plugin.common.MethodChannel class MainActivity: FlutterFragmentActivity() { @@ -41,8 +41,21 @@ class MainActivity: FlutterFragmentActivity() { channel.invokeMethod("onTrimMemory", level) } - override fun getCachedEngineId(): String { - return "engine_id" + override fun provideFlutterEngine(context: Context): FlutterEngine { + 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 { diff --git a/app/android/app/src/main/kotlin/eu/weblibre/gecko/MyApplication.kt b/app/android/app/src/main/kotlin/eu/weblibre/gecko/MyApplication.kt index de82c69a..486eec07 100644 --- a/app/android/app/src/main/kotlin/eu/weblibre/gecko/MyApplication.kt +++ b/app/android/app/src/main/kotlin/eu/weblibre/gecko/MyApplication.kt @@ -19,47 +19,10 @@ */ package eu.weblibre.gecko -import android.app.ActivityManager 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() { - 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() { 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) - } } -} \ No newline at end of file +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt index c95ee676..7355dd34 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/GlobalComponents.kt @@ -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() + } } } -} \ No newline at end of file + + @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 + } + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/NoopBinaryMessenger.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/NoopBinaryMessenger.kt new file mode 100644 index 00000000..c28b5279 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/NoopBinaryMessenger.kt @@ -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 + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaConstants.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaConstants.kt index 89100bfa..40076597 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaConstants.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaConstants.kt @@ -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 diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaSplashCache.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaSplashCache.kt deleted file mode 100644 index 65706578..00000000 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/PwaSplashCache.kt +++ /dev/null @@ -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: - * /pwa_cache//manifest.json - * /pwa_cache//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") - } -} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt index c7ace60a..0ae6604a 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/ExternalAppBrowserActivity.kt @@ -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(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) } } } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt index 6e49ea76..b312c18e 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/activities/IntentReceiverActivity.kt @@ -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. diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPwaApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPwaApiImpl.kt index faaae27e..24cec765 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPwaApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoPwaApiImpl.kt @@ -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 { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/services/CustomTabsService.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/services/CustomTabsService.kt index 028cd47b..d6117d0c 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/services/CustomTabsService.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/services/CustomTabsService.kt @@ -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 } -} \ No newline at end of file +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ui/LoadingScreenManager.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ui/LoadingScreenManager.kt deleted file mode 100644 index 9567223a..00000000 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/ui/LoadingScreenManager.kt +++ /dev/null @@ -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(R.id.loading_icon) - val nameView = view.findViewById(R.id.loading_name) - val statusView = view.findViewById(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 - } - } -} diff --git a/packages/flutter_mozilla_components/android/src/main/res/layout/loading_screen.xml b/packages/flutter_mozilla_components/android/src/main/res/layout/loading_screen.xml deleted file mode 100644 index 57afdb52..00000000 --- a/packages/flutter_mozilla_components/android/src/main/res/layout/loading_screen.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/packages/flutter_mozilla_components/android/src/main/res/values/strings.xml b/packages/flutter_mozilla_components/android/src/main/res/values/strings.xml index a6f520b6..b0872a9f 100644 --- a/packages/flutter_mozilla_components/android/src/main/res/values/strings.xml +++ b/packages/flutter_mozilla_components/android/src/main/res/values/strings.xml @@ -27,8 +27,4 @@ Menu Security - - Opening… - App icon - Website icon - \ No newline at end of file +