add proper extension session lifecycle
This commit is contained in:
+199
@@ -0,0 +1,199 @@
|
||||
/* 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/. */
|
||||
|
||||
package eu.weblibre.flutter_mozilla_components.addons
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.services.DownloadService
|
||||
import mozilla.components.browser.state.action.ContentAction
|
||||
import mozilla.components.browser.state.action.CustomTabListAction
|
||||
import mozilla.components.browser.state.state.CustomTabSessionState
|
||||
import mozilla.components.browser.state.state.EngineState
|
||||
import mozilla.components.browser.state.state.SessionState
|
||||
import mozilla.components.browser.state.state.content.DownloadState
|
||||
import mozilla.components.browser.state.state.createCustomTab
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest
|
||||
import mozilla.components.concept.engine.window.WindowRequest
|
||||
import mozilla.components.concept.fetch.Response
|
||||
import mozilla.components.feature.downloads.DownloadsFeature
|
||||
import mozilla.components.feature.downloads.manager.FetchDownloadManager
|
||||
import mozilla.components.feature.prompts.PromptFeature
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
|
||||
/**
|
||||
* Provides shared functionality to fragments for add-on popups and settings.
|
||||
*
|
||||
* Manages the engine session lifecycle by wrapping it in a custom tab registered in the
|
||||
* BrowserStore, enabling PromptFeature and DownloadsFeature to interact with the session.
|
||||
*/
|
||||
abstract class AddonPopupBaseFragment : Fragment(), EngineSession.Observer, UserInteractionHandler {
|
||||
private val promptsFeature = ViewBoundFeatureWrapper<PromptFeature>()
|
||||
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
|
||||
|
||||
protected val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
protected var session: SessionState? = null
|
||||
protected var engineSession: EngineSession? = null
|
||||
private var canGoBack: Boolean = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
session?.let {
|
||||
promptsFeature.set(
|
||||
feature = PromptFeature(
|
||||
fragment = this,
|
||||
store = components.core.store,
|
||||
customTabId = it.id,
|
||||
fragmentManager = parentFragmentManager,
|
||||
fileUploadsDirCleaner = components.core.fileUploadsDirCleaner,
|
||||
tabsUseCases = components.useCases.tabsUseCases,
|
||||
onNeedToRequestPermissions = { permissions ->
|
||||
@Suppress("DEPRECATION")
|
||||
requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
|
||||
},
|
||||
),
|
||||
owner = this,
|
||||
view = view,
|
||||
)
|
||||
|
||||
downloadsFeature.set(
|
||||
feature = DownloadsFeature(
|
||||
requireContext().applicationContext,
|
||||
store = components.core.store,
|
||||
useCases = components.useCases.downloadsUseCases,
|
||||
fragmentManager = childFragmentManager,
|
||||
tabId = it.id,
|
||||
downloadManager = FetchDownloadManager(
|
||||
requireContext().applicationContext,
|
||||
components.core.store,
|
||||
DownloadService::class,
|
||||
notificationsDelegate = components.notificationsDelegate,
|
||||
),
|
||||
onNeedToRequestPermissions = { permissions ->
|
||||
@Suppress("DEPRECATION")
|
||||
requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS)
|
||||
},
|
||||
),
|
||||
owner = this,
|
||||
view = view,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
engineSession?.register(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
engineSession?.unregister(this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
engineSession?.close()
|
||||
session?.let {
|
||||
components.core.store.dispatch(CustomTabListAction.RemoveCustomTabAction(it.id))
|
||||
}
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPromptRequest(promptRequest: PromptRequest) {
|
||||
session?.let { session ->
|
||||
components.core.store.dispatch(
|
||||
ContentAction.UpdatePromptRequestAction(session.id, promptRequest),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExternalResource(
|
||||
url: String,
|
||||
fileName: String?,
|
||||
contentLength: Long?,
|
||||
contentType: String?,
|
||||
cookie: String?,
|
||||
userAgent: String?,
|
||||
isPrivate: Boolean,
|
||||
skipConfirmation: Boolean,
|
||||
openInApp: Boolean,
|
||||
response: Response?,
|
||||
) {
|
||||
session?.let { session ->
|
||||
val fileSize = if (contentLength != null && contentLength < 0) null else contentLength
|
||||
val download = DownloadState(
|
||||
url,
|
||||
fileName,
|
||||
contentType,
|
||||
fileSize,
|
||||
0,
|
||||
DownloadState.Status.INITIATED,
|
||||
userAgent,
|
||||
Environment.DIRECTORY_DOWNLOADS,
|
||||
private = isPrivate,
|
||||
skipConfirmation = skipConfirmation,
|
||||
openInApp = openInApp,
|
||||
response = response,
|
||||
)
|
||||
components.core.store.dispatch(
|
||||
ContentAction.UpdateDownloadAction(session.id, download),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onWindowRequest(windowRequest: WindowRequest) {
|
||||
if (windowRequest.type == WindowRequest.Type.CLOSE) {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
} else {
|
||||
engineSession?.loadUrl(windowRequest.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigationStateChange(canGoBack: Boolean?, canGoForward: Boolean?) {
|
||||
canGoBack?.let { this.canGoBack = it }
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return if (canGoBack) {
|
||||
engineSession?.goBack()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
protected fun initializeSession(fromEngineSession: EngineSession? = null) {
|
||||
engineSession = fromEngineSession ?: components.core.engine.createSession()
|
||||
session = createCustomTab(
|
||||
url = "",
|
||||
source = SessionState.Source.Internal.CustomTab,
|
||||
).copy(engineState = EngineState(engineSession))
|
||||
components.core.store.dispatch(
|
||||
CustomTabListAction.AddCustomTabAction(session as CustomTabSessionState)
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("OVERRIDE_DEPRECATION")
|
||||
final override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray,
|
||||
) {
|
||||
when (requestCode) {
|
||||
REQUEST_CODE_PROMPT_PERMISSIONS -> promptsFeature.get()?.onPermissionsResult(permissions, grantResults)
|
||||
REQUEST_CODE_DOWNLOAD_PERMISSIONS -> downloadsFeature.get()?.onPermissionsResult(permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REQUEST_CODE_PROMPT_PERMISSIONS = 1
|
||||
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 2
|
||||
}
|
||||
}
|
||||
+30
-29
@@ -11,14 +11,12 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.R
|
||||
import mozilla.components.browser.state.action.WebExtensionAction
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
import mozilla.components.concept.engine.window.WindowRequest
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import eu.weblibre.flutter_mozilla_components.R
|
||||
|
||||
/**
|
||||
* An activity to show the pop up action of a web extension.
|
||||
@@ -53,21 +51,31 @@ class WebExtensionActionPopupActivity : AppCompatActivity() {
|
||||
|
||||
/**
|
||||
* A fragment to show the web extension action popup with [EngineView].
|
||||
*
|
||||
* Extends [AddonPopupBaseFragment] to get proper session lifecycle management,
|
||||
* prompt handling, and download support.
|
||||
*/
|
||||
class WebExtensionActionPopupFragment : Fragment(), EngineSession.Observer {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
class WebExtensionActionPopupFragment : AddonPopupBaseFragment(), EngineSession.Observer {
|
||||
|
||||
private var engineSession: EngineSession? = null
|
||||
private lateinit var webExtensionId: String
|
||||
|
||||
private val safeArguments get() = requireNotNull(arguments)
|
||||
private var sessionConsumed
|
||||
get() = safeArguments.getBoolean("isSessionConsumed", false)
|
||||
set(value) {
|
||||
safeArguments.putBoolean("isSessionConsumed", value)
|
||||
}
|
||||
|
||||
private val addonSettingsEngineView: EngineView
|
||||
get() = requireView().findViewById<View>(R.id.addonSettingsEngineView) as EngineView
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
webExtensionId = requireNotNull(arguments?.getString("web_extension_id"))
|
||||
engineSession = components.core.store.state.extensions[webExtensionId]?.popupSession
|
||||
|
||||
// Grab the popup session from the store if available and register it as a custom tab.
|
||||
components.core.store.state.extensions[webExtensionId]?.popupSession?.let {
|
||||
initializeSession(it)
|
||||
}
|
||||
|
||||
return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
|
||||
}
|
||||
@@ -82,38 +90,31 @@ class WebExtensionActionPopupActivity : AppCompatActivity() {
|
||||
} else {
|
||||
consumeFrom(components.core.store) { state ->
|
||||
state.extensions[webExtensionId]?.let { extState ->
|
||||
extState.popupSession?.let {
|
||||
if (engineSession == null) {
|
||||
addonSettingsEngineView.render(it)
|
||||
consumePopupSession()
|
||||
engineSession = it
|
||||
}
|
||||
val popupSession = extState.popupSession
|
||||
if (popupSession != null) {
|
||||
initializeSession(popupSession)
|
||||
addonSettingsEngineView.render(popupSession)
|
||||
popupSession.register(this)
|
||||
consumePopupSession()
|
||||
engineSession = popupSession
|
||||
} else if (sessionConsumed) {
|
||||
// Session was consumed but lost (e.g. Android recreated the activity).
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
engineSession?.register(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
engineSession?.unregister(this)
|
||||
}
|
||||
|
||||
override fun onWindowRequest(windowRequest: WindowRequest) {
|
||||
if (windowRequest.type == WindowRequest.Type.CLOSE) {
|
||||
activity?.onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private fun consumePopupSession() {
|
||||
components.core.store.dispatch(
|
||||
WebExtensionAction.UpdatePopupSessionAction(webExtensionId, popupSession = null),
|
||||
)
|
||||
sessionConsumed = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
Reference in New Issue
Block a user