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.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||||
|
import eu.weblibre.flutter_mozilla_components.R
|
||||||
import mozilla.components.browser.state.action.WebExtensionAction
|
import mozilla.components.browser.state.action.WebExtensionAction
|
||||||
import mozilla.components.concept.engine.EngineSession
|
import mozilla.components.concept.engine.EngineSession
|
||||||
import mozilla.components.concept.engine.EngineView
|
import mozilla.components.concept.engine.EngineView
|
||||||
import mozilla.components.concept.engine.window.WindowRequest
|
|
||||||
import mozilla.components.lib.state.ext.consumeFrom
|
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.
|
* 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].
|
* 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 {
|
class WebExtensionActionPopupFragment : AddonPopupBaseFragment(), EngineSession.Observer {
|
||||||
private val components by lazy {
|
|
||||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var engineSession: EngineSession? = null
|
|
||||||
private lateinit var webExtensionId: String
|
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
|
private val addonSettingsEngineView: EngineView
|
||||||
get() = requireView().findViewById<View>(R.id.addonSettingsEngineView) as EngineView
|
get() = requireView().findViewById<View>(R.id.addonSettingsEngineView) as EngineView
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
webExtensionId = requireNotNull(arguments?.getString("web_extension_id"))
|
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)
|
return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
|
||||||
}
|
}
|
||||||
@@ -82,38 +90,31 @@ class WebExtensionActionPopupActivity : AppCompatActivity() {
|
|||||||
} else {
|
} else {
|
||||||
consumeFrom(components.core.store) { state ->
|
consumeFrom(components.core.store) { state ->
|
||||||
state.extensions[webExtensionId]?.let { extState ->
|
state.extensions[webExtensionId]?.let { extState ->
|
||||||
extState.popupSession?.let {
|
val popupSession = extState.popupSession
|
||||||
if (engineSession == null) {
|
if (popupSession != null) {
|
||||||
addonSettingsEngineView.render(it)
|
initializeSession(popupSession)
|
||||||
consumePopupSession()
|
addonSettingsEngineView.render(popupSession)
|
||||||
engineSession = it
|
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() {
|
override fun onDestroyView() {
|
||||||
super.onStart()
|
super.onDestroyView()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun consumePopupSession() {
|
private fun consumePopupSession() {
|
||||||
components.core.store.dispatch(
|
components.core.store.dispatch(
|
||||||
WebExtensionAction.UpdatePopupSessionAction(webExtensionId, popupSession = null),
|
WebExtensionAction.UpdatePopupSessionAction(webExtensionId, popupSession = null),
|
||||||
)
|
)
|
||||||
|
sessionConsumed = true
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
Reference in New Issue
Block a user