try mitigate initialization race condition

This commit is contained in:
Fabian Freund
2025-10-23 11:20:30 +02:00
parent b321fcfe86
commit 8cbebb8c46
2 changed files with 258 additions and 243 deletions
@@ -21,12 +21,18 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import eu.weblibre.flutter_mozilla_components.addons.WebExtensionActionPopupActivity
import eu.weblibre.flutter_mozilla_components.addons.WebExtensionPromptFeature import eu.weblibre.flutter_mozilla_components.addons.WebExtensionPromptFeature
import eu.weblibre.flutter_mozilla_components.databinding.FragmentBrowserBinding import eu.weblibre.flutter_mozilla_components.databinding.FragmentBrowserBinding
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
import eu.weblibre.flutter_mozilla_components.feature.ReadabilityExtractFeature
import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature
import eu.weblibre.flutter_mozilla_components.integration.ReaderViewIntegration
import eu.weblibre.flutter_mozilla_components.services.DownloadService import eu.weblibre.flutter_mozilla_components.services.DownloadService
import io.flutter.Log import io.flutter.Log
import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.app.links.AppLinksFeature import mozilla.components.feature.app.links.AppLinksFeature
import mozilla.components.feature.downloads.DownloadsFeature import mozilla.components.feature.downloads.DownloadsFeature
@@ -43,6 +49,7 @@ import mozilla.components.feature.session.SwipeRefreshFeature
import mozilla.components.feature.sitepermissions.SitePermissionsFeature import mozilla.components.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.feature.sitepermissions.SitePermissionsRules import mozilla.components.feature.sitepermissions.SitePermissionsRules
import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
import mozilla.components.feature.tabs.WindowFeature
import mozilla.components.feature.webauthn.WebAuthnFeature import mozilla.components.feature.webauthn.WebAuthnFeature
import mozilla.components.support.base.feature.ActivityResultHandler import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.PermissionsFeature import mozilla.components.support.base.feature.PermissionsFeature
@@ -52,6 +59,7 @@ import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.view.enterImmersiveMode import mozilla.components.support.ktx.android.view.enterImmersiveMode
import mozilla.components.support.ktx.android.view.exitImmersiveMode import mozilla.components.support.ktx.android.view.exitImmersiveMode
import mozilla.components.support.locale.ActivityContextWrapper import mozilla.components.support.locale.ActivityContextWrapper
import mozilla.components.support.webextensions.WebExtensionPopupObserver
/** /**
* Base fragment extended by [BrowserFragment] and [ExternalAppBrowserFragment]. * Base fragment extended by [BrowserFragment] and [ExternalAppBrowserFragment].
@@ -77,6 +85,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
private var pictureInPictureFeature: PictureInPictureFeature? = null private var pictureInPictureFeature: PictureInPictureFeature? = null
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewIntegration>()
private val readabilityExtractFeature = ViewBoundFeatureWrapper<ReadabilityExtractFeature>()
private val webExtensionPopupObserver = ViewBoundFeatureWrapper<WebExtensionPopupObserver>()
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
// Registers a photo picker activity launcher in single-select mode. // Registers a photo picker activity launcher in single-select mode.
private val singleMediaPicker = private val singleMediaPicker =
AndroidPhotoPicker.singleMediaPicker( AndroidPhotoPicker.singleMediaPicker(
@@ -160,184 +175,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
@CallSuper @CallSuper
@Suppress("LongMethod") @Suppress("LongMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
sessionFeature.set( view.post {
feature = SessionFeature( if (GlobalComponents.components != null) {
components.core.store, createAndSetupEngine(view)
components.useCases.sessionUseCases.goBack, } else {
components.useCases.sessionUseCases.goForward, // Retry or handle error
components.engineView!!, view.postDelayed({ createAndSetupEngine(view) }, 100)
sessionId, }
), }
owner = this,
view = view,
)
swipeRefreshFeature.set(
feature = SwipeRefreshFeature(
components.core.store,
components.useCases.sessionUseCases.reload,
binding.swipeToRefresh,
),
owner = this,
view = view,
)
shareResourceFeature.set(
ShareResourceFeature(
context = requireContext().applicationContext,
httpClient = components.core.client,
store = components.core.store,
tabId = sessionId,
),
owner = this,
view = view,
)
downloadsFeature.set(
feature = DownloadsFeature(
requireContext().applicationContext,
store = components.core.store,
useCases = components.useCases.downloadsUseCases,
fragmentManager = childFragmentManager,
onDownloadStopped = { download, id, status ->
Logger.debug("Download done. ID#$id $download with status $status")
},
downloadManager = FetchDownloadManager(
requireContext().applicationContext,
components.core.store,
DownloadService::class,
notificationsDelegate = components.notificationsDelegate,
),
tabId = sessionId,
onNeedToRequestPermissions = { permissions ->
requestDownloadPermissionsLauncher.launch(permissions)
},
),
owner = this,
view = view,
)
appLinksFeature.set(
feature = AppLinksFeature(
context = requireContext(),
store = components.core.store,
sessionId = sessionId,
fragmentManager = parentFragmentManager,
launchInApp = { components.core.prefs.getBoolean(context?.getPreferenceKey(R.string.pref_key_launch_external_app), false) },
loadUrlUseCase = components.useCases.sessionUseCases.loadUrl,
),
owner = this,
view = view,
)
promptFeature.set(
feature = PromptFeature(
fragment = this,
store = components.core.store,
customTabId = sessionId,
tabsUseCases = components.useCases.tabsUseCases,
fragmentManager = parentFragmentManager,
fileUploadsDirCleaner = components.core.fileUploadsDirCleaner,
onNeedToRequestPermissions = { permissions ->
requestPromptsPermissionsLauncher.launch(permissions)
},
androidPhotoPicker = AndroidPhotoPicker(
requireContext(),
singleMediaPicker,
multipleMediaPicker,
),
),
owner = this,
view = view,
)
sitePermissionsFeature.set(
feature = SitePermissionsFeature(
context = requireContext(),
sessionId = sessionId,
storage = components.core.geckoSitePermissionsStorage,
fragmentManager = parentFragmentManager,
sitePermissionsRules = SitePermissionsRules(
autoplayAudible = AutoplayAction.BLOCKED,
autoplayInaudible = AutoplayAction.BLOCKED,
camera = SitePermissionsRules.Action.ASK_TO_ALLOW,
location = SitePermissionsRules.Action.ASK_TO_ALLOW,
notification = SitePermissionsRules.Action.ASK_TO_ALLOW,
microphone = SitePermissionsRules.Action.ASK_TO_ALLOW,
persistentStorage = SitePermissionsRules.Action.ASK_TO_ALLOW,
mediaKeySystemAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
crossOriginStorageAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
localDeviceAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
localNetworkAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
),
onNeedToRequestPermissions = { permissions ->
requestSitePermissionsLauncher.launch(permissions)
},
onShouldShowRequestPermissionRationale = { shouldShowRequestPermissionRationale(it) },
store = components.core.store,
),
owner = this,
view = view,
)
webExtensionPromptFeature.set(
feature = WebExtensionPromptFeature(
store = components.core.store,
context = requireContext(),
fragmentManager = parentFragmentManager,
),
owner = this,
view = view
)
fullScreenFeature.set(
feature = FullScreenFeature(
store = components.core.store,
sessionUseCases = components.useCases.sessionUseCases,
tabId = sessionId,
fullScreenChanged = ::fullScreenChanged,
viewportFitChanged = ::viewportFitChanged
),
owner = this,
view = binding.root,
)
mediaSessionFullscreenFeature.set(
feature = MediaSessionFullscreenFeature(
requireActivity(),
components.core.store,
sessionId,
),
owner = this,
view = binding.root,
)
pictureInPictureFeature = PictureInPictureFeature(
store = components.core.store,
activity = requireActivity(),
tabId = sessionId,
)
secureWindowFeature.set(
feature = SecureWindowFeature(
window = requireActivity().window,
store = components.core.store,
customTabId = sessionId,
),
owner = this,
view = binding.root,
)
webAuthnFeature.set(
feature = WebAuthnFeature(
engine = components.core.engine,
activity = requireActivity(),
exitFullScreen = components.useCases.sessionUseCases.exitFullscreen::invoke,
currentTab = { components.core.store.state.selectedTabId },
),
owner = this,
view = view
)
} }
private fun restartApp(context: Context) { private fun restartApp(context: Context) {
@@ -354,7 +199,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
Runtime.getRuntime().exit(0) Runtime.getRuntime().exit(0)
} }
private fun createAndSetupEngine() { private fun createAndSetupEngine(view: View) {
try { try {
// Set layout parameters // Set layout parameters
val layoutParams = FrameLayout.LayoutParams( val layoutParams = FrameLayout.LayoutParams(
@@ -372,19 +217,249 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
binding.swipeToRefresh.addView(engineNativeView) binding.swipeToRefresh.addView(engineNativeView)
components.engineView = engineView components.engineView = engineView
sessionFeature.set(
feature = SessionFeature(
components.core.store,
components.useCases.sessionUseCases.goBack,
components.useCases.sessionUseCases.goForward,
components.engineView!!,
sessionId,
),
owner = this,
view = view,
)
swipeRefreshFeature.set(
feature = SwipeRefreshFeature(
components.core.store,
components.useCases.sessionUseCases.reload,
binding.swipeToRefresh,
),
owner = this,
view = view,
)
shareResourceFeature.set(
ShareResourceFeature(
context = requireContext().applicationContext,
httpClient = components.core.client,
store = components.core.store,
tabId = sessionId,
),
owner = this,
view = view,
)
downloadsFeature.set(
feature = DownloadsFeature(
requireContext().applicationContext,
store = components.core.store,
useCases = components.useCases.downloadsUseCases,
fragmentManager = childFragmentManager,
onDownloadStopped = { download, id, status ->
Logger.debug("Download done. ID#$id $download with status $status")
},
downloadManager = FetchDownloadManager(
requireContext().applicationContext,
components.core.store,
DownloadService::class,
notificationsDelegate = components.notificationsDelegate,
),
tabId = sessionId,
onNeedToRequestPermissions = { permissions ->
requestDownloadPermissionsLauncher.launch(permissions)
},
),
owner = this,
view = view,
)
appLinksFeature.set(
feature = AppLinksFeature(
context = requireContext(),
store = components.core.store,
sessionId = sessionId,
fragmentManager = parentFragmentManager,
launchInApp = { components.core.prefs.getBoolean(context?.getPreferenceKey(R.string.pref_key_launch_external_app), false) },
loadUrlUseCase = components.useCases.sessionUseCases.loadUrl,
),
owner = this,
view = view,
)
promptFeature.set(
feature = PromptFeature(
fragment = this,
store = components.core.store,
customTabId = sessionId,
tabsUseCases = components.useCases.tabsUseCases,
fragmentManager = parentFragmentManager,
fileUploadsDirCleaner = components.core.fileUploadsDirCleaner,
onNeedToRequestPermissions = { permissions ->
requestPromptsPermissionsLauncher.launch(permissions)
},
androidPhotoPicker = AndroidPhotoPicker(
requireContext(),
singleMediaPicker,
multipleMediaPicker,
),
),
owner = this,
view = view,
)
sitePermissionsFeature.set(
feature = SitePermissionsFeature(
context = requireContext(),
sessionId = sessionId,
storage = components.core.geckoSitePermissionsStorage,
fragmentManager = parentFragmentManager,
sitePermissionsRules = SitePermissionsRules(
autoplayAudible = AutoplayAction.BLOCKED,
autoplayInaudible = AutoplayAction.BLOCKED,
camera = SitePermissionsRules.Action.ASK_TO_ALLOW,
location = SitePermissionsRules.Action.ASK_TO_ALLOW,
notification = SitePermissionsRules.Action.ASK_TO_ALLOW,
microphone = SitePermissionsRules.Action.ASK_TO_ALLOW,
persistentStorage = SitePermissionsRules.Action.ASK_TO_ALLOW,
mediaKeySystemAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
crossOriginStorageAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
localDeviceAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
localNetworkAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
),
onNeedToRequestPermissions = { permissions ->
requestSitePermissionsLauncher.launch(permissions)
},
onShouldShowRequestPermissionRationale = { shouldShowRequestPermissionRationale(it) },
store = components.core.store,
),
owner = this,
view = view,
)
webExtensionPromptFeature.set(
feature = WebExtensionPromptFeature(
store = components.core.store,
context = requireContext(),
fragmentManager = parentFragmentManager,
),
owner = this,
view = view
)
fullScreenFeature.set(
feature = FullScreenFeature(
store = components.core.store,
sessionUseCases = components.useCases.sessionUseCases,
tabId = sessionId,
fullScreenChanged = ::fullScreenChanged,
viewportFitChanged = ::viewportFitChanged
),
owner = this,
view = binding.root,
)
mediaSessionFullscreenFeature.set(
feature = MediaSessionFullscreenFeature(
requireActivity(),
components.core.store,
sessionId,
),
owner = this,
view = binding.root,
)
pictureInPictureFeature = PictureInPictureFeature(
store = components.core.store,
activity = requireActivity(),
tabId = sessionId,
)
secureWindowFeature.set(
feature = SecureWindowFeature(
window = requireActivity().window,
store = components.core.store,
customTabId = sessionId,
),
owner = this,
view = binding.root,
)
webAuthnFeature.set(
feature = WebAuthnFeature(
engine = components.core.engine,
activity = requireActivity(),
exitFullScreen = components.useCases.sessionUseCases.exitFullscreen::invoke,
currentTab = { components.core.store.state.selectedTabId },
),
owner = this,
view = view
)
readerViewFeature.set(
feature = ReaderViewIntegration(
requireContext(),
components.core.engine,
components.core.store,
binding.readerViewBar,
components.events.readerViewEvents,
components.readerViewController,
),
owner = this,
view = view,
)
readabilityExtractFeature.set(
feature = components.features.readabilityExtractFeature,
owner = this,
view = view,
)
windowFeature.set(
feature = WindowFeature(components.core.store, components.useCases.tabsUseCases),
owner = this,
view = view,
)
thumbnailsFeature.set(
feature = BrowserThumbnails(requireContext(), components.engineView!!, components.core.store),
owner = this,
view = view,
)
webExtensionPopupObserver.set(
feature = WebExtensionPopupObserver(components.core.store, ::openPopup),
owner = this,
view = view,
)
webExtToolbarFeature.set(
feature = components.features.webExtensionToolbarFeature,
owner = this,
view = view,
)
components.core.historyStorage.registerStorageMaintenanceWorker()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("EngineCreation", "Failed to create engine: ${e.message}", e) Log.e("EngineCreation", "Failed to create engine: ${e.message}", e)
context?.let { restartApp(it) } context?.let { restartApp(it) }
} }
} }
private fun openPopup(webExtensionState: WebExtensionState) {
val intent = Intent(requireContext().applicationContext, WebExtensionActionPopupActivity::class.java)
intent.putExtra("web_extension_id", webExtensionState.id)
intent.putExtra("web_extension_name", webExtensionState.name)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
@CallSuper @CallSuper
@Suppress("LongMethod") @Suppress("LongMethod")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentBrowserBinding.inflate(inflater, container, false) _binding = FragmentBrowserBinding.inflate(inflater, container, false)
createAndSetupEngine()
return binding.root return binding.root
} }
@@ -27,13 +27,6 @@ import mozilla.components.support.webextensions.WebExtensionPopupObserver
* Fragment used for browsing the web within the main app. * Fragment used for browsing the web within the main app.
*/ */
class BrowserFragment() : BaseBrowserFragment(), UserInteractionHandler { class BrowserFragment() : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewIntegration>()
private val readabilityExtractFeature = ViewBoundFeatureWrapper<ReadabilityExtractFeature>()
private val webExtensionPopupObserver = ViewBoundFeatureWrapper<WebExtensionPopupObserver>()
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
override fun createEngine(components: Components): EngineView { override fun createEngine(components: Components): EngineView {
return components.core.engine.createView(requireContext()).apply { return components.core.engine.createView(requireContext()).apply {
selectionActionDelegate = components.selectionAction selectionActionDelegate = components.selectionAction
@@ -49,63 +42,10 @@ class BrowserFragment() : BaseBrowserFragment(), UserInteractionHandler {
@Suppress("LongMethod") @Suppress("LongMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
readerViewFeature.set(
feature = ReaderViewIntegration(
requireContext(),
components.core.engine,
components.core.store,
binding.readerViewBar,
components.events.readerViewEvents,
components.readerViewController,
),
owner = this,
view = view,
)
readabilityExtractFeature.set(
feature = components.features.readabilityExtractFeature,
owner = this,
view = view,
)
windowFeature.set(
feature = WindowFeature(components.core.store, components.useCases.tabsUseCases),
owner = this,
view = view,
)
thumbnailsFeature.set(
feature = BrowserThumbnails(requireContext(), components.engineView!!, components.core.store),
owner = this,
view = view,
)
webExtensionPopupObserver.set(
feature = WebExtensionPopupObserver(components.core.store, ::openPopup),
owner = this,
view = view,
)
webExtToolbarFeature.set(
feature = components.features.webExtensionToolbarFeature,
owner = this,
view = view,
)
components.core.historyStorage.registerStorageMaintenanceWorker()
}
private fun openPopup(webExtensionState: WebExtensionState) {
val intent = Intent(requireContext().applicationContext, WebExtensionActionPopupActivity::class.java)
intent.putExtra("web_extension_id", webExtensionState.id)
intent.putExtra("web_extension_name", webExtensionState.name)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
} }
override fun onBackPressed(): Boolean = override fun onBackPressed(): Boolean =
readerViewFeature.onBackPressed() || super.onBackPressed() super.readerViewFeature.onBackPressed() || super.onBackPressed()
companion object { companion object {
fun create(sessionId: String? = null) = BrowserFragment().apply { fun create(sessionId: String? = null) = BrowserFragment().apply {