mainly refactoring & addons
This commit is contained in:
@@ -128,3 +128,7 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
}
|
||||
|
||||
+176
-122
@@ -8,18 +8,23 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.fragment.app.Fragment
|
||||
import eu.lensai.flutter_mozilla_components.addons.WebExtensionActionPopupActivity
|
||||
import eu.lensai.flutter_mozilla_components.addons.WebExtensionPromptFeature
|
||||
import eu.lensai.flutter_mozilla_components.databinding.FragmentBrowserBinding
|
||||
import mozilla.components.browser.state.state.WebExtensionState
|
||||
import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey
|
||||
import eu.lensai.flutter_mozilla_components.services.DownloadService
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
import mozilla.components.feature.app.links.AppLinksFeature
|
||||
import mozilla.components.feature.downloads.DownloadsFeature
|
||||
import mozilla.components.feature.downloads.manager.FetchDownloadManager
|
||||
import mozilla.components.feature.downloads.temporary.ShareDownloadFeature
|
||||
import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
|
||||
import mozilla.components.feature.privatemode.feature.SecureWindowFeature
|
||||
import mozilla.components.feature.prompts.PromptFeature
|
||||
import mozilla.components.feature.session.FullScreenFeature
|
||||
import mozilla.components.feature.session.SessionFeature
|
||||
import mozilla.components.feature.session.SwipeRefreshFeature
|
||||
import mozilla.components.feature.sitepermissions.SitePermissionsFeature
|
||||
@@ -29,10 +34,9 @@ import mozilla.components.support.base.feature.ActivityResultHandler
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
|
||||
import mozilla.components.support.ktx.android.view.enterImmersiveMode
|
||||
import mozilla.components.support.ktx.android.view.exitImmersiveMode
|
||||
import mozilla.components.support.locale.ActivityContextWrapper
|
||||
import mozilla.components.support.utils.ext.requestInPlacePermissions
|
||||
import mozilla.components.support.webextensions.WebExtensionPopupObserver
|
||||
|
||||
/**
|
||||
* Base fragment extended by [BrowserFragment] and [ExternalAppBrowserFragment].
|
||||
@@ -42,153 +46,177 @@ import mozilla.components.support.webextensions.WebExtensionPopupObserver
|
||||
@SuppressWarnings("LargeClass")
|
||||
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ActivityResultHandler {
|
||||
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
|
||||
private val shareDownloadsFeature = ViewBoundFeatureWrapper<ShareDownloadFeature>()
|
||||
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
|
||||
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
|
||||
private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
|
||||
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
|
||||
private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
|
||||
private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
|
||||
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
|
||||
private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
|
||||
private val mediaSessionFullscreenFeature =
|
||||
ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
|
||||
|
||||
|
||||
protected val sessionId: String?
|
||||
private val sessionId: String?
|
||||
get() = arguments?.getString(SESSION_ID_KEY)
|
||||
|
||||
private var _binding: FragmentBrowserBinding? = null
|
||||
val binding get() = _binding!!
|
||||
|
||||
protected val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private val backButtonHandler: List<ViewBoundFeatureWrapper<*>> = listOf(
|
||||
fullScreenFeature,
|
||||
sessionFeature,
|
||||
)
|
||||
|
||||
private val activityResultHandler: List<ViewBoundFeatureWrapper<*>> = listOf(
|
||||
promptFeature,
|
||||
)
|
||||
|
||||
private var _engineView: EngineView? = null
|
||||
private var _binding: FragmentBrowserBinding? = null
|
||||
private var _components: Components? = null
|
||||
|
||||
val binding get() = _binding!!
|
||||
val components get() = _components!!
|
||||
val engineView get() = _engineView!!
|
||||
|
||||
protected abstract fun createEngine(components: Components) : EngineView
|
||||
|
||||
private lateinit var requestDownloadPermissionsLauncher: ActivityResultLauncher<Array<String>>
|
||||
private lateinit var requestSitePermissionsLauncher: ActivityResultLauncher<Array<String>>
|
||||
private lateinit var requestPromptsPermissionsLauncher: ActivityResultLauncher<Array<String>>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
requestDownloadPermissionsLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
|
||||
val permissions = results.keys.toTypedArray()
|
||||
val grantResults =
|
||||
results.values.map {
|
||||
if (it) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED
|
||||
}.toIntArray()
|
||||
downloadsFeature.withFeature {
|
||||
it.onPermissionsResult(permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
requestSitePermissionsLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
|
||||
val permissions = results.keys.toTypedArray()
|
||||
val grantResults =
|
||||
results.values.map {
|
||||
if (it) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED
|
||||
}.toIntArray()
|
||||
sitePermissionsFeature.withFeature {
|
||||
it.onPermissionsResult(permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
requestPromptsPermissionsLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
|
||||
val permissions = results.keys.toTypedArray()
|
||||
val grantResults =
|
||||
results.values.map {
|
||||
if (it) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED
|
||||
}.toIntArray()
|
||||
promptFeature.withFeature {
|
||||
it.onPermissionsResult(permissions, grantResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Suppress("LongMethod")
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentBrowserBinding.inflate(inflater, container, false)
|
||||
_components = GlobalComponents.components
|
||||
|
||||
_engineView = createEngine(components)
|
||||
_components?.engineView = _engineView
|
||||
|
||||
val engineNativeView = engineView.asView()
|
||||
// Set layout parameters
|
||||
val layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
engineNativeView.layoutParams = layoutParams
|
||||
|
||||
binding.swipeToRefresh.addView(engineNativeView)
|
||||
|
||||
val originalContext = ActivityContextWrapper.getOriginalContext(requireActivity())
|
||||
engineView.setActivityContext(originalContext)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
sessionFeature.set(
|
||||
feature = SessionFeature(
|
||||
components.store,
|
||||
components.sessionUseCases.goBack,
|
||||
components.sessionUseCases.goForward,
|
||||
engineView,
|
||||
components.core.store,
|
||||
components.useCases.sessionUseCases.goBack,
|
||||
components.useCases.sessionUseCases.goForward,
|
||||
components.engineView!!,
|
||||
sessionId,
|
||||
),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
view = view,
|
||||
)
|
||||
|
||||
swipeRefreshFeature.set(
|
||||
feature = SwipeRefreshFeature(
|
||||
components.store,
|
||||
components.sessionUseCases.reload,
|
||||
components.core.store,
|
||||
components.useCases.sessionUseCases.reload,
|
||||
binding.swipeToRefresh,
|
||||
),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
view = view,
|
||||
)
|
||||
|
||||
shareDownloadsFeature.set(
|
||||
ShareDownloadFeature(
|
||||
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.store,
|
||||
useCases = components.downloadsUseCases,
|
||||
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.store,
|
||||
components.core.store,
|
||||
DownloadService::class,
|
||||
notificationsDelegate = components.notificationsDelegate,
|
||||
),
|
||||
tabId = sessionId,
|
||||
onNeedToRequestPermissions = { permissions ->
|
||||
requestInPlacePermissions(REQUEST_KEY_DOWNLOAD_PERMISSIONS, permissions) { result ->
|
||||
downloadsFeature.get()?.onPermissionsResult(
|
||||
result.keys.toTypedArray(),
|
||||
result.values.map {
|
||||
when (it) {
|
||||
true -> PackageManager.PERMISSION_GRANTED
|
||||
false -> PackageManager.PERMISSION_DENIED
|
||||
}
|
||||
}.toIntArray(),
|
||||
)
|
||||
}
|
||||
requestDownloadPermissionsLauncher.launch(permissions)
|
||||
},
|
||||
),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
view = view,
|
||||
)
|
||||
|
||||
appLinksFeature.set(
|
||||
feature = AppLinksFeature(
|
||||
context = requireContext(),
|
||||
store = components.store,
|
||||
store = components.core.store,
|
||||
sessionId = sessionId,
|
||||
fragmentManager = parentFragmentManager,
|
||||
launchInApp = { components.preferences.getBoolean(DefaultComponents.PREF_LAUNCH_EXTERNAL_APP, false) },
|
||||
loadUrlUseCase = components.sessionUseCases.loadUrl,
|
||||
launchInApp = { components.core.prefs.getBoolean(context?.getPreferenceKey(R.string.pref_key_launch_external_app), false) },
|
||||
loadUrlUseCase = components.useCases.sessionUseCases.loadUrl,
|
||||
),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
view = view,
|
||||
)
|
||||
|
||||
promptFeature.set(
|
||||
feature = PromptFeature(
|
||||
fragment = this,
|
||||
store = components.store,
|
||||
store = components.core.store,
|
||||
customTabId = sessionId,
|
||||
tabsUseCases = components.tabsUseCases,
|
||||
tabsUseCases = components.useCases.tabsUseCases,
|
||||
fragmentManager = parentFragmentManager,
|
||||
fileUploadsDirCleaner = components.fileUploadsDirCleaner,
|
||||
fileUploadsDirCleaner = components.core.fileUploadsDirCleaner,
|
||||
onNeedToRequestPermissions = { permissions ->
|
||||
requestInPlacePermissions(REQUEST_KEY_PROMPT_PERMISSIONS, permissions) { result ->
|
||||
promptFeature.get()?.onPermissionsResult(
|
||||
result.keys.toTypedArray(),
|
||||
result.values.map {
|
||||
when (it) {
|
||||
true -> PackageManager.PERMISSION_GRANTED
|
||||
false -> PackageManager.PERMISSION_DENIED
|
||||
}
|
||||
}.toIntArray(),
|
||||
)
|
||||
}
|
||||
requestPromptsPermissionsLauncher.launch(permissions)
|
||||
},
|
||||
),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
view = view,
|
||||
)
|
||||
|
||||
sitePermissionsFeature.set(
|
||||
feature = SitePermissionsFeature(
|
||||
context = requireContext(),
|
||||
sessionId = sessionId,
|
||||
storage = components.permissionStorage,
|
||||
storage = components.core.geckoSitePermissionsStorage,
|
||||
fragmentManager = parentFragmentManager,
|
||||
sitePermissionsRules = SitePermissionsRules(
|
||||
autoplayAudible = AutoplayAction.BLOCKED,
|
||||
@@ -202,63 +230,92 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
crossOriginStorageAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
|
||||
),
|
||||
onNeedToRequestPermissions = { permissions ->
|
||||
requestInPlacePermissions(REQUEST_KEY_SITE_PERMISSIONS, permissions) { result ->
|
||||
sitePermissionsFeature.get()?.onPermissionsResult(
|
||||
result.keys.toTypedArray(),
|
||||
result.values.map {
|
||||
when (it) {
|
||||
true -> PackageManager.PERMISSION_GRANTED
|
||||
false -> PackageManager.PERMISSION_DENIED
|
||||
}
|
||||
}.toIntArray(),
|
||||
)
|
||||
}
|
||||
requestSitePermissionsLauncher.launch(permissions)
|
||||
},
|
||||
onShouldShowRequestPermissionRationale = { shouldShowRequestPermissionRationale(it) },
|
||||
store = components.store,
|
||||
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(
|
||||
components.core.store,
|
||||
components.useCases.sessionUseCases,
|
||||
sessionId,
|
||||
) { inFullScreen ->
|
||||
if (inFullScreen) {
|
||||
activity?.enterImmersiveMode()
|
||||
} else {
|
||||
activity?.exitImmersiveMode()
|
||||
}
|
||||
},
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
)
|
||||
|
||||
mediaSessionFullscreenFeature.set(
|
||||
feature = MediaSessionFullscreenFeature(
|
||||
requireActivity(),
|
||||
components.core.store,
|
||||
sessionId,
|
||||
),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
)
|
||||
|
||||
webExtensionPromptFeature.set(
|
||||
feature = WebExtensionPromptFeature(
|
||||
store = components.store,
|
||||
context = requireContext(),
|
||||
fragmentManager = parentFragmentManager,
|
||||
secureWindowFeature.set(
|
||||
feature = SecureWindowFeature(
|
||||
window = requireActivity().window,
|
||||
store = components.core.store,
|
||||
customTabId = sessionId,
|
||||
),
|
||||
owner = this,
|
||||
view = binding.root
|
||||
view = binding.root,
|
||||
)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
@Suppress("LongMethod")
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = FragmentBrowserBinding.inflate(inflater, container, false)
|
||||
|
||||
// Set layout parameters
|
||||
val layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
val secureWindowFeature = SecureWindowFeature(
|
||||
window = requireActivity().window,
|
||||
store = components.store,
|
||||
customTabId = sessionId,
|
||||
)
|
||||
val originalContext = ActivityContextWrapper.getOriginalContext(requireActivity())
|
||||
|
||||
val webExtensionPopupObserver = WebExtensionPopupObserver(components.store, ::openPopup)
|
||||
val engineView = createEngine(components)
|
||||
components.engineView = engineView
|
||||
|
||||
// Observe the lifecycle for supported features
|
||||
lifecycle.addObservers(
|
||||
secureWindowFeature,
|
||||
webExtensionPopupObserver,
|
||||
)
|
||||
val engineNativeView = engineView.asView()
|
||||
engineNativeView.layoutParams = layoutParams
|
||||
|
||||
engineView.setActivityContext(originalContext)
|
||||
|
||||
binding.swipeToRefresh.addView(engineNativeView)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
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
|
||||
override fun onBackPressed(): Boolean =
|
||||
listOf(sessionFeature).any { it.onBackPressed() }
|
||||
override fun onBackPressed(): Boolean {
|
||||
return backButtonHandler.any { it.onBackPressed() }
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
|
||||
@@ -268,10 +325,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
companion object {
|
||||
private const val SESSION_ID_KEY = "session_id"
|
||||
|
||||
private const val REQUEST_KEY_DOWNLOAD_PERMISSIONS = "downloadFeature"
|
||||
private const val REQUEST_KEY_PROMPT_PERMISSIONS = "promptFeature"
|
||||
private const val REQUEST_KEY_SITE_PERMISSIONS = "sitePermissionsFeature"
|
||||
|
||||
@JvmStatic
|
||||
protected fun Bundle.putSessionId(sessionId: String?) {
|
||||
putString(SESSION_ID_KEY, sessionId)
|
||||
@@ -279,7 +332,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
}
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
engineView.setActivityContext(null)
|
||||
|
||||
components.engineView?.setActivityContext(null)
|
||||
_binding = null
|
||||
}
|
||||
}
|
||||
|
||||
+39
-57
@@ -2,107 +2,89 @@ package eu.lensai.flutter_mozilla_components
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
||||
import eu.lensai.flutter_mozilla_components.addons.WebExtensionActionPopupActivity
|
||||
import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature
|
||||
import eu.lensai.flutter_mozilla_components.integration.ReaderViewIntegration
|
||||
import mozilla.components.browser.state.state.WebExtensionState
|
||||
import mozilla.components.browser.thumbnails.BrowserThumbnails
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
|
||||
import mozilla.components.feature.session.FullScreenFeature
|
||||
import mozilla.components.feature.tabs.WindowFeature
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import mozilla.components.support.ktx.android.view.enterImmersiveMode
|
||||
import mozilla.components.support.ktx.android.view.exitImmersiveMode
|
||||
import mozilla.components.support.webextensions.WebExtensionPopupObserver
|
||||
|
||||
/**
|
||||
* Fragment used for browsing the web within the main app.
|
||||
*/
|
||||
class BrowserFragment(private val context: Context) : BaseBrowserFragment(), UserInteractionHandler {
|
||||
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
|
||||
private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
|
||||
private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewIntegration>()
|
||||
private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
|
||||
private val mediaSessionFullscreenFeature =
|
||||
ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
|
||||
private val webExtensionPopupObserver = ViewBoundFeatureWrapper<WebExtensionPopupObserver>()
|
||||
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
|
||||
|
||||
override fun createEngine(components: Components): EngineView {
|
||||
return components.engine.createView(context).apply {
|
||||
return components.core.engine.createView(context).apply {
|
||||
selectionActionDelegate = components.selectionAction
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
val binding = super.binding
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
readerViewFeature.set(
|
||||
feature = ReaderViewIntegration(
|
||||
requireContext(),
|
||||
components.engine,
|
||||
components.store,
|
||||
components.core.engine,
|
||||
components.core.store,
|
||||
binding.readerViewBar,
|
||||
components.readerViewEvents,
|
||||
components.events.readerViewEvents,
|
||||
components.readerViewController,
|
||||
),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
view = view,
|
||||
)
|
||||
|
||||
fullScreenFeature.set(
|
||||
feature = FullScreenFeature(
|
||||
components.store,
|
||||
components.sessionUseCases,
|
||||
sessionId,
|
||||
) { inFullScreen ->
|
||||
if (inFullScreen) {
|
||||
activity?.enterImmersiveMode()
|
||||
} else {
|
||||
activity?.exitImmersiveMode()
|
||||
}
|
||||
},
|
||||
windowFeature.set(
|
||||
feature = WindowFeature(components.core.store, components.useCases.tabsUseCases),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
)
|
||||
|
||||
mediaSessionFullscreenFeature.set(
|
||||
feature = MediaSessionFullscreenFeature(
|
||||
requireActivity(),
|
||||
components.store,
|
||||
sessionId,
|
||||
),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
view = view,
|
||||
)
|
||||
|
||||
thumbnailsFeature.set(
|
||||
feature = BrowserThumbnails(requireContext(), engineView, components.store),
|
||||
feature = BrowserThumbnails(requireContext(), components.engineView!!, components.core.store),
|
||||
owner = this,
|
||||
view = binding.root,
|
||||
view = view,
|
||||
)
|
||||
|
||||
val windowFeature = WindowFeature(components.store, components.tabsUseCases)
|
||||
lifecycle.addObserver(windowFeature)
|
||||
webExtensionPopupObserver.set(
|
||||
feature = WebExtensionPopupObserver(components.core.store, ::openPopup),
|
||||
owner = this,
|
||||
view = view,
|
||||
)
|
||||
|
||||
return binding.root
|
||||
webExtToolbarFeature.set(
|
||||
feature = components.features.webExtensionToolbarFeature,
|
||||
owner = this,
|
||||
view = view,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return when {
|
||||
fullScreenFeature.onBackPressed() -> true
|
||||
else -> super.onBackPressed()
|
||||
}
|
||||
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 =
|
||||
readerViewFeature.onBackPressed() || super.onBackPressed()
|
||||
|
||||
companion object {
|
||||
fun create(context: Context, sessionId: String? = null) = BrowserFragment(context).apply {
|
||||
arguments = Bundle().apply {
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import eu.lensai.flutter_mozilla_components.components.Core
|
||||
import eu.lensai.flutter_mozilla_components.components.Events
|
||||
import eu.lensai.flutter_mozilla_components.components.Features
|
||||
import eu.lensai.flutter_mozilla_components.components.Services
|
||||
import eu.lensai.flutter_mozilla_components.components.UseCases
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||
import mozilla.components.support.base.android.NotificationsDelegate
|
||||
|
||||
class Components(private val context: Context,
|
||||
val flutterEvents: GeckoStateEvents,
|
||||
val readerViewController: ReaderViewController,
|
||||
val selectionAction: SelectionActionDelegate,
|
||||
val addonEvents: GeckoAddonEvents
|
||||
) {
|
||||
val core by lazy { Core(context, this, flutterEvents) }
|
||||
val events by lazy { Events(flutterEvents) }
|
||||
val useCases by lazy { UseCases(context, core.engine, core.store) }
|
||||
val services by lazy { Services(context, useCases.tabsUseCases) }
|
||||
val features by lazy { Features(core.store, addonEvents) }
|
||||
|
||||
var engineView: EngineView? = null
|
||||
var engineReportedInitialized = false
|
||||
|
||||
private val notificationManagerCompat = NotificationManagerCompat.from(context)
|
||||
val notificationsDelegate: NotificationsDelegate by lazy {
|
||||
NotificationsDelegate(
|
||||
notificationManagerCompat,
|
||||
)
|
||||
}
|
||||
}
|
||||
+415
-415
@@ -1,415 +1,415 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import eu.lensai.flutter_mozilla_components.api.ReaderViewEventsImpl
|
||||
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
|
||||
import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.FindResultState
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.HistoryItem
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.HistoryState
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderableState
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.TabContentState
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import mozilla.components.browser.engine.gecko.GeckoEngine
|
||||
import mozilla.components.browser.icons.BrowserIcons
|
||||
import mozilla.components.browser.session.storage.SessionStorage
|
||||
import mozilla.components.browser.state.engine.EngineMiddleware
|
||||
import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
|
||||
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
||||
import mozilla.components.concept.base.crash.Breadcrumb
|
||||
import mozilla.components.concept.engine.DefaultSettings
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
|
||||
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||
import mozilla.components.concept.fetch.Client
|
||||
import mozilla.components.feature.addons.AddonManager
|
||||
import mozilla.components.feature.addons.amo.AMOAddonsProvider
|
||||
import mozilla.components.feature.addons.logger
|
||||
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
|
||||
import mozilla.components.feature.addons.update.DefaultAddonUpdater
|
||||
import mozilla.components.feature.app.links.AppLinksInterceptor
|
||||
import mozilla.components.feature.app.links.AppLinksUseCases
|
||||
import mozilla.components.feature.downloads.DownloadMiddleware
|
||||
import mozilla.components.feature.downloads.DownloadsUseCases
|
||||
import mozilla.components.feature.media.MediaSessionFeature
|
||||
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
|
||||
import mozilla.components.feature.prompts.PromptMiddleware
|
||||
import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
|
||||
import mozilla.components.feature.readerview.ReaderViewMiddleware
|
||||
import mozilla.components.feature.session.SessionUseCases
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import mozilla.components.feature.session.middleware.LastAccessMiddleware
|
||||
import mozilla.components.feature.session.middleware.undo.UndoMiddleware
|
||||
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
|
||||
import mozilla.components.feature.webnotifications.WebNotificationFeature
|
||||
import mozilla.components.lib.crash.Crash
|
||||
import mozilla.components.lib.crash.CrashReporter
|
||||
import mozilla.components.lib.crash.service.CrashReporterService
|
||||
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
|
||||
import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
|
||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||
import mozilla.components.lib.state.ext.flowScoped
|
||||
import mozilla.components.service.digitalassetlinks.local.StatementApi
|
||||
import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
|
||||
import mozilla.components.support.base.android.NotificationsDelegate
|
||||
import mozilla.components.support.base.worker.Frequency
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val DAY_IN_MINUTES = 24 * 60L
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Suppress("LargeClass")
|
||||
open class DefaultComponents(
|
||||
private val applicationContext: Context,
|
||||
val flutterEvents: GeckoStateEvents,
|
||||
val readerViewController: ReaderViewController,
|
||||
val selectionAction: SelectionActionDelegate,
|
||||
) {
|
||||
companion object {
|
||||
const val SAMPLE_BROWSER_PREFERENCES = "sample_browser_preferences"
|
||||
const val PREF_LAUNCH_EXTERNAL_APP = "sample_browser_launch_external_app"
|
||||
const val PREF_GLOBAL_PRIVACY_CONTROL = "sample_browser_global_privacy_control"
|
||||
}
|
||||
|
||||
var engineView: EngineView? = null
|
||||
var engineReportedInitialized = false
|
||||
|
||||
val preferences: SharedPreferences =
|
||||
applicationContext.getSharedPreferences(SAMPLE_BROWSER_PREFERENCES, Context.MODE_PRIVATE)
|
||||
|
||||
private val securePreferences by lazy { SecureAbove22Preferences(applicationContext, "key_store") }
|
||||
|
||||
val publicSuffixList by lazy { PublicSuffixList(applicationContext) }
|
||||
|
||||
// Engine Settings
|
||||
val engineSettings by lazy {
|
||||
DefaultSettings().apply {
|
||||
//historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
|
||||
requestInterceptor = AppRequestInterceptor(applicationContext)
|
||||
remoteDebuggingEnabled = true
|
||||
supportMultipleWindows = true
|
||||
preferredColorScheme = PreferredColorScheme.Dark
|
||||
httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED
|
||||
globalPrivacyControlEnabled = preferences.getBoolean(
|
||||
PREF_GLOBAL_PRIVACY_CONTROL,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val notificationManagerCompat = NotificationManagerCompat.from(applicationContext)
|
||||
|
||||
val notificationsDelegate: NotificationsDelegate by lazy {
|
||||
NotificationsDelegate(
|
||||
notificationManagerCompat,
|
||||
)
|
||||
}
|
||||
|
||||
val addonUpdater =
|
||||
DefaultAddonUpdater(applicationContext, Frequency(1, TimeUnit.DAYS), notificationsDelegate)
|
||||
|
||||
// Engine
|
||||
open val engine: Engine by lazy {
|
||||
GeckoEngine(applicationContext, engineSettings)
|
||||
}
|
||||
|
||||
val icons by lazy { BrowserIcons(applicationContext, client) }
|
||||
|
||||
open val client: Client by lazy { HttpURLConnectionClient() }
|
||||
|
||||
// Storage
|
||||
//private val lazyHistoryStorage = lazy { PlacesHistoryStorage(applicationContext) }
|
||||
//val historyStorage by lazy { lazyHistoryStorage.value }
|
||||
|
||||
val sessionStorage by lazy { SessionStorage(applicationContext, engine) }
|
||||
|
||||
val permissionStorage by lazy { OnDiskSitePermissionsStorage(applicationContext) }
|
||||
|
||||
val thumbnailStorage by lazy { ThumbnailStorage(applicationContext) }
|
||||
|
||||
val fileUploadsDirCleaner: FileUploadsDirCleaner by lazy {
|
||||
FileUploadsDirCleaner { applicationContext.cacheDir }
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val store by lazy {
|
||||
BrowserStore(
|
||||
middleware = listOf(
|
||||
FlutterEventMiddleware(flutterEvents),
|
||||
DownloadMiddleware(applicationContext, DownloadService::class.java),
|
||||
ReaderViewMiddleware(),
|
||||
ThumbnailsMiddleware(thumbnailStorage),
|
||||
UndoMiddleware(),
|
||||
RecordingDevicesMiddleware(applicationContext, notificationsDelegate),
|
||||
LastAccessMiddleware(),
|
||||
PromptMiddleware(),
|
||||
SessionPrioritizationMiddleware(),
|
||||
) + EngineMiddleware.create(engine),
|
||||
).apply {
|
||||
this.flowScoped { flow ->
|
||||
flow.map { state -> state.selectedTabId }
|
||||
.distinctUntilChanged()
|
||||
.collect { tabId ->
|
||||
flutterEvents.onSelectedTabChange(
|
||||
System.currentTimeMillis(),
|
||||
tabId
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
this.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.content
|
||||
}
|
||||
.ifAnyChanged { arrayOf (it.content.icon) }
|
||||
.debounce { 50 }
|
||||
.collect { tab ->
|
||||
val iconBytes = tab.content.icon?.toWebPBytes()
|
||||
flutterEvents.onIconChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
iconBytes
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
this.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.content.securityInfo
|
||||
}
|
||||
.debounce { 50 }
|
||||
.collect { tab ->
|
||||
flutterEvents.onSecurityInfoStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
SecurityInfoState(
|
||||
tab.content.securityInfo.secure,
|
||||
tab.content.securityInfo.host,
|
||||
tab.content.securityInfo.issuer,
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
this.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.readerState
|
||||
}
|
||||
.ifAnyChanged { arrayOf(
|
||||
it.readerState.readerable,
|
||||
it.readerState.active,
|
||||
)
|
||||
}
|
||||
.debounce { 50 }
|
||||
.collect { tab ->
|
||||
flutterEvents.onReaderableStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
ReaderableState(
|
||||
tab.readerState.readerable,
|
||||
tab.readerState.active,
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
this.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.content
|
||||
}
|
||||
.ifAnyChanged { arrayOf(
|
||||
it.content.history,
|
||||
it.content.canGoBack,
|
||||
it.content.canGoForward,
|
||||
)
|
||||
}
|
||||
.debounce { 50 }
|
||||
.collect { tab ->
|
||||
flutterEvents.onHistoryStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
HistoryState(
|
||||
items = tab.content.history.items.map { item -> HistoryItem(
|
||||
url = item.uri,
|
||||
title = item.title
|
||||
) },
|
||||
currentIndex = tab.content.history.currentIndex.toLong(),
|
||||
canGoBack = tab.content.canGoBack,
|
||||
canGoForward = tab.content.canGoForward,
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
this.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} }
|
||||
.distinctUntilChanged()
|
||||
.collect { tabs ->
|
||||
flutterEvents.onTabListChange(System.currentTimeMillis(), tabs) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
this.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.content
|
||||
}
|
||||
.ifAnyChanged { arrayOf(
|
||||
it.content.url,
|
||||
it.content.title,
|
||||
it.content.private,
|
||||
it.content.fullScreen,
|
||||
it.content.progress,
|
||||
it.content.loading)
|
||||
}
|
||||
.debounce { 50 }
|
||||
.collect { tab ->
|
||||
logger.info("title: ${tab.content.title} ${tab.content.url}")
|
||||
flutterEvents.onTabContentStateChange(
|
||||
System.currentTimeMillis(),
|
||||
TabContentState(
|
||||
id = tab.id,
|
||||
contextId = tab.contextId,
|
||||
url = tab.content.url,
|
||||
title = tab.content.title,
|
||||
progress = tab.content.progress.toLong(),
|
||||
isPrivate = tab.content.private,
|
||||
isFullScreen = tab.content.fullScreen,
|
||||
isLoading = tab.content.loading
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
this.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.content.findResults
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collect { tab ->
|
||||
tab.content.findResults
|
||||
flutterEvents.onFindResults(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
tab.content.findResults.map { result -> FindResultState(
|
||||
activeMatchOrdinal = result.activeMatchOrdinal.toLong(),
|
||||
numberOfMatches = result.numberOfMatches.toLong(),
|
||||
isDoneCounting = result.isDoneCounting,
|
||||
) }
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
icons.install(engine, this)
|
||||
|
||||
WebNotificationFeature(
|
||||
applicationContext,
|
||||
engine,
|
||||
icons,
|
||||
R.drawable.ic_launcher_foreground,
|
||||
permissionStorage,
|
||||
NotificationActivity::class.java,
|
||||
notificationsDelegate = notificationsDelegate,
|
||||
)
|
||||
|
||||
MediaSessionFeature(applicationContext, MediaSessionService::class.java, this).start()
|
||||
}
|
||||
}
|
||||
|
||||
val sessionUseCases by lazy { SessionUseCases(store) }
|
||||
val tabsUseCases by lazy { TabsUseCases(store) }
|
||||
|
||||
val readerViewEvents by lazy { ReaderViewEventsImpl() }
|
||||
|
||||
// Addons
|
||||
val addonManager by lazy {
|
||||
AddonManager(store, engine, addonsProvider, addonUpdater)
|
||||
}
|
||||
|
||||
val addonsProvider by lazy {
|
||||
AMOAddonsProvider(
|
||||
applicationContext,
|
||||
client,
|
||||
collectionName = "7dfae8669acc4312a65e8ba5553036",
|
||||
maxCacheAgeInMinutes = DAY_IN_MINUTES,
|
||||
)
|
||||
}
|
||||
|
||||
val supportedAddonsChecker by lazy {
|
||||
DefaultSupportedAddonsChecker(applicationContext, Frequency(1, TimeUnit.DAYS))
|
||||
}
|
||||
|
||||
val appLinksUseCases by lazy { AppLinksUseCases(applicationContext) }
|
||||
|
||||
val appLinksInterceptor by lazy {
|
||||
AppLinksInterceptor(
|
||||
applicationContext,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = {
|
||||
preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Digital Asset Links checking
|
||||
val relationChecker by lazy {
|
||||
StatementRelationChecker(StatementApi(client))
|
||||
}
|
||||
|
||||
val downloadsUseCases: DownloadsUseCases by lazy { DownloadsUseCases(store) }
|
||||
|
||||
val crashReporter: CrashReporter by lazy {
|
||||
CrashReporter(
|
||||
applicationContext,
|
||||
services = listOf(
|
||||
object : CrashReporterService {
|
||||
override val id: String
|
||||
get() = "xxx"
|
||||
override val name: String
|
||||
get() = "Test"
|
||||
|
||||
override fun createCrashReportUrl(identifier: String): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun report(crash: Crash.UncaughtExceptionCrash): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun report(crash: Crash.NativeCodeCrash): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun report(
|
||||
throwable: Throwable,
|
||||
breadcrumbs: ArrayList<Breadcrumb>,
|
||||
): String? {
|
||||
return null
|
||||
}
|
||||
},
|
||||
),
|
||||
notificationsDelegate = notificationsDelegate,
|
||||
).install(applicationContext)
|
||||
}
|
||||
}
|
||||
//package eu.lensai.flutter_mozilla_components
|
||||
//
|
||||
//
|
||||
//import android.annotation.SuppressLint
|
||||
//import android.content.Context
|
||||
//import android.content.SharedPreferences
|
||||
//import androidx.core.app.NotificationManagerCompat
|
||||
//import eu.lensai.flutter_mozilla_components.api.ReaderViewEventsImpl
|
||||
//import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
|
||||
//import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware
|
||||
//import eu.lensai.flutter_mozilla_components.pigeons.FindResultState
|
||||
//import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
//import eu.lensai.flutter_mozilla_components.pigeons.HistoryItem
|
||||
//import eu.lensai.flutter_mozilla_components.pigeons.HistoryState
|
||||
//import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
||||
//import eu.lensai.flutter_mozilla_components.pigeons.ReaderableState
|
||||
//import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState
|
||||
//import eu.lensai.flutter_mozilla_components.pigeons.TabContentState
|
||||
//import kotlinx.coroutines.FlowPreview
|
||||
//import kotlinx.coroutines.flow.debounce
|
||||
//import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
//import kotlinx.coroutines.flow.map
|
||||
//import kotlinx.coroutines.flow.mapNotNull
|
||||
//import mozilla.components.browser.engine.gecko.GeckoEngine
|
||||
//import mozilla.components.browser.icons.BrowserIcons
|
||||
//import mozilla.components.browser.session.storage.SessionStorage
|
||||
//import mozilla.components.browser.state.engine.EngineMiddleware
|
||||
//import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware
|
||||
//import mozilla.components.browser.state.store.BrowserStore
|
||||
//import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
|
||||
//import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
||||
//import mozilla.components.concept.base.crash.Breadcrumb
|
||||
//import mozilla.components.concept.engine.DefaultSettings
|
||||
//import mozilla.components.concept.engine.Engine
|
||||
//import mozilla.components.concept.engine.EngineView
|
||||
//import mozilla.components.concept.engine.mediaquery.PreferredColorScheme
|
||||
//import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||
//import mozilla.components.concept.fetch.Client
|
||||
//import mozilla.components.feature.addons.AddonManager
|
||||
//import mozilla.components.feature.addons.amo.AMOAddonsProvider
|
||||
//import mozilla.components.feature.addons.logger
|
||||
//import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
|
||||
//import mozilla.components.feature.addons.update.DefaultAddonUpdater
|
||||
//import mozilla.components.feature.app.links.AppLinksInterceptor
|
||||
//import mozilla.components.feature.app.links.AppLinksUseCases
|
||||
//import mozilla.components.feature.downloads.DownloadMiddleware
|
||||
//import mozilla.components.feature.downloads.DownloadsUseCases
|
||||
//import mozilla.components.feature.media.MediaSessionFeature
|
||||
//import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
|
||||
//import mozilla.components.feature.prompts.PromptMiddleware
|
||||
//import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
|
||||
//import mozilla.components.feature.readerview.ReaderViewMiddleware
|
||||
//import mozilla.components.feature.session.SessionUseCases
|
||||
//import mozilla.components.feature.tabs.TabsUseCases
|
||||
//import mozilla.components.feature.session.middleware.LastAccessMiddleware
|
||||
//import mozilla.components.feature.session.middleware.undo.UndoMiddleware
|
||||
//import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
|
||||
//import mozilla.components.feature.webnotifications.WebNotificationFeature
|
||||
//import mozilla.components.lib.crash.Crash
|
||||
//import mozilla.components.lib.crash.CrashReporter
|
||||
//import mozilla.components.lib.crash.service.CrashReporterService
|
||||
//import mozilla.components.lib.dataprotect.SecureAbove22Preferences
|
||||
//import mozilla.components.lib.fetch.httpurlconnection.HttpURLConnectionClient
|
||||
//import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
||||
//import mozilla.components.lib.state.ext.flowScoped
|
||||
//import mozilla.components.service.digitalassetlinks.local.StatementApi
|
||||
//import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
|
||||
//import mozilla.components.support.base.android.NotificationsDelegate
|
||||
//import mozilla.components.support.base.worker.Frequency
|
||||
//import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
|
||||
//import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
|
||||
//import java.util.concurrent.TimeUnit
|
||||
//
|
||||
//private const val DAY_IN_MINUTES = 24 * 60L
|
||||
//
|
||||
//@SuppressLint("NewApi")
|
||||
//@Suppress("LargeClass")
|
||||
//open class DefaultComponents(
|
||||
// private val applicationContext: Context,
|
||||
// val flutterEvents: GeckoStateEvents,
|
||||
// val readerViewController: ReaderViewController,
|
||||
// val selectionAction: SelectionActionDelegate,
|
||||
//) {
|
||||
// companion object {
|
||||
// const val SAMPLE_BROWSER_PREFERENCES = "sample_browser_preferences"
|
||||
// const val PREF_LAUNCH_EXTERNAL_APP = "sample_browser_launch_external_app"
|
||||
// const val PREF_GLOBAL_PRIVACY_CONTROL = "sample_browser_global_privacy_control"
|
||||
// }
|
||||
//
|
||||
// var engineView: EngineView? = null
|
||||
// var engineReportedInitialized = false
|
||||
//
|
||||
// val preferences: SharedPreferences =
|
||||
// applicationContext.getSharedPreferences(SAMPLE_BROWSER_PREFERENCES, Context.MODE_PRIVATE)
|
||||
//
|
||||
// private val securePreferences by lazy { SecureAbove22Preferences(applicationContext, "key_store") }
|
||||
//
|
||||
// val publicSuffixList by lazy { PublicSuffixList(applicationContext) }
|
||||
//
|
||||
// // Engine Settings
|
||||
// val engineSettings by lazy {
|
||||
// DefaultSettings().apply {
|
||||
// //historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
|
||||
// requestInterceptor = AppRequestInterceptor(applicationContext)
|
||||
// remoteDebuggingEnabled = true
|
||||
// supportMultipleWindows = true
|
||||
// preferredColorScheme = PreferredColorScheme.Dark
|
||||
// httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED
|
||||
// globalPrivacyControlEnabled = preferences.getBoolean(
|
||||
// PREF_GLOBAL_PRIVACY_CONTROL,
|
||||
// false,
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private val notificationManagerCompat = NotificationManagerCompat.from(applicationContext)
|
||||
//
|
||||
// val notificationsDelegate: NotificationsDelegate by lazy {
|
||||
// NotificationsDelegate(
|
||||
// notificationManagerCompat,
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// val addonUpdater =
|
||||
// DefaultAddonUpdater(applicationContext, Frequency(1, TimeUnit.DAYS), notificationsDelegate)
|
||||
//
|
||||
// // Engine
|
||||
// open val engine: Engine by lazy {
|
||||
// GeckoEngine(applicationContext, engineSettings)
|
||||
// }
|
||||
//
|
||||
// val icons by lazy { BrowserIcons(applicationContext, client) }
|
||||
//
|
||||
// open val client: Client by lazy { HttpURLConnectionClient() }
|
||||
//
|
||||
// // Storage
|
||||
// //private val lazyHistoryStorage = lazy { PlacesHistoryStorage(applicationContext) }
|
||||
// //val historyStorage by lazy { lazyHistoryStorage.value }
|
||||
//
|
||||
// val sessionStorage by lazy { SessionStorage(applicationContext, engine) }
|
||||
//
|
||||
// val permissionStorage by lazy { OnDiskSitePermissionsStorage(applicationContext) }
|
||||
//
|
||||
// val thumbnailStorage by lazy { ThumbnailStorage(applicationContext) }
|
||||
//
|
||||
// val fileUploadsDirCleaner: FileUploadsDirCleaner by lazy {
|
||||
// FileUploadsDirCleaner { applicationContext.cacheDir }
|
||||
// }
|
||||
//
|
||||
// @OptIn(FlowPreview::class)
|
||||
// val store by lazy {
|
||||
// BrowserStore(
|
||||
// middleware = listOf(
|
||||
// FlutterEventMiddleware(flutterEvents),
|
||||
// DownloadMiddleware(applicationContext, DownloadService::class.java),
|
||||
// ReaderViewMiddleware(),
|
||||
// ThumbnailsMiddleware(thumbnailStorage),
|
||||
// UndoMiddleware(),
|
||||
// RecordingDevicesMiddleware(applicationContext, notificationsDelegate),
|
||||
// LastAccessMiddleware(),
|
||||
// PromptMiddleware(),
|
||||
// SessionPrioritizationMiddleware(),
|
||||
// ) + EngineMiddleware.create(engine),
|
||||
// ).apply {
|
||||
// this.flowScoped { flow ->
|
||||
// flow.map { state -> state.selectedTabId }
|
||||
// .distinctUntilChanged()
|
||||
// .collect { tabId ->
|
||||
// flutterEvents.onSelectedTabChange(
|
||||
// System.currentTimeMillis(),
|
||||
// tabId
|
||||
// ) { _ -> }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// this.flowScoped { flow ->
|
||||
// flow.mapNotNull { state -> state.tabs }
|
||||
// .filterChanged {
|
||||
// it.content
|
||||
// }
|
||||
// .ifAnyChanged { arrayOf (it.content.icon) }
|
||||
// .debounce { 50 }
|
||||
// .collect { tab ->
|
||||
// val iconBytes = tab.content.icon?.toWebPBytes()
|
||||
// flutterEvents.onIconChange(
|
||||
// System.currentTimeMillis(),
|
||||
// tab.id,
|
||||
// iconBytes
|
||||
// ) { _ -> }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// this.flowScoped { flow ->
|
||||
// flow.mapNotNull { state -> state.tabs }
|
||||
// .filterChanged {
|
||||
// it.content.securityInfo
|
||||
// }
|
||||
// .debounce { 50 }
|
||||
// .collect { tab ->
|
||||
// flutterEvents.onSecurityInfoStateChange(
|
||||
// System.currentTimeMillis(),
|
||||
// tab.id,
|
||||
// SecurityInfoState(
|
||||
// tab.content.securityInfo.secure,
|
||||
// tab.content.securityInfo.host,
|
||||
// tab.content.securityInfo.issuer,
|
||||
// )
|
||||
// ) { _ -> }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// this.flowScoped { flow ->
|
||||
// flow.mapNotNull { state -> state.tabs }
|
||||
// .filterChanged {
|
||||
// it.readerState
|
||||
// }
|
||||
// .ifAnyChanged { arrayOf(
|
||||
// it.readerState.readerable,
|
||||
// it.readerState.active,
|
||||
// )
|
||||
// }
|
||||
// .debounce { 50 }
|
||||
// .collect { tab ->
|
||||
// flutterEvents.onReaderableStateChange(
|
||||
// System.currentTimeMillis(),
|
||||
// tab.id,
|
||||
// ReaderableState(
|
||||
// tab.readerState.readerable,
|
||||
// tab.readerState.active,
|
||||
// )
|
||||
// ) { _ -> }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// this.flowScoped { flow ->
|
||||
// flow.mapNotNull { state -> state.tabs }
|
||||
// .filterChanged {
|
||||
// it.content
|
||||
// }
|
||||
// .ifAnyChanged { arrayOf(
|
||||
// it.content.history,
|
||||
// it.content.canGoBack,
|
||||
// it.content.canGoForward,
|
||||
// )
|
||||
// }
|
||||
// .debounce { 50 }
|
||||
// .collect { tab ->
|
||||
// flutterEvents.onHistoryStateChange(
|
||||
// System.currentTimeMillis(),
|
||||
// tab.id,
|
||||
// HistoryState(
|
||||
// items = tab.content.history.items.map { item -> HistoryItem(
|
||||
// url = item.uri,
|
||||
// title = item.title
|
||||
// ) },
|
||||
// currentIndex = tab.content.history.currentIndex.toLong(),
|
||||
// canGoBack = tab.content.canGoBack,
|
||||
// canGoForward = tab.content.canGoForward,
|
||||
// )
|
||||
// ) { _ -> }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// this.flowScoped { flow ->
|
||||
// flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} }
|
||||
// .distinctUntilChanged()
|
||||
// .collect { tabs ->
|
||||
// flutterEvents.onTabListChange(System.currentTimeMillis(), tabs) { _ -> }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// this.flowScoped { flow ->
|
||||
// flow.mapNotNull { state -> state.tabs }
|
||||
// .filterChanged {
|
||||
// it.content
|
||||
// }
|
||||
// .ifAnyChanged { arrayOf(
|
||||
// it.content.url,
|
||||
// it.content.title,
|
||||
// it.content.private,
|
||||
// it.content.fullScreen,
|
||||
// it.content.progress,
|
||||
// it.content.loading)
|
||||
// }
|
||||
// .debounce { 50 }
|
||||
// .collect { tab ->
|
||||
// logger.info("title: ${tab.content.title} ${tab.content.url}")
|
||||
// flutterEvents.onTabContentStateChange(
|
||||
// System.currentTimeMillis(),
|
||||
// TabContentState(
|
||||
// id = tab.id,
|
||||
// contextId = tab.contextId,
|
||||
// url = tab.content.url,
|
||||
// title = tab.content.title,
|
||||
// progress = tab.content.progress.toLong(),
|
||||
// isPrivate = tab.content.private,
|
||||
// isFullScreen = tab.content.fullScreen,
|
||||
// isLoading = tab.content.loading
|
||||
// )
|
||||
// ) { _ -> }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// this.flowScoped { flow ->
|
||||
// flow.mapNotNull { state -> state.tabs }
|
||||
// .filterChanged {
|
||||
// it.content.findResults
|
||||
// }
|
||||
// .distinctUntilChanged()
|
||||
// .collect { tab ->
|
||||
// tab.content.findResults
|
||||
// flutterEvents.onFindResults(
|
||||
// System.currentTimeMillis(),
|
||||
// tab.id,
|
||||
// tab.content.findResults.map { result -> FindResultState(
|
||||
// activeMatchOrdinal = result.activeMatchOrdinal.toLong(),
|
||||
// numberOfMatches = result.numberOfMatches.toLong(),
|
||||
// isDoneCounting = result.isDoneCounting,
|
||||
// ) }
|
||||
// ) { _ -> }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// icons.install(engine, this)
|
||||
//
|
||||
// WebNotificationFeature(
|
||||
// applicationContext,
|
||||
// engine,
|
||||
// icons,
|
||||
// R.drawable.ic_launcher_foreground,
|
||||
// permissionStorage,
|
||||
// NotificationActivity::class.java,
|
||||
// notificationsDelegate = notificationsDelegate,
|
||||
// )
|
||||
//
|
||||
// MediaSessionFeature(applicationContext, MediaSessionService::class.java, this).start()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// val sessionUseCases by lazy { SessionUseCases(store) }
|
||||
// val tabsUseCases by lazy { TabsUseCases(store) }
|
||||
//
|
||||
// val readerViewEvents by lazy { ReaderViewEventsImpl() }
|
||||
//
|
||||
// // Addons
|
||||
// val addonManager by lazy {
|
||||
// AddonManager(store, engine, addonsProvider, addonUpdater)
|
||||
// }
|
||||
//
|
||||
// val addonsProvider by lazy {
|
||||
// AMOAddonsProvider(
|
||||
// applicationContext,
|
||||
// client,
|
||||
// collectionName = "7dfae8669acc4312a65e8ba5553036",
|
||||
// maxCacheAgeInMinutes = DAY_IN_MINUTES,
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// val supportedAddonsChecker by lazy {
|
||||
// DefaultSupportedAddonsChecker(applicationContext, Frequency(1, TimeUnit.DAYS))
|
||||
// }
|
||||
//
|
||||
// val appLinksUseCases by lazy { AppLinksUseCases(applicationContext) }
|
||||
//
|
||||
// val appLinksInterceptor by lazy {
|
||||
// AppLinksInterceptor(
|
||||
// applicationContext,
|
||||
// interceptLinkClicks = true,
|
||||
// launchInApp = {
|
||||
// preferences.getBoolean(PREF_LAUNCH_EXTERNAL_APP, false)
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// // Digital Asset Links checking
|
||||
// val relationChecker by lazy {
|
||||
// StatementRelationChecker(StatementApi(client))
|
||||
// }
|
||||
//
|
||||
// val downloadsUseCases: DownloadsUseCases by lazy { DownloadsUseCases(store) }
|
||||
//
|
||||
// val crashReporter: CrashReporter by lazy {
|
||||
// CrashReporter(
|
||||
// applicationContext,
|
||||
// services = listOf(
|
||||
// object : CrashReporterService {
|
||||
// override val id: String
|
||||
// get() = "xxx"
|
||||
// override val name: String
|
||||
// get() = "Test"
|
||||
//
|
||||
// override fun createCrashReportUrl(identifier: String): String? {
|
||||
// return null
|
||||
// }
|
||||
//
|
||||
// override fun report(crash: Crash.UncaughtExceptionCrash): String? {
|
||||
// return null
|
||||
// }
|
||||
//
|
||||
// override fun report(crash: Crash.NativeCodeCrash): String? {
|
||||
// return null
|
||||
// }
|
||||
//
|
||||
// override fun report(
|
||||
// throwable: Throwable,
|
||||
// breadcrumbs: ArrayList<Breadcrumb>,
|
||||
// ): String? {
|
||||
// return null
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// notificationsDelegate = notificationsDelegate,
|
||||
// ).install(applicationContext)
|
||||
// }
|
||||
//}
|
||||
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.feature.downloads.AbstractFetchDownloadService
|
||||
import mozilla.components.support.base.android.NotificationsDelegate
|
||||
|
||||
class DownloadService : AbstractFetchDownloadService() {
|
||||
override val httpClient by lazy { GlobalComponents.components!!.client }
|
||||
override val store: BrowserStore by lazy { GlobalComponents.components!!.store }
|
||||
override val notificationsDelegate: NotificationsDelegate by lazy { GlobalComponents.components!!.notificationsDelegate }
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/* 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.lensai.flutter_mozilla_components
|
||||
|
||||
import android.content.Context
|
||||
import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature
|
||||
import mozilla.components.browser.engine.gecko.GeckoEngine
|
||||
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||
import mozilla.components.concept.engine.DefaultSettings
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.fetch.Client
|
||||
import mozilla.components.feature.webcompat.WebCompatFeature
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||
|
||||
object EngineProvider {
|
||||
private var runtime: GeckoRuntime? = null
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateRuntime(context: Context): GeckoRuntime {
|
||||
if (runtime == null) {
|
||||
Logger.debug("Creating Runtime")
|
||||
val builder = GeckoRuntimeSettings.Builder()
|
||||
|
||||
// if (isCrashReportActive) {
|
||||
// builder.crashHandler(CrashHandlerService::class.java)
|
||||
// }
|
||||
|
||||
// About config it's no longer enabled by default
|
||||
builder.aboutConfigEnabled(true)
|
||||
builder.extensionsWebAPIEnabled(true)
|
||||
runtime = GeckoRuntime.create(context, builder.build())
|
||||
}
|
||||
|
||||
return runtime!!
|
||||
}
|
||||
|
||||
fun createEngine(context: Context, defaultSettings: DefaultSettings): Engine {
|
||||
Logger.debug("Creating Engine")
|
||||
val runtime = getOrCreateRuntime(context)
|
||||
|
||||
return GeckoEngine(context, defaultSettings, runtime).also {
|
||||
WebCompatFeature.install(it)
|
||||
CookieManagerFeature.install(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun createClient(context: Context): Client {
|
||||
Logger.debug("Fetching Client")
|
||||
val runtime = getOrCreateRuntime(context)
|
||||
return GeckoViewFetchClient(context, runtime)
|
||||
}
|
||||
}
|
||||
+14
-60
@@ -1,9 +1,10 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoAddonsApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoCookieApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl
|
||||
@@ -12,8 +13,9 @@ import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoSelectionActionControllerImpl
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.api.GeckoTabsApiImpl
|
||||
import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature
|
||||
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi
|
||||
@@ -30,68 +32,16 @@ import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import mozilla.components.browser.engine.gecko.GeckoEngine
|
||||
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||
import mozilla.components.experiment.NimbusExperimentDelegate
|
||||
import mozilla.components.feature.webcompat.WebCompatFeature
|
||||
import mozilla.components.lib.crash.handler.CrashHandlerService
|
||||
import mozilla.components.support.base.log.Log
|
||||
import mozilla.components.support.base.log.sink.AndroidLogSink
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||
|
||||
/**
|
||||
* Helper class for lazily instantiating components needed by the application.
|
||||
*/
|
||||
class Components(
|
||||
private val applicationContext: Context,
|
||||
flutterEvents: GeckoStateEvents,
|
||||
readerViewController: ReaderViewController,
|
||||
selectionAction: SelectionActionDelegate,
|
||||
) : DefaultComponents(
|
||||
applicationContext,
|
||||
flutterEvents,
|
||||
readerViewController,
|
||||
selectionAction
|
||||
) {
|
||||
private val runtime by lazy {
|
||||
// Allow for exfiltrating Gecko metrics through the Glean SDK.
|
||||
val builder = GeckoRuntimeSettings.Builder()
|
||||
.aboutConfigEnabled(true)
|
||||
.extensionsWebAPIEnabled(true)
|
||||
|
||||
builder.experimentDelegate(NimbusExperimentDelegate())
|
||||
builder.crashHandler(CrashHandlerService::class.java)
|
||||
|
||||
GeckoRuntime.create(applicationContext, builder.build())
|
||||
}
|
||||
|
||||
override val engine: Engine by lazy {
|
||||
GeckoEngine(applicationContext, engineSettings, runtime).also {
|
||||
// it.installBuiltInWebExtension("borderify@mozac.org", "resource://android/assets/extensions/borderify/") {
|
||||
// throwable ->
|
||||
// Log.log(Log.Priority.ERROR, "SampleBrowser", throwable, "Failed to install borderify")
|
||||
// }
|
||||
//
|
||||
// it.installBuiltInWebExtension("testext@mozac.org", "resource://android/assets/extensions/test/") {
|
||||
// throwable ->
|
||||
// Log.log(Log.Priority.ERROR, "SampleBrowser", throwable, "Failed to install testext")
|
||||
// }
|
||||
|
||||
WebCompatFeature.install(it)
|
||||
CookieManagerFeature.install(it)
|
||||
//WebCompatReporterFeature.install(it)
|
||||
}
|
||||
}
|
||||
|
||||
override val client by lazy { GeckoViewFetchClient(applicationContext, runtime) }
|
||||
}
|
||||
|
||||
|
||||
/** FlutterMozillaComponentsPlugin */
|
||||
class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private var activity: Activity? = null
|
||||
|
||||
private lateinit var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding;
|
||||
@@ -112,11 +62,14 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
val readerViewController =
|
||||
ReaderViewController(_flutterPluginBinding.binaryMessenger)
|
||||
|
||||
val addonEvents = GeckoAddonEvents(_flutterPluginBinding.binaryMessenger)
|
||||
|
||||
GlobalComponents.setUp(
|
||||
flutterPluginBinding.applicationContext,
|
||||
_flutterEvents,
|
||||
readerViewController,
|
||||
selectionActionDelegate
|
||||
selectionActionDelegate,
|
||||
addonEvents
|
||||
)
|
||||
|
||||
GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl {
|
||||
@@ -124,6 +77,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
})
|
||||
|
||||
GeckoEngineSettingsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoEngineSettingsApiImpl())
|
||||
GeckoAddonsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAddonsApiImpl(flutterPluginBinding.applicationContext))
|
||||
GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl())
|
||||
GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl())
|
||||
GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl())
|
||||
@@ -135,7 +89,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
|
||||
|
||||
ReaderViewEvents.setUp(
|
||||
_flutterPluginBinding.binaryMessenger,
|
||||
GlobalComponents.components!!.readerViewEvents
|
||||
components.events.readerViewEvents
|
||||
)
|
||||
|
||||
val intent = Intent(flutterPluginBinding.applicationContext, NotificationActivity::class.java)
|
||||
|
||||
+5
-1
@@ -26,6 +26,10 @@ private class NativeFragmentView(
|
||||
containerId: Int,
|
||||
private val flutterEvents: GeckoStateEvents
|
||||
) : PlatformView {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private val container: View
|
||||
|
||||
init {
|
||||
@@ -41,7 +45,7 @@ private class NativeFragmentView(
|
||||
override fun onFlutterViewAttached(flutterView: View) {
|
||||
super.onFlutterViewAttached(flutterView)
|
||||
|
||||
GlobalComponents.components!!.engineReportedInitialized = false;
|
||||
components.engineReportedInitialized = false;
|
||||
flutterEvents.onViewReadyStateChange(System.currentTimeMillis(),true) { _ -> }
|
||||
}
|
||||
|
||||
|
||||
+34
-18
@@ -1,6 +1,7 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
|
||||
import android.content.Context
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
@@ -11,7 +12,9 @@ import mozilla.components.concept.engine.selection.SelectionActionDelegate
|
||||
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
|
||||
import mozilla.components.support.base.facts.Facts
|
||||
import mozilla.components.support.base.facts.processor.LogFactProcessor
|
||||
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.util.concurrent.TimeUnit
|
||||
|
||||
@@ -23,60 +26,69 @@ object GlobalComponents {
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
private fun restoreBrowserState(newComponents: Components) = GlobalScope.launch(Dispatchers.Main) {
|
||||
newComponents.tabsUseCases.restore(newComponents.sessionStorage)
|
||||
newComponents.useCases.tabsUseCases.restore(newComponents.core.sessionStorage)
|
||||
|
||||
newComponents.sessionStorage.autoSave(newComponents.store)
|
||||
newComponents.core.sessionStorage.autoSave(newComponents.core.store)
|
||||
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
|
||||
.whenGoingToBackground()
|
||||
.whenSessionsChange()
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun setUp(
|
||||
applicationContext: Context,
|
||||
flutterEvents: GeckoStateEvents,
|
||||
readerViewController: ReaderViewController,
|
||||
selectionAction: SelectionActionDelegate
|
||||
selectionAction: SelectionActionDelegate,
|
||||
addonEvents: GeckoAddonEvents
|
||||
) {
|
||||
Logger.debug("Creating new components")
|
||||
|
||||
val newComponents = Components(
|
||||
applicationContext,
|
||||
flutterEvents,
|
||||
readerViewController,
|
||||
selectionAction
|
||||
selectionAction,
|
||||
addonEvents,
|
||||
)
|
||||
|
||||
newComponents.crashReporter.install(applicationContext)
|
||||
Facts.registerProcessor(LogFactProcessor())
|
||||
//newComponents.crashReporter.install(applicationContext)
|
||||
|
||||
newComponents.engine.warmUp()
|
||||
//Facts.registerProcessor(LogFactProcessor())
|
||||
|
||||
//RustHttpConfig.setClient(lazy { newComponents.core.client })
|
||||
|
||||
newComponents.core.engine.warmUp()
|
||||
|
||||
restoreBrowserState(newComponents)
|
||||
newComponents.downloadsUseCases.restoreDownloads()
|
||||
|
||||
//newComponents.useCases.downloadsUseCases.restoreDownloads()
|
||||
|
||||
try {
|
||||
GlobalAddonDependencyProvider.initialize(
|
||||
newComponents.addonManager,
|
||||
newComponents.addonUpdater,
|
||||
newComponents.core.addonManager,
|
||||
newComponents.core.addonUpdater,
|
||||
)
|
||||
|
||||
WebExtensionSupport.initialize(
|
||||
newComponents.engine,
|
||||
newComponents.store,
|
||||
newComponents.core.engine,
|
||||
newComponents.core.store,
|
||||
onNewTabOverride = {
|
||||
_, engineSession, url ->
|
||||
newComponents.tabsUseCases.addTab(url, selectTab = true, engineSession = engineSession)
|
||||
newComponents.useCases.tabsUseCases.addTab(url, selectTab = true, engineSession = engineSession)
|
||||
},
|
||||
onCloseTabOverride = {
|
||||
_, sessionId ->
|
||||
newComponents.tabsUseCases.removeTab(sessionId)
|
||||
newComponents.useCases.tabsUseCases.removeTab(sessionId)
|
||||
},
|
||||
onSelectTabOverride = {
|
||||
_, sessionId ->
|
||||
newComponents.tabsUseCases.selectTab(sessionId)
|
||||
newComponents.useCases.tabsUseCases.selectTab(sessionId)
|
||||
},
|
||||
onUpdatePermissionRequest = newComponents.addonUpdater::onUpdatePermissionRequest,
|
||||
onUpdatePermissionRequest = newComponents.core.addonUpdater::onUpdatePermissionRequest,
|
||||
onExtensionsLoaded = { extensions ->
|
||||
newComponents.addonUpdater.registerForFutureUpdates(extensions)
|
||||
newComponents.supportedAddonsChecker.registerForChecks()
|
||||
newComponents.core.addonUpdater.registerForFutureUpdates(extensions)
|
||||
newComponents.core.supportedAddonsChecker.registerForChecks()
|
||||
},
|
||||
)
|
||||
} catch (e: UnsupportedOperationException) {
|
||||
@@ -84,6 +96,10 @@ object GlobalComponents {
|
||||
Logger.error("Failed to initialize web extension support", e)
|
||||
}
|
||||
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
newComponents.core.fileUploadsDirCleaner.cleanUploadsDirectory()
|
||||
}
|
||||
|
||||
_components = newComponents
|
||||
}
|
||||
}
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class NotificationActivity: AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
GlobalComponents.components!!.notificationsDelegate.bindToActivity(this)
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package eu.lensai.flutter_mozilla_components.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import eu.lensai.flutter_mozilla_components.GlobalComponents
|
||||
|
||||
class NotificationActivity: AppCompatActivity() {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
components.notificationsDelegate.bindToActivity(this)
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -21,13 +21,14 @@ import mozilla.components.feature.addons.ui.translateName
|
||||
import mozilla.components.support.utils.ext.getParcelableCompat
|
||||
import mozilla.components.support.utils.ext.getParcelableExtraCompat
|
||||
import eu.lensai.flutter_mozilla_components.R
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
|
||||
/**
|
||||
* An activity to show the settings of an add-on.
|
||||
*/
|
||||
class AddonSettingsActivity : AppCompatActivity() {
|
||||
private val components: Components by lazy { GlobalComponents.components!! }
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -47,7 +48,7 @@ class AddonSettingsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
|
||||
when (name) {
|
||||
EngineView::class.java.name -> components.engine.createView(context, attrs).asView()
|
||||
EngineView::class.java.name -> components.core.engine.createView(context, attrs).asView()
|
||||
else -> super.onCreateView(parent, name, context, attrs)
|
||||
}
|
||||
|
||||
@@ -55,7 +56,9 @@ class AddonSettingsActivity : AppCompatActivity() {
|
||||
* A fragment to show the settings of an add-on with [EngineView].
|
||||
*/
|
||||
class AddonSettingsFragment : Fragment() {
|
||||
private val components: Components by lazy { GlobalComponents.components!! }
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private lateinit var optionsPageUrl: String
|
||||
private lateinit var engineSession: EngineSession
|
||||
@@ -68,7 +71,7 @@ class AddonSettingsActivity : AppCompatActivity() {
|
||||
)?.installedState?.optionsPageUrl,
|
||||
)
|
||||
|
||||
engineSession = components.engine.createSession()
|
||||
engineSession = components.core.engine.createSession()
|
||||
|
||||
return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
|
||||
}
|
||||
|
||||
+7
-5
@@ -30,7 +30,9 @@ import mozilla.components.feature.addons.R as MozComp
|
||||
* Fragment use for managing add-ons.
|
||||
*/
|
||||
class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
|
||||
private val components: Components by lazy { GlobalComponents.components!! }
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
@@ -54,7 +56,7 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
|
||||
bindRecyclerView(rootView)
|
||||
webExtensionPromptFeature.set(
|
||||
feature = WebExtensionPromptFeature(
|
||||
store = components.store,
|
||||
store = components.core.store,
|
||||
context = requireContext(),
|
||||
fragmentManager = parentFragmentManager,
|
||||
),
|
||||
@@ -78,13 +80,13 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
|
||||
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
scope.launch {
|
||||
try {
|
||||
addons = components.addonManager.getAddons()
|
||||
addons = components.core.addonManager.getAddons()
|
||||
|
||||
scope.launch(Dispatchers.Main) {
|
||||
adapter = AddonsManagerAdapter(
|
||||
this@AddonsFragment,
|
||||
addons,
|
||||
store = components.store,
|
||||
store = components.core.store,
|
||||
)
|
||||
recyclerView.adapter = adapter
|
||||
}
|
||||
@@ -122,7 +124,7 @@ class AddonsFragment : Fragment(), AddonsManagerAdapterDelegate {
|
||||
private val installAddon: ((Addon) -> Unit) = { addon ->
|
||||
addonProgressOverlay.visibility = View.VISIBLE
|
||||
isInstallationInProgress = true
|
||||
components.addonManager.installAddon(
|
||||
components.core.addonManager.installAddon(
|
||||
url = addon.downloadUrl,
|
||||
onSuccess = {
|
||||
runIfFragmentIsAttached {
|
||||
|
||||
+8
-6
@@ -27,7 +27,9 @@ import mozilla.components.feature.addons.R as MozComp
|
||||
* An activity to show the details of a installed add-on.
|
||||
*/
|
||||
class InstalledAddonDetailsActivity : AppCompatActivity() {
|
||||
private val components: Components by lazy { GlobalComponents.components!! }
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
@@ -46,7 +48,7 @@ class InstalledAddonDetailsActivity : AppCompatActivity() {
|
||||
private fun bindAddon(addon: Addon) {
|
||||
scope.launch {
|
||||
try {
|
||||
val addons = components.addonManager.getAddons()
|
||||
val addons = components.core.addonManager.getAddons()
|
||||
scope.launch(Dispatchers.Main) {
|
||||
addons.find { addon.id == it.id }.let {
|
||||
if (it == null) {
|
||||
@@ -89,7 +91,7 @@ class InstalledAddonDetailsActivity : AppCompatActivity() {
|
||||
switch.setState(addon.isEnabled())
|
||||
switch.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
components.addonManager.enableAddon(
|
||||
components.core.addonManager.enableAddon(
|
||||
addon,
|
||||
onSuccess = {
|
||||
switch.setState(true)
|
||||
@@ -108,7 +110,7 @@ class InstalledAddonDetailsActivity : AppCompatActivity() {
|
||||
},
|
||||
)
|
||||
} else {
|
||||
components.addonManager.disableAddon(
|
||||
components.core.addonManager.disableAddon(
|
||||
addon,
|
||||
onSuccess = {
|
||||
switch.setState(false)
|
||||
@@ -161,7 +163,7 @@ class InstalledAddonDetailsActivity : AppCompatActivity() {
|
||||
val switch = findViewById<SwitchCompat>(R.id.allow_in_private_browsing_switch)
|
||||
switch.isChecked = addon.isAllowedInPrivateBrowsing()
|
||||
switch.setOnCheckedChangeListener { _, isChecked ->
|
||||
components.addonManager.setAddonAllowedInPrivateBrowsing(
|
||||
components.core.addonManager.setAddonAllowedInPrivateBrowsing(
|
||||
addon,
|
||||
isChecked,
|
||||
onSuccess = {
|
||||
@@ -173,7 +175,7 @@ class InstalledAddonDetailsActivity : AppCompatActivity() {
|
||||
|
||||
private fun bindRemoveButton(addon: Addon) {
|
||||
findViewById<View>(R.id.remove_add_on).setOnClickListener {
|
||||
components.addonManager.uninstallAddon(
|
||||
components.core.addonManager.uninstallAddon(
|
||||
addon,
|
||||
onSuccess = {
|
||||
Toast.makeText(
|
||||
|
||||
+10
-6
@@ -25,7 +25,9 @@ import eu.lensai.flutter_mozilla_components.R
|
||||
* An activity to show the pop up action of a web extension.
|
||||
*/
|
||||
class WebExtensionActionPopupActivity : AppCompatActivity() {
|
||||
private val components: Components by lazy { GlobalComponents.components!! }
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private lateinit var webExtensionId: String
|
||||
|
||||
@@ -46,7 +48,7 @@ class WebExtensionActionPopupActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? =
|
||||
when (name) {
|
||||
EngineView::class.java.name -> components.engine.createView(context, attrs).asView()
|
||||
EngineView::class.java.name -> components.core.engine.createView(context, attrs).asView()
|
||||
else -> super.onCreateView(parent, name, context, attrs)
|
||||
}
|
||||
|
||||
@@ -54,7 +56,9 @@ class WebExtensionActionPopupActivity : AppCompatActivity() {
|
||||
* A fragment to show the web extension action popup with [EngineView].
|
||||
*/
|
||||
class WebExtensionActionPopupFragment : Fragment(), EngineSession.Observer {
|
||||
private val components: Components by lazy { GlobalComponents.components!! }
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private var engineSession: EngineSession? = null
|
||||
private lateinit var webExtensionId: String
|
||||
@@ -64,7 +68,7 @@ class WebExtensionActionPopupActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
webExtensionId = requireNotNull(arguments?.getString("web_extension_id"))
|
||||
engineSession = components.store.state.extensions[webExtensionId]?.popupSession
|
||||
engineSession = components.core.store.state.extensions[webExtensionId]?.popupSession
|
||||
|
||||
return inflater.inflate(R.layout.fragment_add_on_settings, container, false)
|
||||
}
|
||||
@@ -77,7 +81,7 @@ class WebExtensionActionPopupActivity : AppCompatActivity() {
|
||||
addonSettingsEngineView.render(session)
|
||||
consumePopupSession()
|
||||
} else {
|
||||
consumeFrom(components.store) { state ->
|
||||
consumeFrom(components.core.store) { state ->
|
||||
state.extensions[webExtensionId]?.let { extState ->
|
||||
extState.popupSession?.let {
|
||||
if (engineSession == null) {
|
||||
@@ -108,7 +112,7 @@ class WebExtensionActionPopupActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun consumePopupSession() {
|
||||
components.store.dispatch(
|
||||
components.core.store.dispatch(
|
||||
WebExtensionAction.UpdatePopupSessionAction(webExtensionId, popupSession = null),
|
||||
)
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package eu.lensai.flutter_mozilla_components.api
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import eu.lensai.flutter_mozilla_components.GlobalComponents
|
||||
import eu.lensai.flutter_mozilla_components.addons.AddonsActivity
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.WebExtensionActionType
|
||||
|
||||
class GeckoAddonsApiImpl(private val context: Context) : GeckoAddonsApi {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override fun startAddonManagerActivity() {
|
||||
val intent = Intent(context, AddonsActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType) {
|
||||
when(actionType) {
|
||||
WebExtensionActionType.BROWSER -> components.features.webExtensionToolbarFeature.invokeAddonBrowserAction(extensionId)
|
||||
WebExtensionActionType.PAGE -> components.features.webExtensionToolbarFeature.invokeAddonPageAction(extensionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-6
@@ -5,17 +5,35 @@ import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
|
||||
import mozilla.components.browser.state.action.SystemAction
|
||||
import mozilla.components.feature.addons.logger
|
||||
|
||||
/**
|
||||
* Implementation of GeckoBrowserApi that handles browser-related operations
|
||||
* @param showFragmentCallback Callback function to show native fragment
|
||||
*/
|
||||
class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Unit) : GeckoBrowserApi {
|
||||
companion object {
|
||||
private const val TAG = "GeckoBrowserApiImpl"
|
||||
}
|
||||
|
||||
override fun showNativeFragment() {
|
||||
showFragmentCallback.invoke();
|
||||
try {
|
||||
showFragmentCallback()
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to show native fragment", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Long) {
|
||||
logger.debug("onTrimMemory: $level")
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
|
||||
val components = GlobalComponents.components!!
|
||||
logger.debug("$TAG: onTrimMemory called with level: $level")
|
||||
|
||||
components.store.dispatch(SystemAction.LowMemoryAction(level.toInt()))
|
||||
components.icons.onTrimMemory(level.toInt())
|
||||
with(GlobalComponents.components!!) {
|
||||
try {
|
||||
core.store.dispatch(SystemAction.LowMemoryAction(level.toInt()))
|
||||
core.icons.onTrimMemory(level.toInt())
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to handle memory trim", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+104
-224
@@ -2,64 +2,96 @@ package eu.lensai.flutter_mozilla_components.api
|
||||
|
||||
import eu.lensai.flutter_mozilla_components.feature.CookieManagerFeature
|
||||
import eu.lensai.flutter_mozilla_components.feature.ResultConsumer
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.Cookie
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.CookiePartitionKey
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.CookieSameSiteStatus
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.*
|
||||
import org.json.JSONObject
|
||||
|
||||
class GeckoCookieApiImpl : GeckoCookieApi {
|
||||
private fun toValueOrNull(json: JSONObject, key: String): Any? {
|
||||
if (!json.has(key)) {
|
||||
throw RuntimeException("Invalid map key")
|
||||
private companion object {
|
||||
const val ERROR_INVALID_KEY = "Invalid map key"
|
||||
}
|
||||
|
||||
private fun JSONObject.putNullable(key: String, value: Any?) {
|
||||
put(key, value ?: JSONObject.NULL)
|
||||
}
|
||||
|
||||
private fun JSONObject.getValueOrNull(key: String): Any? {
|
||||
if (!has(key)) throw RuntimeException(ERROR_INVALID_KEY)
|
||||
return if (!isNull(key)) get(key) else null
|
||||
}
|
||||
|
||||
private fun CookiePartitionKey.toJSON() = JSONObject().apply {
|
||||
put("topLevelSite", topLevelSite)
|
||||
}
|
||||
|
||||
private fun cookiePartitionKeyFromJSON(json: JSONObject) = CookiePartitionKey(
|
||||
topLevelSite = json.getString("topLevelSite")
|
||||
)
|
||||
|
||||
private fun cookieFromJSON(json: JSONObject): Cookie {
|
||||
val partitionKeyJson = json.getValueOrNull("partitionKey") as JSONObject?
|
||||
val partitionKey = partitionKeyJson?.takeUnless { it.length() == 0 }?.let {
|
||||
cookiePartitionKeyFromJSON(it)
|
||||
}
|
||||
|
||||
if (!json.isNull(key)) {
|
||||
return json.get(key)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun cookiePartitionKeyFromJSON(inputJSON: JSONObject): CookiePartitionKey {
|
||||
return CookiePartitionKey(
|
||||
topLevelSite = inputJSON.getString("topLevelSite")
|
||||
)
|
||||
}
|
||||
|
||||
private fun CookiePartitionKey.toJSON(): JSONObject {
|
||||
val json = JSONObject()
|
||||
json.put("topLevelSite", topLevelSite)
|
||||
return json
|
||||
}
|
||||
|
||||
private fun cookieFromJSON(inputJSON: JSONObject): Cookie {
|
||||
val partitionKeyRaw = toValueOrNull(inputJSON, "partitionKey") as JSONObject?
|
||||
val partitionKey = if (partitionKeyRaw == null || partitionKeyRaw.length() == 0) null
|
||||
else cookiePartitionKeyFromJSON(partitionKeyRaw)
|
||||
|
||||
return Cookie (
|
||||
domain = inputJSON.getString("domain"),
|
||||
expirationDate = (toValueOrNull(inputJSON, "expirationDate") as Int?)?.toLong(),
|
||||
firstPartyDomain = inputJSON.getString("firstPartyDomain"),
|
||||
hostOnly = inputJSON.getBoolean("hostOnly"),
|
||||
httpOnly = inputJSON.getBoolean("httpOnly"),
|
||||
name = inputJSON.getString("name"),
|
||||
return Cookie(
|
||||
domain = json.getString("domain"),
|
||||
expirationDate = (json.getValueOrNull("expirationDate") as Int?)?.toLong(),
|
||||
firstPartyDomain = json.getString("firstPartyDomain"),
|
||||
hostOnly = json.getBoolean("hostOnly"),
|
||||
httpOnly = json.getBoolean("httpOnly"),
|
||||
name = json.getString("name"),
|
||||
partitionKey = partitionKey,
|
||||
path = inputJSON.getString("path"),
|
||||
secure = inputJSON.getBoolean("secure"),
|
||||
session = inputJSON.getBoolean("session"),
|
||||
sameSite = when(inputJSON.getString("sameSite")) {
|
||||
"no_restriction" -> CookieSameSiteStatus.NO_RESTRICTION
|
||||
"lax" -> CookieSameSiteStatus.LAX
|
||||
"strict" -> CookieSameSiteStatus.STRICT
|
||||
else -> CookieSameSiteStatus.UNSPECIFIED
|
||||
},
|
||||
storeId = inputJSON.getString("storeId"),
|
||||
value = inputJSON.getString("value")
|
||||
path = json.getString("path"),
|
||||
secure = json.getBoolean("secure"),
|
||||
session = json.getBoolean("session"),
|
||||
sameSite = parseSameSiteStatus(json.getString("sameSite")),
|
||||
storeId = json.getString("storeId"),
|
||||
value = json.getString("value")
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseSameSiteStatus(status: String) = when(status) {
|
||||
"no_restriction" -> CookieSameSiteStatus.NO_RESTRICTION
|
||||
"lax" -> CookieSameSiteStatus.LAX
|
||||
"strict" -> CookieSameSiteStatus.STRICT
|
||||
else -> CookieSameSiteStatus.UNSPECIFIED
|
||||
}
|
||||
|
||||
private fun sameSiteToString(status: CookieSameSiteStatus) = when(status) {
|
||||
CookieSameSiteStatus.NO_RESTRICTION -> "no_restriction"
|
||||
CookieSameSiteStatus.LAX -> "lax"
|
||||
CookieSameSiteStatus.STRICT -> "strict"
|
||||
CookieSameSiteStatus.UNSPECIFIED -> ""
|
||||
}
|
||||
|
||||
private fun createBaseArgs(
|
||||
firstPartyDomain: String?,
|
||||
partitionKey: CookiePartitionKey?,
|
||||
storeId: String?,
|
||||
url: String
|
||||
) = JSONObject().apply {
|
||||
putNullable("firstPartyDomain", firstPartyDomain)
|
||||
putNullable("partitionKey", partitionKey?.toJSON())
|
||||
putNullable("storeId", storeId)
|
||||
put("url", url)
|
||||
}
|
||||
|
||||
private fun handleRequest(
|
||||
action: String,
|
||||
args: JSONObject,
|
||||
callback: (Result<Unit>) -> Unit
|
||||
) {
|
||||
CookieManagerFeature.scheduleRequest(action, args, object : ResultConsumer<JSONObject> {
|
||||
override fun success(result: JSONObject) {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
|
||||
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getCookie(
|
||||
firstPartyDomain: String?,
|
||||
name: String,
|
||||
@@ -68,30 +100,11 @@ class GeckoCookieApiImpl : GeckoCookieApi {
|
||||
url: String,
|
||||
callback: (Result<Cookie>) -> Unit
|
||||
) {
|
||||
val args = JSONObject()
|
||||
|
||||
if (firstPartyDomain == null) {
|
||||
args.put("firstPartyDomain", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("firstPartyDomain", firstPartyDomain)
|
||||
}
|
||||
args.put("name", name)
|
||||
|
||||
if (partitionKey == null) {
|
||||
args.put("partitionKey", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("partitionKey", partitionKey.toJSON())
|
||||
val args = createBaseArgs(firstPartyDomain, partitionKey, storeId, url).apply {
|
||||
put("name", name)
|
||||
}
|
||||
|
||||
if (storeId == null) {
|
||||
args.put("storeId", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("storeId", storeId)
|
||||
}
|
||||
|
||||
args.put("url", url)
|
||||
|
||||
CookieManagerFeature.scheduleRequest("get", args, object: ResultConsumer<JSONObject> {
|
||||
CookieManagerFeature.scheduleRequest("get", args, object : ResultConsumer<JSONObject> {
|
||||
override fun success(result: JSONObject) {
|
||||
callback(Result.success(cookieFromJSON(result.getJSONObject("result"))))
|
||||
}
|
||||
@@ -99,7 +112,6 @@ class GeckoCookieApiImpl : GeckoCookieApi {
|
||||
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -112,53 +124,22 @@ class GeckoCookieApiImpl : GeckoCookieApi {
|
||||
url: String,
|
||||
callback: (Result<List<Cookie>>) -> Unit
|
||||
) {
|
||||
val args = JSONObject()
|
||||
|
||||
if (domain == null) {
|
||||
args.put("domain", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("domain", domain)
|
||||
val args = createBaseArgs(firstPartyDomain, partitionKey, storeId, url).apply {
|
||||
putNullable("domain", domain)
|
||||
putNullable("name", name)
|
||||
}
|
||||
|
||||
if (firstPartyDomain == null) {
|
||||
args.put("firstPartyDomain", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("firstPartyDomain", firstPartyDomain)
|
||||
}
|
||||
|
||||
args.put("name", name)
|
||||
|
||||
if (partitionKey == null) {
|
||||
args.put("partitionKey", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("partitionKey", partitionKey.toJSON())
|
||||
}
|
||||
|
||||
if (storeId == null) {
|
||||
args.put("storeId", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("storeId", storeId)
|
||||
}
|
||||
|
||||
args.put("url", url)
|
||||
|
||||
CookieManagerFeature.scheduleRequest("getAll", args, object: ResultConsumer<JSONObject> {
|
||||
CookieManagerFeature.scheduleRequest("getAll", args, object : ResultConsumer<JSONObject> {
|
||||
override fun success(result: JSONObject) {
|
||||
val jsonArray = result.getJSONArray("result")
|
||||
val cookies: MutableList<Cookie> = mutableListOf()
|
||||
|
||||
repeat(jsonArray.length()) {
|
||||
index ->
|
||||
cookies.add(cookieFromJSON(jsonArray.getJSONObject(index)))
|
||||
val cookies = result.getJSONArray("result").let { jsonArray ->
|
||||
List(jsonArray.length()) { cookieFromJSON(jsonArray.getJSONObject(it)) }
|
||||
}
|
||||
|
||||
callback(Result.success(cookies))
|
||||
}
|
||||
|
||||
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -177,90 +158,18 @@ class GeckoCookieApiImpl : GeckoCookieApi {
|
||||
value: String?,
|
||||
callback: (Result<Unit>) -> Unit
|
||||
) {
|
||||
val args = JSONObject()
|
||||
|
||||
if (domain == null) {
|
||||
args.put("domain", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("domain", domain)
|
||||
val args = createBaseArgs(firstPartyDomain, partitionKey, storeId, url).apply {
|
||||
putNullable("domain", domain)
|
||||
putNullable("expirationDate", expirationDate)
|
||||
putNullable("httpOnly", httpOnly)
|
||||
putNullable("name", name)
|
||||
putNullable("path", path)
|
||||
putNullable("sameSite", sameSite?.let { sameSiteToString(it) })
|
||||
putNullable("secure", secure)
|
||||
putNullable("value", value)
|
||||
}
|
||||
|
||||
if (expirationDate == null) {
|
||||
args.put("expirationDate", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("expirationDate", domain)
|
||||
}
|
||||
|
||||
if (firstPartyDomain == null) {
|
||||
args.put("firstPartyDomain", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("firstPartyDomain", firstPartyDomain)
|
||||
}
|
||||
|
||||
if (httpOnly == null) {
|
||||
args.put("httpOnly", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("httpOnly", httpOnly)
|
||||
}
|
||||
|
||||
if (name == null) {
|
||||
args.put("name", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("name", name)
|
||||
}
|
||||
|
||||
if (partitionKey == null) {
|
||||
args.put("partitionKey", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("partitionKey", partitionKey.toJSON())
|
||||
}
|
||||
|
||||
if (path == null) {
|
||||
args.put("path", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("path", path)
|
||||
}
|
||||
|
||||
if (sameSite == null) {
|
||||
args.put("sameSite", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("sameSite", when(sameSite) {
|
||||
CookieSameSiteStatus.NO_RESTRICTION -> "no_restriction"
|
||||
CookieSameSiteStatus.LAX -> "lax"
|
||||
CookieSameSiteStatus.STRICT -> "strict"
|
||||
CookieSameSiteStatus.UNSPECIFIED -> ""
|
||||
})
|
||||
}
|
||||
|
||||
if (secure == null) {
|
||||
args.put("secure", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("secure", secure)
|
||||
}
|
||||
|
||||
if (storeId == null) {
|
||||
args.put("storeId", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("storeId", storeId)
|
||||
}
|
||||
|
||||
args.put("url", url)
|
||||
|
||||
if (value == null) {
|
||||
args.put("value", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("value", value)
|
||||
}
|
||||
|
||||
CookieManagerFeature.scheduleRequest("set", args, object: ResultConsumer<JSONObject> {
|
||||
override fun success(result: JSONObject) {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
|
||||
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
|
||||
}
|
||||
})
|
||||
handleRequest("set", args, callback)
|
||||
}
|
||||
|
||||
override fun removeCookie(
|
||||
@@ -271,39 +180,10 @@ class GeckoCookieApiImpl : GeckoCookieApi {
|
||||
url: String,
|
||||
callback: (Result<Unit>) -> Unit
|
||||
) {
|
||||
val args = JSONObject()
|
||||
|
||||
if (firstPartyDomain == null) {
|
||||
args.put("firstPartyDomain", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("firstPartyDomain", firstPartyDomain)
|
||||
val args = createBaseArgs(firstPartyDomain, partitionKey, storeId, url).apply {
|
||||
put("name", name)
|
||||
}
|
||||
|
||||
args.put("name", name)
|
||||
|
||||
if (partitionKey == null) {
|
||||
args.put("partitionKey", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("partitionKey", partitionKey.toJSON())
|
||||
}
|
||||
|
||||
if (storeId == null) {
|
||||
args.put("storeId", JSONObject.NULL)
|
||||
} else {
|
||||
args.put("storeId", storeId)
|
||||
}
|
||||
|
||||
args.put("url", url)
|
||||
|
||||
CookieManagerFeature.scheduleRequest("remove", args, object: ResultConsumer<JSONObject> {
|
||||
override fun success(result: JSONObject) {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
|
||||
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
|
||||
}
|
||||
})
|
||||
handleRequest("remove", args, callback)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
+19
-3
@@ -3,11 +3,27 @@ package eu.lensai.flutter_mozilla_components.api
|
||||
import eu.lensai.flutter_mozilla_components.GlobalComponents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.feature.addons.logger
|
||||
|
||||
/**
|
||||
* Implementation of GeckoEngineSettingsApi that manages engine-specific settings
|
||||
*/
|
||||
class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi {
|
||||
private val engine: Engine by lazy { GlobalComponents.components!!.engine }
|
||||
companion object {
|
||||
private const val TAG = "GeckoEngineSettingsApi"
|
||||
}
|
||||
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override fun javaScriptEnabled(state: Boolean) {
|
||||
engine.settings.javascriptEnabled = state
|
||||
try {
|
||||
components.core.engine.settings.javascriptEnabled = state
|
||||
logger.debug("$TAG: JavaScript enabled state changed to: $state")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to set JavaScript enabled state", e)
|
||||
throw IllegalStateException("Failed to set JavaScript enabled state", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -7,17 +7,19 @@ import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
|
||||
class GeckoFindApiImpl : GeckoFindApi {
|
||||
private val state: BrowserState by lazy { GlobalComponents.components!!.store.state }
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private fun sessionByTabId(tabId: String?) : EngineSession? {
|
||||
val loadSessionId = tabId
|
||||
?: state.selectedTabId
|
||||
?: components.core.store.state.selectedTabId
|
||||
|
||||
if (loadSessionId == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val tab = state.findTabOrCustomTab(loadSessionId)
|
||||
val tab = components.core.store.state.findTabOrCustomTab(loadSessionId)
|
||||
return tab?.engineState?.engineSession
|
||||
}
|
||||
|
||||
|
||||
+53
-33
@@ -7,47 +7,63 @@ import kotlinx.coroutines.*
|
||||
import mozilla.components.browser.icons.BrowserIcons
|
||||
import mozilla.components.browser.icons.Icon
|
||||
import mozilla.components.concept.engine.manifest.Size as HtmlSize
|
||||
import mozilla.components.feature.addons.logger
|
||||
|
||||
typealias MozillaIconRequest = mozilla.components.browser.icons.IconRequest
|
||||
typealias MozillaIconSize = mozilla.components.browser.icons.IconRequest.Size
|
||||
typealias MozillaIconResource = mozilla.components.browser.icons.IconRequest.Resource
|
||||
typealias MozillaIconResourceType = mozilla.components.browser.icons.IconRequest.Resource.Type
|
||||
private typealias MozillaIconRequest = mozilla.components.browser.icons.IconRequest
|
||||
private typealias MozillaIconSize = mozilla.components.browser.icons.IconRequest.Size
|
||||
private typealias MozillaIconResource = mozilla.components.browser.icons.IconRequest.Resource
|
||||
private typealias MozillaIconResourceType = mozilla.components.browser.icons.IconRequest.Resource.Type
|
||||
|
||||
class GeckoIconsApiImpl() : GeckoIconsApi {
|
||||
private val icons: BrowserIcons by lazy { GlobalComponents.components!!.icons }
|
||||
/**
|
||||
* Implementation of GeckoIconsApi that handles icon loading and processing
|
||||
*/
|
||||
class GeckoIconsApiImpl : GeckoIconsApi {
|
||||
companion object {
|
||||
private const val TAG = "GeckoIconsApi"
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
}
|
||||
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override fun loadIcon(request: IconRequest, callback: (Result<IconResult>) -> Unit) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
runCatching {
|
||||
loadIconAsync(request.toMozillaIconRequest())
|
||||
}.fold(
|
||||
onSuccess = { result ->
|
||||
withContext(Dispatchers.Main) {
|
||||
callback(Result.success(result))
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
withContext(Dispatchers.Main) {
|
||||
callback(Result.failure(error))
|
||||
}
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val mozillaRequest = request.toMozillaIconRequest()
|
||||
logger.debug("$TAG: Loading icon for URL: ${request.url}")
|
||||
|
||||
val result = loadIconAsync(mozillaRequest)
|
||||
withContext(Dispatchers.Main) {
|
||||
callback(Result.success(result))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to load icon", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadIconAsync(request: MozillaIconRequest): IconResult {
|
||||
val result = icons.loadIcon(request).await()
|
||||
val imageBytes = result.bitmap.toWebPBytes()
|
||||
return IconResult(
|
||||
image = imageBytes,
|
||||
maskable = result.maskable,
|
||||
color = result.color?.toLong(),
|
||||
source = result.source.toApiSource()
|
||||
)
|
||||
return try {
|
||||
val result = components.core.icons.loadIcon(request).await()
|
||||
val imageBytes = result.bitmap.toWebPBytes()
|
||||
IconResult(
|
||||
image = imageBytes,
|
||||
maskable = result.maskable,
|
||||
color = result.color?.toLong(),
|
||||
source = result.source.toApiSource()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Error in loadIconAsync", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun IconRequest.toMozillaIconRequest(): MozillaIconRequest =
|
||||
MozillaIconRequest(
|
||||
private fun IconRequest.toMozillaIconRequest(): MozillaIconRequest {
|
||||
return MozillaIconRequest(
|
||||
url = url,
|
||||
size = size.toMozillaSize(),
|
||||
color = color?.toInt(),
|
||||
@@ -55,6 +71,7 @@ class GeckoIconsApiImpl() : GeckoIconsApi {
|
||||
isPrivate = isPrivate,
|
||||
resources = resources.filterNotNull().map { it.toMozillaResource() }
|
||||
)
|
||||
}
|
||||
|
||||
private fun IconSize.toMozillaSize(): MozillaIconSize = when (this) {
|
||||
IconSize.DEFAULT_SIZE -> MozillaIconSize.DEFAULT
|
||||
@@ -62,14 +79,17 @@ class GeckoIconsApiImpl() : GeckoIconsApi {
|
||||
IconSize.LAUNCHER_ADAPTIVE -> MozillaIconSize.LAUNCHER_ADAPTIVE
|
||||
}
|
||||
|
||||
private fun Resource.toMozillaResource(): MozillaIconResource =
|
||||
MozillaIconResource(
|
||||
private fun Resource.toMozillaResource(): MozillaIconResource {
|
||||
return MozillaIconResource(
|
||||
url = url,
|
||||
mimeType = mimeType,
|
||||
maskable = maskable,
|
||||
type = type.toMozillaType(),
|
||||
sizes = sizes.filterNotNull().map { HtmlSize(it.height.toInt(), it.width.toInt()) }
|
||||
sizes = sizes.filterNotNull().map {
|
||||
HtmlSize(it.height.toInt(), it.width.toInt())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun IconType.toMozillaType(): MozillaIconResourceType = when (this) {
|
||||
IconType.FAVICON -> MozillaIconResourceType.FAVICON
|
||||
|
||||
+29
-3
@@ -3,11 +3,37 @@ package eu.lensai.flutter_mozilla_components.api
|
||||
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.CustomSelectionAction
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionController
|
||||
import mozilla.components.feature.addons.logger
|
||||
|
||||
/**
|
||||
* Implementation of GeckoSelectionActionController that manages custom text selection actions
|
||||
* @property selectionActionDelegate Delegate handling selection actions
|
||||
*/
|
||||
class GeckoSelectionActionControllerImpl(
|
||||
private val selectionActionDelegate: DefaultSelectionActionDelegate
|
||||
) : GeckoSelectionActionController {
|
||||
override fun setActions(actions: List<CustomSelectionAction>) {
|
||||
selectionActionDelegate.actions = actions.associateBy { it.id }
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GeckoSelectionActionController"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the available custom selection actions
|
||||
* @param actions List of custom selection actions to be registered
|
||||
*/
|
||||
override fun setActions(actions: List<CustomSelectionAction>) {
|
||||
try {
|
||||
require(actions.distinctBy { it.id }.size == actions.size) {
|
||||
"Duplicate action IDs found in selection actions"
|
||||
}
|
||||
|
||||
logger.debug("$TAG: Setting ${actions.size} custom selection actions")
|
||||
selectionActionDelegate.actions = actions.associateBy { it.id }.also { actionMap ->
|
||||
logger.debug("$TAG: Registered actions: ${actionMap.keys.joinToString()}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to set selection actions", e)
|
||||
throw IllegalArgumentException("Failed to set selection actions", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+201
-83
@@ -1,36 +1,36 @@
|
||||
package eu.lensai.flutter_mozilla_components.api
|
||||
|
||||
import android.util.Log
|
||||
import eu.lensai.flutter_mozilla_components.GlobalComponents
|
||||
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.LoadUrlFlagsValue
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.*
|
||||
import kotlinx.coroutines.*
|
||||
import mozilla.components.browser.state.action.ContentAction
|
||||
import mozilla.components.browser.state.selector.findTab
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.TranslationOptions as PigeonTranslationOptions
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
||||
import mozilla.components.concept.base.images.ImageLoadRequest
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
import mozilla.components.concept.engine.translate.TranslationOptions
|
||||
import mozilla.components.feature.session.SessionUseCases
|
||||
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||
import mozilla.components.feature.addons.logger
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.TranslationOptions as PigeonTranslationOptions
|
||||
|
||||
/**
|
||||
* Implementation of GeckoSessionApi that manages browser session operations
|
||||
*/
|
||||
class GeckoSessionApiImpl : GeckoSessionApi {
|
||||
private val sessionUseCases: SessionUseCases by lazy { GlobalComponents.components!!.sessionUseCases }
|
||||
private val store: BrowserStore by lazy { GlobalComponents.components!!.store }
|
||||
private val state: BrowserState by lazy { GlobalComponents.components!!.store.state }
|
||||
private val thumbnailStorage: ThumbnailStorage by lazy { GlobalComponents.components!!.thumbnailStorage }
|
||||
private val events: GeckoStateEvents by lazy { GlobalComponents.components!!.flutterEvents }
|
||||
private val engineView: EngineView? by lazy { GlobalComponents.components!!.engineView }
|
||||
companion object {
|
||||
private const val TAG = "GeckoSessionApi"
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
}
|
||||
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private fun getTabId(tabId: String?) = tabId ?: components.core.store.state.selectedTabId
|
||||
?: throw IllegalStateException("No tab ID provided and no selected tab")
|
||||
|
||||
override fun loadUrl(
|
||||
tabId: String?,
|
||||
@@ -38,72 +38,138 @@ class GeckoSessionApiImpl : GeckoSessionApi {
|
||||
flags: LoadUrlFlagsValue,
|
||||
additionalHeaders: Map<String, String>?
|
||||
) {
|
||||
sessionUseCases.loadUrl(
|
||||
url = url,
|
||||
sessionId = tabId,
|
||||
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()),
|
||||
additionalHeaders = additionalHeaders
|
||||
)
|
||||
try {
|
||||
logger.debug("$TAG: Loading URL: $url for tab: $tabId")
|
||||
components.useCases.sessionUseCases.loadUrl(
|
||||
url = url,
|
||||
sessionId = getTabId(tabId),
|
||||
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()),
|
||||
additionalHeaders = additionalHeaders
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to load URL", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadData(tabId: String?, data: String, mimeType: String, encoding: String) {
|
||||
sessionUseCases.loadData(
|
||||
data = data,
|
||||
tabId = tabId ?: state.selectedTabId,
|
||||
mimeType = mimeType,
|
||||
encoding = encoding
|
||||
)
|
||||
try {
|
||||
logger.debug("$TAG: Loading data with mimeType: $mimeType for tab: $tabId")
|
||||
components.useCases.sessionUseCases.loadData(
|
||||
data = data,
|
||||
tabId = getTabId(tabId),
|
||||
mimeType = mimeType,
|
||||
encoding = encoding
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to load data", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun reload(tabId: String?, flags: LoadUrlFlagsValue) {
|
||||
sessionUseCases.reload(
|
||||
tabId = tabId ?: state.selectedTabId,
|
||||
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt())
|
||||
)
|
||||
try {
|
||||
logger.debug("$TAG: Reloading tab: $tabId")
|
||||
components.useCases.sessionUseCases.reload(
|
||||
tabId = getTabId(tabId),
|
||||
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt())
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to reload", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun stopLoading(tabId: String?) {
|
||||
sessionUseCases.stopLoading(tabId = tabId ?: state.selectedTabId)
|
||||
try {
|
||||
logger.debug("$TAG: Stopping loading for tab: $tabId")
|
||||
components.useCases.sessionUseCases.stopLoading(tabId = getTabId(tabId))
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to stop loading", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun goBack(tabId: String?, userInteraction: Boolean) {
|
||||
sessionUseCases.goBack(
|
||||
tabId = tabId ?: state.selectedTabId,
|
||||
userInteraction = userInteraction
|
||||
)
|
||||
try {
|
||||
logger.debug("$TAG: Going back in tab: $tabId")
|
||||
components.useCases.sessionUseCases.goBack(
|
||||
tabId = getTabId(tabId),
|
||||
userInteraction = userInteraction
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to go back", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun goForward(tabId: String?, userInteraction: Boolean) {
|
||||
sessionUseCases.goForward(
|
||||
tabId = tabId ?: state.selectedTabId,
|
||||
userInteraction = userInteraction
|
||||
)
|
||||
try {
|
||||
logger.debug("$TAG: Going forward in tab: $tabId")
|
||||
components.useCases.sessionUseCases.goForward(
|
||||
tabId = getTabId(tabId),
|
||||
userInteraction = userInteraction
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to go forward", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun goToHistoryIndex(index: Long, tabId: String?) {
|
||||
sessionUseCases.goToHistoryIndex(
|
||||
tabId = tabId ?: state.selectedTabId,
|
||||
index = index.toInt()
|
||||
)
|
||||
try {
|
||||
logger.debug("$TAG: Going to history index $index in tab: $tabId")
|
||||
components.useCases.sessionUseCases.goToHistoryIndex(
|
||||
tabId = getTabId(tabId),
|
||||
index = index.toInt()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to go to history index", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestDesktopSite(tabId: String?, enable: Boolean) {
|
||||
sessionUseCases.requestDesktopSite(
|
||||
tabId = tabId ?: state.selectedTabId,
|
||||
enable = enable
|
||||
)
|
||||
try {
|
||||
logger.debug("$TAG: Setting desktop site to $enable for tab: $tabId")
|
||||
components.useCases.sessionUseCases.requestDesktopSite(
|
||||
tabId = getTabId(tabId),
|
||||
enable = enable
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to request desktop site", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun exitFullscreen(tabId: String?) {
|
||||
sessionUseCases.exitFullscreen(tabId = tabId ?: state.selectedTabId)
|
||||
try {
|
||||
logger.debug("$TAG: Exiting fullscreen for tab: $tabId")
|
||||
components.useCases.sessionUseCases.exitFullscreen(tabId = getTabId(tabId))
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to exit fullscreen", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun saveToPdf(tabId: String?) {
|
||||
sessionUseCases.saveToPdf(tabId = tabId ?: state.selectedTabId)
|
||||
try {
|
||||
logger.debug("$TAG: Saving to PDF for tab: $tabId")
|
||||
components.useCases.sessionUseCases.saveToPdf(tabId = getTabId(tabId))
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to save to PDF", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun printContent(tabId: String?) {
|
||||
sessionUseCases.printContent(tabId = tabId ?: state.selectedTabId)
|
||||
try {
|
||||
logger.debug("$TAG: Printing content for tab: $tabId")
|
||||
components.useCases.sessionUseCases.printContent(tabId = getTabId(tabId))
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to print content", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun translate(
|
||||
@@ -112,51 +178,103 @@ class GeckoSessionApiImpl : GeckoSessionApi {
|
||||
toLanguage: String,
|
||||
options: PigeonTranslationOptions?
|
||||
) {
|
||||
sessionUseCases.translate(
|
||||
tabId = tabId ?: state.selectedTabId,
|
||||
fromLanguage = fromLanguage,
|
||||
toLanguage = toLanguage,
|
||||
options = options?.let { TranslationOptions(downloadModel = it.downloadModel) }
|
||||
)
|
||||
try {
|
||||
logger.debug("$TAG: Translating from $fromLanguage to $toLanguage for tab: $tabId")
|
||||
components.useCases.sessionUseCases.translate(
|
||||
tabId = getTabId(tabId),
|
||||
fromLanguage = fromLanguage,
|
||||
toLanguage = toLanguage,
|
||||
options = options?.let { TranslationOptions(downloadModel = it.downloadModel) }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to translate", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun translateRestore(tabId: String?) {
|
||||
sessionUseCases.translateRestore(tabId = tabId ?: state.selectedTabId)
|
||||
try {
|
||||
logger.debug("$TAG: Restoring translation for tab: $tabId")
|
||||
components.useCases.sessionUseCases.translateRestore(tabId = getTabId(tabId))
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to restore translation", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun crashRecovery(tabIds: List<String>?) {
|
||||
if (tabIds != null) {
|
||||
sessionUseCases.crashRecovery.invoke(tabIds = tabIds)
|
||||
} else {
|
||||
sessionUseCases.crashRecovery.invoke()
|
||||
try {
|
||||
logger.debug("$TAG: Performing crash recovery for tabs: $tabIds")
|
||||
if (tabIds != null) {
|
||||
components.useCases.sessionUseCases.crashRecovery.invoke(tabIds = tabIds)
|
||||
} else {
|
||||
components.useCases.sessionUseCases.crashRecovery.invoke()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to perform crash recovery", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun purgeHistory() {
|
||||
sessionUseCases.purgeHistory()
|
||||
try {
|
||||
logger.debug("$TAG: Purging history")
|
||||
components.useCases.sessionUseCases.purgeHistory()
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to purge history", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateLastAccess(tabId: String?, lastAccess: Long?) {
|
||||
sessionUseCases.updateLastAccess(
|
||||
tabId = tabId ?: state.selectedTabId,
|
||||
lastAccess = lastAccess ?: System.currentTimeMillis()
|
||||
)
|
||||
try {
|
||||
val timestamp = lastAccess ?: System.currentTimeMillis()
|
||||
logger.debug("$TAG: Updating last access time to $timestamp for tab: $tabId")
|
||||
components.useCases.sessionUseCases.updateLastAccess(
|
||||
tabId = getTabId(tabId),
|
||||
lastAccess = timestamp
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to update last access", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestScreenshot(callback: (Result<ByteArray?>) -> Unit) {
|
||||
val tab = state.selectedTab
|
||||
if (tab != null) {
|
||||
engineView?.captureThumbnail { bitmap ->
|
||||
if (bitmap != null) {
|
||||
store.dispatch(ContentAction.UpdateThumbnailAction(tab.id, bitmap))
|
||||
val compressed = bitmap.toWebPBytes()
|
||||
callback(Result.success(compressed))
|
||||
} else {
|
||||
callback(Result.success(null))
|
||||
}
|
||||
override fun requestScreenshot(sendBack: Boolean, callback: (Result<ByteArray?>) -> Unit) {
|
||||
try {
|
||||
val tab = components.core.store.state.selectedTab
|
||||
if (tab == null) {
|
||||
logger.warn("$TAG: No selected tab for screenshot")
|
||||
callback(Result.failure(IllegalStateException("No selected tab for screenshot")))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(Exception("No selected tab for screenshot")))
|
||||
|
||||
components.engineView?.captureThumbnail { bitmap ->
|
||||
try {
|
||||
if (bitmap != null) {
|
||||
components.core.store.dispatch(ContentAction.UpdateThumbnailAction(tab.id, bitmap))
|
||||
if (sendBack) {
|
||||
val compressed = bitmap.toWebPBytes()
|
||||
logger.debug("$TAG: Screenshot captured successfully")
|
||||
callback(Result.success(compressed))
|
||||
} else {
|
||||
callback(Result.success(null))
|
||||
}
|
||||
} else {
|
||||
logger.warn("$TAG: Failed to capture screenshot - null bitmap")
|
||||
callback(Result.success(null))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to process screenshot", e)
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
} ?: run {
|
||||
logger.warn("$TAG: No engine view available for screenshot")
|
||||
callback(Result.failure(IllegalStateException("No engine view available")))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to request screenshot", e)
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+337
-241
@@ -37,80 +37,126 @@ import mozilla.components.browser.state.state.recover.RecoverableTab
|
||||
import mozilla.components.browser.state.state.recover.TabState
|
||||
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
||||
import mozilla.components.concept.base.images.ImageLoadRequest
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.storage.HistoryMetadataKey
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import org.json.JSONObject
|
||||
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||
import mozilla.components.feature.addons.logger
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
|
||||
class GeckoTabsApiImpl() : GeckoTabsApi {
|
||||
private val tabsUseCases: TabsUseCases by lazy { GlobalComponents.components!!.tabsUseCases }
|
||||
private val engine: Engine by lazy { GlobalComponents.components!!.engine }
|
||||
private val state: BrowserState by lazy { GlobalComponents.components!!.store.state }
|
||||
private val thumbnailStorage: ThumbnailStorage by lazy { GlobalComponents.components!!.thumbnailStorage }
|
||||
private val icons: BrowserIcons by lazy { GlobalComponents.components!!.icons }
|
||||
private val events: GeckoStateEvents by lazy { GlobalComponents.components!!.flutterEvents }
|
||||
class GeckoTabsApiImpl : GeckoTabsApi {
|
||||
companion object {
|
||||
private const val TAG = "GeckoTabsApi"
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
}
|
||||
|
||||
private fun restoreSource(source: SourceValue ) : SessionState.Source {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
private fun restoreSource(source: SourceValue): SessionState.Source {
|
||||
return SessionState.Source.restore(
|
||||
source.id.toInt(),
|
||||
source.caller?.packageId,
|
||||
source.caller?.category?.value?.toInt()
|
||||
)
|
||||
}
|
||||
private fun mapTab(t: PigeonRecoverableTab) : RecoverableTab {
|
||||
|
||||
private fun mapTab(tab: PigeonRecoverableTab): RecoverableTab {
|
||||
return RecoverableTab(
|
||||
engineSessionState = if(t.engineSessionStateJson != null)
|
||||
engine.createSessionState(JSONObject(t.engineSessionStateJson))
|
||||
else
|
||||
null,
|
||||
engineSessionState = tab.engineSessionStateJson?.let { json ->
|
||||
components.core.engine.createSessionState(JSONObject(json))
|
||||
},
|
||||
state = TabState(
|
||||
id = t.state.id,
|
||||
url = t.state.url,
|
||||
parentId = t.state.parentId,
|
||||
title = t.state.title,
|
||||
searchTerm = t.state.searchTerm,
|
||||
contextId = t.state.contextId,
|
||||
id = tab.state.id,
|
||||
url = tab.state.url,
|
||||
parentId = tab.state.parentId,
|
||||
title = tab.state.title,
|
||||
searchTerm = tab.state.searchTerm,
|
||||
contextId = tab.state.contextId,
|
||||
readerState = ReaderState(
|
||||
readerable = t.state.readerState.readerable,
|
||||
active = t.state.readerState.active,
|
||||
checkRequired = t.state.readerState.checkRequired,
|
||||
connectRequired = t.state.readerState.connectRequired,
|
||||
baseUrl = t.state.readerState.baseUrl,
|
||||
activeUrl = t.state.readerState.activeUrl,
|
||||
scrollY = t.state.readerState.scrollY?.toInt(),
|
||||
readerable = tab.state.readerState.readerable,
|
||||
active = tab.state.readerState.active,
|
||||
checkRequired = tab.state.readerState.checkRequired,
|
||||
connectRequired = tab.state.readerState.connectRequired,
|
||||
baseUrl = tab.state.readerState.baseUrl,
|
||||
activeUrl = tab.state.readerState.activeUrl,
|
||||
scrollY = tab.state.readerState.scrollY?.toInt()
|
||||
),
|
||||
lastAccess = t.state.lastAccess,
|
||||
createdAt = t.state.createdAt,
|
||||
lastAccess = tab.state.lastAccess,
|
||||
createdAt = tab.state.createdAt,
|
||||
lastMediaAccessState = LastMediaAccessState(
|
||||
lastMediaUrl = t.state.lastMediaAccessState.lastMediaUrl,
|
||||
lastMediaAccess = t.state.lastMediaAccessState.lastMediaAccess,
|
||||
mediaSessionActive = t.state.lastMediaAccessState.mediaSessionActive
|
||||
lastMediaUrl = tab.state.lastMediaAccessState.lastMediaUrl,
|
||||
lastMediaAccess = tab.state.lastMediaAccessState.lastMediaAccess,
|
||||
mediaSessionActive = tab.state.lastMediaAccessState.mediaSessionActive
|
||||
),
|
||||
private = t.state.private,
|
||||
historyMetadata = if(t.state.historyMetadata != null)
|
||||
private = tab.state.private,
|
||||
historyMetadata = tab.state.historyMetadata?.let { metadata ->
|
||||
HistoryMetadataKey(
|
||||
url = t.state.historyMetadata.url,
|
||||
searchTerm = t.state.historyMetadata.searchTerm,
|
||||
referrerUrl = t.state.historyMetadata.referrerUrl
|
||||
url = metadata.url,
|
||||
searchTerm = metadata.searchTerm,
|
||||
referrerUrl = metadata.referrerUrl
|
||||
)
|
||||
else null,
|
||||
source = restoreSource(t.state.source),
|
||||
index = t.state.index.toInt(),
|
||||
hasFormData = t.state.hasFormData,
|
||||
},
|
||||
source = restoreSource(tab.state.source),
|
||||
index = tab.state.index.toInt(),
|
||||
hasFormData = tab.state.hasFormData
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun mapRestoreLocation(t: PigeonRestoreLocation) : TabListAction.RestoreAction.RestoreLocation {
|
||||
return when(t) {
|
||||
private fun mapRestoreLocation(location: PigeonRestoreLocation): TabListAction.RestoreAction.RestoreLocation {
|
||||
return when(location) {
|
||||
RestoreLocation.BEGINNING -> TabListAction.RestoreAction.RestoreLocation.BEGINNING
|
||||
RestoreLocation.END -> TabListAction.RestoreAction.RestoreLocation.END
|
||||
RestoreLocation.AT_INDEX -> TabListAction.RestoreAction.RestoreLocation.AT_INDEX
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleIconChange(tab: SessionState) {
|
||||
try {
|
||||
if (tab.content.icon != null) {
|
||||
val iconBytes = tab.content.icon?.toWebPBytes()
|
||||
withContext(Dispatchers.Main) {
|
||||
components.flutterEvents.onIconChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
iconBytes
|
||||
) { }
|
||||
}
|
||||
} else {
|
||||
val result = components.core.icons.loadIcon(IconRequest(url = tab.content.url)).await()
|
||||
val iconBytes = result.bitmap.toWebPBytes()
|
||||
withContext(Dispatchers.Main) {
|
||||
components.flutterEvents.onIconChange(System.currentTimeMillis(), tab.id, iconBytes) { }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to handle icon change for tab ${tab.id}", e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleThumbnailChange(tab: SessionState) {
|
||||
try {
|
||||
val bitmap = components.core.thumbnailStorage.loadThumbnail(
|
||||
ImageLoadRequest(
|
||||
id = tab.id,
|
||||
size = 1024,
|
||||
isPrivate = tab.content.private
|
||||
)
|
||||
).await()
|
||||
|
||||
bitmap?.let {
|
||||
val bytes = it.toWebPBytes()
|
||||
withContext(Dispatchers.Main) {
|
||||
components.flutterEvents.onThumbnailChange(System.currentTimeMillis(), tab.id, bytes) { }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to handle thumbnail change for tab ${tab.id}", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun syncEvents(
|
||||
onSelectedTabChange: Boolean,
|
||||
onTabListChange: Boolean,
|
||||
@@ -122,155 +168,118 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
|
||||
onFindResults: Boolean,
|
||||
onThumbnailChange: Boolean,
|
||||
) {
|
||||
val tabs = state.tabs.map { x -> x.copy() }.toList()
|
||||
val selectedTab = state.selectedTabId
|
||||
try {
|
||||
val tabs = components.core.store.state.tabs.map { it.copy() }
|
||||
val selectedTab = components.core.store.state.selectedTabId
|
||||
|
||||
if(onSelectedTabChange) {
|
||||
events.onSelectedTabChange(
|
||||
System.currentTimeMillis(),
|
||||
selectedTab
|
||||
) { _ -> }
|
||||
}
|
||||
|
||||
if(onTabListChange) {
|
||||
events.onTabListChange(
|
||||
System.currentTimeMillis(),
|
||||
tabs.map {tab -> tab.id}
|
||||
) { _ -> }
|
||||
}
|
||||
|
||||
if(onTabContentStateChange) {
|
||||
tabs.forEach { tab ->
|
||||
events.onTabContentStateChange(
|
||||
System.currentTimeMillis(),
|
||||
TabContentState(
|
||||
id = tab.id,
|
||||
contextId = tab.contextId,
|
||||
url = tab.content.url,
|
||||
title = tab.content.title,
|
||||
progress = tab.content.progress.toLong(),
|
||||
isPrivate = tab.content.private,
|
||||
isFullScreen = tab.content.fullScreen,
|
||||
isLoading = tab.content.loading
|
||||
)
|
||||
) { _ -> }
|
||||
if (onSelectedTabChange) {
|
||||
components.flutterEvents.onSelectedTabChange(System.currentTimeMillis(), selectedTab) { }
|
||||
}
|
||||
|
||||
if (onTabListChange) {
|
||||
components.flutterEvents.onTabListChange(System.currentTimeMillis(), tabs.map { it.id }) { }
|
||||
}
|
||||
}
|
||||
|
||||
if(onIconChange) {
|
||||
tabs.forEach { tab ->
|
||||
if(tab.content.icon != null) {
|
||||
val iconBytes = tab.content.icon?.toWebPBytes()
|
||||
events.onIconChange(
|
||||
if (onTabContentStateChange) {
|
||||
components.flutterEvents.onTabContentStateChange(
|
||||
System.currentTimeMillis(),
|
||||
TabContentState(
|
||||
id = tab.id,
|
||||
contextId = tab.contextId,
|
||||
url = tab.content.url,
|
||||
title = tab.content.title,
|
||||
progress = tab.content.progress.toLong(),
|
||||
isPrivate = tab.content.private,
|
||||
isFullScreen = tab.content.fullScreen,
|
||||
isLoading = tab.content.loading
|
||||
)
|
||||
) { }
|
||||
}
|
||||
|
||||
if (onIconChange) {
|
||||
coroutineScope.launch { handleIconChange(tab) }
|
||||
}
|
||||
|
||||
if (onSecurityInfoStateChange) {
|
||||
components.flutterEvents.onSecurityInfoStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
iconBytes
|
||||
) { _ -> }
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val result = icons.loadIcon(IconRequest(url = tab.content.url)).await()
|
||||
val iconBytes = result.bitmap.toWebPBytes()
|
||||
runOnUiThread {
|
||||
events.onIconChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
iconBytes
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(onSecurityInfoStateChange) {
|
||||
tabs.forEach { tab ->
|
||||
events.onSecurityInfoStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
SecurityInfoState(
|
||||
tab.content.securityInfo.secure,
|
||||
tab.content.securityInfo.host,
|
||||
tab.content.securityInfo.issuer,
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
if(onReaderableStateChange) {
|
||||
tabs.forEach { tab ->
|
||||
events.onReaderableStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
ReaderableState(
|
||||
tab.readerState.readerable,
|
||||
tab.readerState.active,
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
if(onHistoryStateChange) {
|
||||
tabs.forEach { tab ->
|
||||
events.onHistoryStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
HistoryState(
|
||||
items = tab.content.history.items.map { item -> HistoryItem(
|
||||
url = item.uri,
|
||||
title = item.title
|
||||
) },
|
||||
currentIndex = tab.content.history.currentIndex.toLong(),
|
||||
canGoBack = tab.content.canGoBack,
|
||||
canGoForward = tab.content.canGoForward,
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
if(onFindResults) {
|
||||
tabs.forEach { tab ->
|
||||
events.onFindResults(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
tab.content.findResults.map { result -> FindResultState(
|
||||
activeMatchOrdinal = result.activeMatchOrdinal.toLong(),
|
||||
numberOfMatches = result.numberOfMatches.toLong(),
|
||||
isDoneCounting = result.isDoneCounting,
|
||||
) }
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
if(onThumbnailChange) {
|
||||
tabs.forEach { tab ->
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
val bitmap = thumbnailStorage.loadThumbnail(
|
||||
ImageLoadRequest(
|
||||
id = tab.id,
|
||||
//TODO: make this configurable
|
||||
size = 1024,
|
||||
isPrivate = tab.content.private
|
||||
SecurityInfoState(
|
||||
tab.content.securityInfo.secure,
|
||||
tab.content.securityInfo.host,
|
||||
tab.content.securityInfo.issuer
|
||||
)
|
||||
).await()
|
||||
) { }
|
||||
}
|
||||
|
||||
if(bitmap != null) {
|
||||
val bytes = bitmap.toWebPBytes()
|
||||
runOnUiThread {
|
||||
events.onThumbnailChange(System.currentTimeMillis(), tab.id, bytes) { _ -> }
|
||||
if (onReaderableStateChange) {
|
||||
components.flutterEvents.onReaderableStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
ReaderableState(
|
||||
tab.readerState.readerable,
|
||||
tab.readerState.active
|
||||
)
|
||||
) { }
|
||||
}
|
||||
|
||||
if (onHistoryStateChange) {
|
||||
components.flutterEvents.onHistoryStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
HistoryState(
|
||||
items = tab.content.history.items.map { item ->
|
||||
HistoryItem(url = item.uri, title = item.title)
|
||||
},
|
||||
currentIndex = tab.content.history.currentIndex.toLong(),
|
||||
canGoBack = tab.content.canGoBack,
|
||||
canGoForward = tab.content.canGoForward
|
||||
)
|
||||
) { }
|
||||
}
|
||||
|
||||
if (onFindResults) {
|
||||
components.flutterEvents.onFindResults(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
tab.content.findResults.map { result ->
|
||||
FindResultState(
|
||||
activeMatchOrdinal = result.activeMatchOrdinal.toLong(),
|
||||
numberOfMatches = result.numberOfMatches.toLong(),
|
||||
isDoneCounting = result.isDoneCounting
|
||||
)
|
||||
}
|
||||
}
|
||||
) { }
|
||||
}
|
||||
|
||||
if (onThumbnailChange) {
|
||||
coroutineScope.launch { handleThumbnailChange(tab) }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to sync events", e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectTab(tabId: String) {
|
||||
tabsUseCases.selectTab(tabId = tabId)
|
||||
try {
|
||||
components.useCases.tabsUseCases.selectTab(tabId = tabId)
|
||||
logger.debug("$TAG: Selected tab $tabId")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to select tab $tabId", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeTab(tabId: String) {
|
||||
tabsUseCases.removeTab(tabId = tabId)
|
||||
try {
|
||||
components.useCases.tabsUseCases.removeTab(tabId = tabId)
|
||||
logger.debug("$TAG: Removed tab $tabId")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to remove tab $tabId", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun addTab(
|
||||
@@ -285,44 +294,81 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
|
||||
historyMetadata: PigeonHistoryMetadataKey?,
|
||||
additionalHeaders: Map<String, String>?
|
||||
): String {
|
||||
return tabsUseCases.addTab(
|
||||
url = url,
|
||||
selectTab = selectTab,
|
||||
startLoading = startLoading,
|
||||
parentId = parentId,
|
||||
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()),
|
||||
contextId = contextId,
|
||||
source = restoreSource(source),
|
||||
private = private,
|
||||
historyMetadata = if(historyMetadata != null)
|
||||
HistoryMetadataKey(
|
||||
url = historyMetadata.url,
|
||||
searchTerm = historyMetadata.searchTerm,
|
||||
referrerUrl = historyMetadata.referrerUrl
|
||||
)
|
||||
else null,
|
||||
additionalHeaders = additionalHeaders
|
||||
)
|
||||
try {
|
||||
return components.useCases.tabsUseCases.addTab(
|
||||
url = url,
|
||||
selectTab = selectTab,
|
||||
startLoading = startLoading,
|
||||
parentId = parentId,
|
||||
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()),
|
||||
contextId = contextId,
|
||||
source = restoreSource(source),
|
||||
private = private,
|
||||
historyMetadata = historyMetadata?.let { metadata ->
|
||||
HistoryMetadataKey(
|
||||
url = metadata.url,
|
||||
searchTerm = metadata.searchTerm,
|
||||
referrerUrl = metadata.referrerUrl
|
||||
)
|
||||
},
|
||||
additionalHeaders = additionalHeaders
|
||||
).also {
|
||||
logger.debug("$TAG: Added new tab with ID $it")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to add tab", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeAllTabs(recoverable: Boolean) {
|
||||
tabsUseCases.removeAllTabs(recoverable = recoverable)
|
||||
try {
|
||||
components.useCases.tabsUseCases.removeAllTabs(recoverable = recoverable)
|
||||
logger.debug("$TAG: Removed all tabs, recoverable: $recoverable")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to remove all tabs", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeTabs(ids: List<String>) {
|
||||
tabsUseCases.removeTabs(ids = ids)
|
||||
try {
|
||||
components.useCases.tabsUseCases.removeTabs(ids = ids)
|
||||
logger.debug("$TAG: Removed tabs: ${ids.joinToString()}")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to remove tabs", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeNormalTabs() {
|
||||
tabsUseCases.removeNormalTabs()
|
||||
try {
|
||||
components.useCases.tabsUseCases.removeNormalTabs()
|
||||
logger.debug("$TAG: Removed all normal tabs")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to remove normal tabs", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun removePrivateTabs() {
|
||||
tabsUseCases.removePrivateTabs()
|
||||
try {
|
||||
components.useCases.tabsUseCases.removePrivateTabs()
|
||||
logger.debug("$TAG: Removed all private tabs")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to remove private tabs", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun undo() {
|
||||
tabsUseCases.undo()
|
||||
try {
|
||||
components.useCases.tabsUseCases.undo()
|
||||
logger.debug("$TAG: Performed undo operation")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to perform undo", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun restoreTabsByList(
|
||||
@@ -330,35 +376,57 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
|
||||
selectTabId: String?,
|
||||
restoreLocation: PigeonRestoreLocation
|
||||
) {
|
||||
tabsUseCases.restore(
|
||||
tabs = tabs.map { t -> mapTab(t) },
|
||||
restoreLocation = mapRestoreLocation(restoreLocation),
|
||||
selectTabId = selectTabId,
|
||||
)
|
||||
try {
|
||||
components.useCases.tabsUseCases.restore(
|
||||
tabs = tabs.map { mapTab(it) },
|
||||
restoreLocation = mapRestoreLocation(restoreLocation),
|
||||
selectTabId = selectTabId
|
||||
)
|
||||
logger.debug("$TAG: Restored ${tabs.size} tabs")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to restore tabs", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun restoreTabsByBrowserState(
|
||||
state: PigeonRecoverableBrowserState,
|
||||
restoreLocation: PigeonRestoreLocation
|
||||
) {
|
||||
tabsUseCases.restore(
|
||||
state = RecoverableBrowserState(
|
||||
selectedTabId = state.selectedTabId,
|
||||
tabs = state.tabs.filterNotNull().map { t -> mapTab(t) },
|
||||
),
|
||||
restoreLocation = mapRestoreLocation(restoreLocation),
|
||||
)
|
||||
try {
|
||||
components.useCases.tabsUseCases.restore(
|
||||
state = RecoverableBrowserState(
|
||||
selectedTabId = state.selectedTabId,
|
||||
tabs = state.tabs.filterNotNull().map { mapTab(it) }
|
||||
),
|
||||
restoreLocation = mapRestoreLocation(restoreLocation)
|
||||
)
|
||||
logger.debug("$TAG: Restored browser state")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to restore browser state", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectOrAddTabByHistory(url: String, historyMetadata: PigeonHistoryMetadataKey): String {
|
||||
return tabsUseCases.selectOrAddTab(
|
||||
url = url,
|
||||
historyMetadata = HistoryMetadataKey(
|
||||
url = historyMetadata.url,
|
||||
searchTerm = historyMetadata.searchTerm,
|
||||
referrerUrl = historyMetadata.referrerUrl
|
||||
),
|
||||
)
|
||||
override fun selectOrAddTabByHistory(
|
||||
url: String,
|
||||
historyMetadata: PigeonHistoryMetadataKey
|
||||
): String {
|
||||
try {
|
||||
return components.useCases.tabsUseCases.selectOrAddTab(
|
||||
url = url,
|
||||
historyMetadata = HistoryMetadataKey(
|
||||
url = historyMetadata.url,
|
||||
searchTerm = historyMetadata.searchTerm,
|
||||
referrerUrl = historyMetadata.referrerUrl
|
||||
)
|
||||
).also {
|
||||
logger.debug("$TAG: Selected or added tab by history with ID $it")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to select or add tab by history", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectOrAddTabByUrl(
|
||||
@@ -368,36 +436,64 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
|
||||
flags: LoadUrlFlagsValue,
|
||||
ignoreFragment: Boolean
|
||||
): String {
|
||||
return tabsUseCases.selectOrAddTab(
|
||||
url = url,
|
||||
private = private,
|
||||
source = restoreSource(source),
|
||||
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()),
|
||||
ignoreFragment = ignoreFragment,
|
||||
)
|
||||
try {
|
||||
return components.useCases.tabsUseCases.selectOrAddTab(
|
||||
url = url,
|
||||
private = private,
|
||||
source = restoreSource(source),
|
||||
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()),
|
||||
ignoreFragment = ignoreFragment
|
||||
).also {
|
||||
logger.debug("$TAG: Selected or added tab by URL with ID $it")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to select or add tab by URL", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun duplicateTab(selectTabId: String?, selectNewTab: Boolean): String {
|
||||
val tabState = if(selectTabId != null) state.findTab(selectTabId) else null
|
||||
try {
|
||||
val tabState = selectTabId?.let { components.core.store.state.findTab(it) }
|
||||
?: throw IllegalArgumentException("Tab not found")
|
||||
|
||||
return tabsUseCases.duplicateTab(
|
||||
tab = tabState!!,
|
||||
selectNewTab = selectNewTab,
|
||||
)
|
||||
return components.useCases.tabsUseCases.duplicateTab(
|
||||
tab = tabState,
|
||||
selectNewTab = selectNewTab
|
||||
).also {
|
||||
logger.debug("$TAG: Duplicated tab $selectTabId to new tab $it")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to duplicate tab", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun moveTabs(tabIds: List<String>, targetTabId: String, placeAfter: Boolean) {
|
||||
tabsUseCases.moveTabs(
|
||||
tabIds = tabIds,
|
||||
targetTabId = targetTabId,
|
||||
placeAfter = placeAfter,
|
||||
)
|
||||
try {
|
||||
components.useCases.tabsUseCases.moveTabs(
|
||||
tabIds = tabIds,
|
||||
targetTabId = targetTabId,
|
||||
placeAfter = placeAfter
|
||||
)
|
||||
logger.debug("$TAG: Moved tabs ${tabIds.joinToString()} to $targetTabId")
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to move tabs", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
override fun migratePrivateTabUseCase(tabId: String, alternativeUrl: String?): String {
|
||||
return tabsUseCases.migratePrivateTabUseCase(
|
||||
tabId = tabId,
|
||||
alternativeUrl = alternativeUrl
|
||||
)
|
||||
try {
|
||||
return components.useCases.tabsUseCases.migratePrivateTabUseCase(
|
||||
tabId = tabId,
|
||||
alternativeUrl = alternativeUrl
|
||||
).also {
|
||||
logger.debug("$TAG: Migrated private tab $tabId")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$TAG: Failed to migrate private tab", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package eu.lensai.flutter_mozilla_components.components
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.lensai.flutter_mozilla_components.Components
|
||||
import eu.lensai.flutter_mozilla_components.interceptor.AppRequestInterceptor
|
||||
import eu.lensai.flutter_mozilla_components.services.DownloadService
|
||||
import eu.lensai.flutter_mozilla_components.EngineProvider
|
||||
import eu.lensai.flutter_mozilla_components.services.MediaSessionService
|
||||
import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
|
||||
import eu.lensai.flutter_mozilla_components.R
|
||||
import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey
|
||||
import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage
|
||||
import mozilla.components.browser.icons.BrowserIcons
|
||||
import mozilla.components.browser.session.storage.SessionStorage
|
||||
import mozilla.components.browser.state.engine.EngineMiddleware
|
||||
import mozilla.components.browser.state.engine.middleware.SessionPrioritizationMiddleware
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
|
||||
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
||||
import mozilla.components.concept.engine.DefaultSettings
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
|
||||
import mozilla.components.concept.fetch.Client
|
||||
import mozilla.components.feature.addons.AddonManager
|
||||
import mozilla.components.feature.addons.amo.AMOAddonsProvider
|
||||
import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
|
||||
import mozilla.components.feature.addons.update.DefaultAddonUpdater
|
||||
import mozilla.components.feature.downloads.DownloadMiddleware
|
||||
import mozilla.components.feature.media.MediaSessionFeature
|
||||
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
|
||||
import mozilla.components.feature.prompts.file.FileUploadsDirCleaner
|
||||
import mozilla.components.feature.readerview.ReaderViewMiddleware
|
||||
import mozilla.components.feature.session.middleware.LastAccessMiddleware
|
||||
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
|
||||
import mozilla.components.feature.webnotifications.WebNotificationFeature
|
||||
import mozilla.components.support.base.worker.Frequency
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val DAY_IN_MINUTES = 24 * 60L
|
||||
|
||||
class Core(private val context: Context,
|
||||
private val components: Components,
|
||||
private val flutterEvents: GeckoStateEvents,
|
||||
) {
|
||||
val prefs by lazy {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
|
||||
val engineSettings by lazy {
|
||||
DefaultSettings().apply {
|
||||
//historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
|
||||
requestInterceptor = AppRequestInterceptor(context)
|
||||
remoteDebuggingEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_remote_debugging), false)
|
||||
testingModeEnabled = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_testing_mode), false)
|
||||
trackingProtectionPolicy = createTrackingProtectionPolicy(prefs)
|
||||
httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED
|
||||
globalPrivacyControlEnabled = prefs.getBoolean(
|
||||
context.getPreferenceKey(R.string.pref_key_global_privacy_control),
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val engine: Engine by lazy {
|
||||
EngineProvider.createEngine(context, engineSettings)
|
||||
}
|
||||
|
||||
/**
|
||||
* The [Client] implementation (`concept-fetch`) used for HTTP requests.
|
||||
*/
|
||||
val client: Client by lazy {
|
||||
EngineProvider.createClient(context)
|
||||
}
|
||||
|
||||
val thumbnailStorage by lazy { ThumbnailStorage(context) }
|
||||
|
||||
val icons by lazy { BrowserIcons(context, client) }
|
||||
|
||||
/**
|
||||
* A storage component for site permissions.
|
||||
*/
|
||||
val geckoSitePermissionsStorage by lazy {
|
||||
val geckoRuntime = EngineProvider.getOrCreateRuntime(context)
|
||||
GeckoSitePermissionsStorage(geckoRuntime, OnDiskSitePermissionsStorage(context))
|
||||
}
|
||||
|
||||
// Addons
|
||||
val addonManager by lazy {
|
||||
AddonManager(store, engine, addonsProvider, addonUpdater)
|
||||
}
|
||||
|
||||
val addonUpdater by lazy {
|
||||
DefaultAddonUpdater(
|
||||
context,
|
||||
Frequency(1, TimeUnit.DAYS),
|
||||
components.notificationsDelegate
|
||||
)
|
||||
}
|
||||
|
||||
val addonsProvider by lazy {
|
||||
AMOAddonsProvider(
|
||||
context,
|
||||
client,
|
||||
collectionName = "7dfae8669acc4312a65e8ba5553036",
|
||||
maxCacheAgeInMinutes = DAY_IN_MINUTES,
|
||||
)
|
||||
}
|
||||
|
||||
val supportedAddonsChecker by lazy {
|
||||
DefaultSupportedAddonsChecker(context, Frequency(1, TimeUnit.DAYS))
|
||||
}
|
||||
|
||||
val fileUploadsDirCleaner: FileUploadsDirCleaner by lazy {
|
||||
FileUploadsDirCleaner { context.cacheDir }
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
val store by lazy {
|
||||
BrowserStore(
|
||||
middleware = listOf(
|
||||
FlutterEventMiddleware(flutterEvents),
|
||||
DownloadMiddleware(context, DownloadService::class.java),
|
||||
ThumbnailsMiddleware(thumbnailStorage),
|
||||
ReaderViewMiddleware(),
|
||||
// UndoMiddleware(),
|
||||
LastAccessMiddleware(),
|
||||
// PromptMiddleware(),
|
||||
SessionPrioritizationMiddleware(),
|
||||
RecordingDevicesMiddleware(context, components.notificationsDelegate),
|
||||
) + EngineMiddleware.create(engine),
|
||||
).apply {
|
||||
components.events.registerFlowEvents(this)
|
||||
|
||||
icons.install(engine, this)
|
||||
|
||||
WebNotificationFeature(
|
||||
context,
|
||||
engine,
|
||||
icons,
|
||||
R.drawable.ic_launcher_foreground,
|
||||
geckoSitePermissionsStorage,
|
||||
NotificationActivity::class.java,
|
||||
notificationsDelegate = components.notificationsDelegate,
|
||||
)
|
||||
|
||||
MediaSessionFeature(context, MediaSessionService::class.java, this).start()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The storage component for persisting browser tab sessions.
|
||||
*/
|
||||
val sessionStorage: SessionStorage by lazy {
|
||||
SessionStorage(context, engine)
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a [TrackingProtectionPolicy] based on current preferences.
|
||||
*
|
||||
* @param prefs the shared preferences to use when reading tracking
|
||||
* protection settings.
|
||||
* @param normalMode whether or not tracking protection should be enabled
|
||||
* in normal browsing mode, defaults to the current preference value.
|
||||
* @param privateMode whether or not tracking protection should be enabled
|
||||
* in private browsing mode, default to the current preference value.
|
||||
* @return the constructed tracking protection policy based on preferences.
|
||||
*/
|
||||
private fun createTrackingProtectionPolicy(
|
||||
prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context),
|
||||
normalMode: Boolean = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_tracking_protection_normal), true),
|
||||
privateMode: Boolean = prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_tracking_protection_private), true),
|
||||
): TrackingProtectionPolicy {
|
||||
val trackingPolicy = TrackingProtectionPolicy.recommended()
|
||||
return when {
|
||||
normalMode && privateMode -> trackingPolicy
|
||||
normalMode && !privateMode -> trackingPolicy.forRegularSessionsOnly()
|
||||
!normalMode && privateMode -> trackingPolicy.forPrivateSessionsOnly()
|
||||
else -> TrackingProtectionPolicy.none()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+193
@@ -0,0 +1,193 @@
|
||||
package eu.lensai.flutter_mozilla_components.components
|
||||
|
||||
import eu.lensai.flutter_mozilla_components.api.ReaderViewEventsImpl
|
||||
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.FindResultState
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.HistoryItem
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.HistoryState
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.ReaderableState
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.TabContentState
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.feature.addons.logger
|
||||
import mozilla.components.lib.state.Store
|
||||
import mozilla.components.lib.state.ext.flowScoped
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
|
||||
|
||||
class Events(
|
||||
private val flutterEvents: GeckoStateEvents,
|
||||
) {
|
||||
val readerViewEvents by lazy { ReaderViewEventsImpl() }
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
fun registerFlowEvents(stateFlow: Store<BrowserState, BrowserAction>) {
|
||||
stateFlow.flowScoped { flow ->
|
||||
flow.map { state -> state.selectedTabId }
|
||||
.distinctUntilChanged()
|
||||
.collect { tabId ->
|
||||
flutterEvents.onSelectedTabChange(
|
||||
System.currentTimeMillis(),
|
||||
tabId
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
stateFlow.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.content
|
||||
}
|
||||
.ifAnyChanged { arrayOf (it.content.icon) }
|
||||
.debounce { 50 }
|
||||
.collect { tab ->
|
||||
val iconBytes = tab.content.icon?.toWebPBytes()
|
||||
flutterEvents.onIconChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
iconBytes
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
stateFlow.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.content.securityInfo
|
||||
}
|
||||
.debounce { 50 }
|
||||
.collect { tab ->
|
||||
flutterEvents.onSecurityInfoStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
SecurityInfoState(
|
||||
tab.content.securityInfo.secure,
|
||||
tab.content.securityInfo.host,
|
||||
tab.content.securityInfo.issuer,
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
stateFlow.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.readerState
|
||||
}
|
||||
.ifAnyChanged { arrayOf(
|
||||
it.readerState.readerable,
|
||||
it.readerState.active,
|
||||
)
|
||||
}
|
||||
.debounce { 50 }
|
||||
.collect { tab ->
|
||||
flutterEvents.onReaderableStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
ReaderableState(
|
||||
tab.readerState.readerable,
|
||||
tab.readerState.active,
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
stateFlow.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.content
|
||||
}
|
||||
.ifAnyChanged { arrayOf(
|
||||
it.content.history,
|
||||
it.content.canGoBack,
|
||||
it.content.canGoForward,
|
||||
)
|
||||
}
|
||||
.debounce { 50 }
|
||||
.collect { tab ->
|
||||
flutterEvents.onHistoryStateChange(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
HistoryState(
|
||||
items = tab.content.history.items.map { item -> HistoryItem(
|
||||
url = item.uri,
|
||||
title = item.title
|
||||
) },
|
||||
currentIndex = tab.content.history.currentIndex.toLong(),
|
||||
canGoBack = tab.content.canGoBack,
|
||||
canGoForward = tab.content.canGoForward,
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
stateFlow.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} }
|
||||
.distinctUntilChanged()
|
||||
.collect { tabs ->
|
||||
flutterEvents.onTabListChange(System.currentTimeMillis(), tabs) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
stateFlow.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.content
|
||||
}
|
||||
.ifAnyChanged { arrayOf(
|
||||
it.content.url,
|
||||
it.content.title,
|
||||
it.content.private,
|
||||
it.content.fullScreen,
|
||||
it.content.progress,
|
||||
it.content.loading)
|
||||
}
|
||||
.debounce { 50 }
|
||||
.collect { tab ->
|
||||
logger.info("title: ${tab.content.title} ${tab.content.url}")
|
||||
flutterEvents.onTabContentStateChange(
|
||||
System.currentTimeMillis(),
|
||||
TabContentState(
|
||||
id = tab.id,
|
||||
contextId = tab.contextId,
|
||||
url = tab.content.url,
|
||||
title = tab.content.title,
|
||||
progress = tab.content.progress.toLong(),
|
||||
isPrivate = tab.content.private,
|
||||
isFullScreen = tab.content.fullScreen,
|
||||
isLoading = tab.content.loading
|
||||
)
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
stateFlow.flowScoped { flow ->
|
||||
flow.mapNotNull { state -> state.tabs }
|
||||
.filterChanged {
|
||||
it.content.findResults
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collect { tab ->
|
||||
tab.content.findResults
|
||||
flutterEvents.onFindResults(
|
||||
System.currentTimeMillis(),
|
||||
tab.id,
|
||||
tab.content.findResults.map { result -> FindResultState(
|
||||
activeMatchOrdinal = result.activeMatchOrdinal.toLong(),
|
||||
numberOfMatches = result.numberOfMatches.toLong(),
|
||||
isDoneCounting = result.isDoneCounting,
|
||||
) }
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package eu.lensai.flutter_mozilla_components.components
|
||||
|
||||
import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
|
||||
class Features(
|
||||
private val store: BrowserStore,
|
||||
private val addonEvents: GeckoAddonEvents
|
||||
) {
|
||||
val webExtensionToolbarFeature by lazy {
|
||||
WebExtensionToolbarFeature(
|
||||
store,
|
||||
addonEvents
|
||||
)
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/* 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.lensai.flutter_mozilla_components.components
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.lensai.flutter_mozilla_components.R
|
||||
import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey
|
||||
import mozilla.components.feature.app.links.AppLinksInterceptor
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
|
||||
/**
|
||||
* Component group which encapsulates foreground-friendly services.
|
||||
*/
|
||||
class Services(
|
||||
private val context: Context,
|
||||
private val tabsUseCases: TabsUseCases,
|
||||
) {
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
val appLinksInterceptor by lazy {
|
||||
AppLinksInterceptor(
|
||||
context,
|
||||
interceptLinkClicks = true,
|
||||
launchInApp = {
|
||||
prefs.getBoolean(context.getPreferenceKey(R.string.pref_key_launch_external_app), false)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/* 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.lensai.flutter_mozilla_components.components
|
||||
|
||||
import android.content.Context
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.feature.app.links.AppLinksUseCases
|
||||
import mozilla.components.feature.downloads.DownloadsUseCases
|
||||
import mozilla.components.feature.session.SessionUseCases
|
||||
import mozilla.components.feature.session.SettingsUseCases
|
||||
import mozilla.components.feature.tabs.CustomTabsUseCases
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
|
||||
/**
|
||||
* Component group for all use cases. Use cases are provided by feature
|
||||
* modules and can be triggered by UI interactions.
|
||||
*/
|
||||
class UseCases(
|
||||
private val context: Context,
|
||||
private val engine: Engine,
|
||||
private val store: BrowserStore,
|
||||
) {
|
||||
/**
|
||||
* Use cases that provide engine interactions for a given browser session.
|
||||
*/
|
||||
val sessionUseCases by lazy { SessionUseCases(store) }
|
||||
|
||||
/**
|
||||
* Use cases that provide tab management.
|
||||
*/
|
||||
val tabsUseCases: TabsUseCases by lazy { TabsUseCases(store) }
|
||||
|
||||
/**
|
||||
* Use cases that provide settings management.
|
||||
*/
|
||||
val settingsUseCases by lazy { SettingsUseCases(engine, store) }
|
||||
|
||||
/**
|
||||
* Use cases related to the downloads feature.
|
||||
*/
|
||||
val downloadsUseCases: DownloadsUseCases by lazy { DownloadsUseCases(store) }
|
||||
|
||||
/**
|
||||
* Use cases related to Custom Tabs.
|
||||
*/
|
||||
val customTabsUseCases: CustomTabsUseCases by lazy { CustomTabsUseCases(store, sessionUseCases.loadUrl) }
|
||||
|
||||
val appLinksUseCases by lazy { AppLinksUseCases(context) }
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package eu.lensai.flutter_mozilla_components.ext
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
fun Context.getPreferenceKey(@StringRes resourceId: Int): String =
|
||||
resources.getString(resourceId)
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
/* 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.lensai.flutter_mozilla_components.feature
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.WebExtensionActionType
|
||||
import eu.lensai.flutter_mozilla_components.pigeons.WebExtensionData
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.android.asCoroutineDispatcher
|
||||
import mozilla.components.browser.state.selector.selectedTab
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.SessionState
|
||||
import mozilla.components.browser.state.state.WebExtensionState
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.webextension.Action
|
||||
import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction
|
||||
import mozilla.components.lib.state.ext.flowScoped
|
||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||
import mozilla.components.support.base.log.Log
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
|
||||
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||
|
||||
/**
|
||||
* Web extension toolbar implementation that updates the toolbar whenever the state of web
|
||||
* extensions changes.
|
||||
*/
|
||||
class WebExtensionToolbarFeature(
|
||||
private var store: BrowserStore,
|
||||
private var addonEvents: GeckoAddonEvents,
|
||||
) : LifecycleAwareFeature {
|
||||
// This maps web extension ids to [WebExtensionToolbarAction]s for efficient
|
||||
// updates of global and tab-specific browser/page actions within the same
|
||||
// lifecycle.
|
||||
@VisibleForTesting
|
||||
internal val webExtensionBrowserActions = HashMap<String, WebExtensionBrowserAction>()
|
||||
internal val webExtensionPageActions = HashMap<String, WebExtensionBrowserAction>()
|
||||
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
internal val iconThread = HandlerThread("IconThread")
|
||||
internal val iconHandler by lazy {
|
||||
iconThread.start()
|
||||
Handler(iconThread.looper)
|
||||
}
|
||||
|
||||
internal var iconJobDispatcher: CoroutineDispatcher = Dispatchers.Main
|
||||
|
||||
init {
|
||||
renderWebExtensionActions(store.state)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts observing for the state of web extensions changes
|
||||
*/
|
||||
override fun start() {
|
||||
// The feature could start with an existing view and toolbar so
|
||||
// we have to check if any stale actions (from uninstalled or
|
||||
// disabled extensions) are being displayed and remove them.
|
||||
webExtensionBrowserActions
|
||||
.filterKeys { !store.state.extensions.containsKey(it) || store.state.extensions[it]?.enabled == false }
|
||||
.forEach { (extensionId, action) ->
|
||||
// toolbar.removeBrowserAction(action)
|
||||
// toolbar.invalidateActions()
|
||||
webExtensionBrowserActions.remove(extensionId)
|
||||
}
|
||||
|
||||
webExtensionPageActions
|
||||
.filterKeys { !store.state.extensions.containsKey(it) || store.state.extensions[it]?.enabled == false }
|
||||
.forEach { (extensionId, action) ->
|
||||
// toolbar.removePageAction(action)
|
||||
// toolbar.invalidateActions()
|
||||
webExtensionPageActions.remove(extensionId)
|
||||
}
|
||||
|
||||
iconJobDispatcher = iconHandler.asCoroutineDispatcher("WebExtensionIconDispatcher")
|
||||
scope = store.flowScoped { flow ->
|
||||
flow.ifAnyChanged { arrayOf(it.selectedTab, it.extensions) }
|
||||
.collect { state ->
|
||||
renderWebExtensionActions(state, state.selectedTab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
iconJobDispatcher.cancel()
|
||||
scope?.cancel()
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||
internal fun renderWebExtensionActions(state: BrowserState, tab: SessionState? = null) {
|
||||
val extensions = state.extensions.values.toList()
|
||||
extensions.filter { it.enabled }.sortedBy { it.name }.forEach { extension ->
|
||||
if (extensionNotAllowedInTab(extension, tab)) {
|
||||
webExtensionPageActions[extension.id]?.let {
|
||||
addonEvents.onRemoveWebExtensionAction(
|
||||
timestampArg = System.currentTimeMillis(),
|
||||
extensionIdArg = extension.id,
|
||||
actionTypeArg = WebExtensionActionType.PAGE,
|
||||
) {}
|
||||
webExtensionPageActions.remove(extension.id)
|
||||
}
|
||||
webExtensionBrowserActions[extension.id]?.let {
|
||||
addonEvents.onRemoveWebExtensionAction(
|
||||
timestampArg = System.currentTimeMillis(),
|
||||
extensionIdArg = extension.id,
|
||||
actionTypeArg = WebExtensionActionType.BROWSER,
|
||||
) {}
|
||||
webExtensionBrowserActions.remove(extension.id)
|
||||
}
|
||||
return@forEach
|
||||
}
|
||||
|
||||
extension.browserAction?.let { browserAction ->
|
||||
addOrUpdateAction(
|
||||
extension = extension,
|
||||
defaultAction = browserAction,
|
||||
tabAction = tab?.extensionState?.get(extension.id)?.browserAction,
|
||||
)
|
||||
}
|
||||
|
||||
extension.pageAction?.let { pageAction ->
|
||||
val tabPageAction = tab?.extensionState?.get(extension.id)?.pageAction
|
||||
|
||||
// Unlike browser actions, page actions are not displayed by default (only if enabled):
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action
|
||||
if (pageAction.copyWithOverride(tabPageAction).enabled == true) {
|
||||
addOrUpdateAction(
|
||||
extension = extension,
|
||||
defaultAction = pageAction,
|
||||
tabAction = tabPageAction,
|
||||
isPageAction = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extensionNotAllowedInTab(
|
||||
extension: WebExtensionState?,
|
||||
tab: SessionState?,
|
||||
): Boolean = extension?.allowedInPrivateBrowsing == false && tab?.content?.private == true
|
||||
|
||||
fun invokeAddonBrowserAction(extensionId: String) {
|
||||
webExtensionBrowserActions[extensionId]?.onClick?.invoke()
|
||||
}
|
||||
|
||||
fun invokeAddonPageAction(extensionId: String) {
|
||||
webExtensionPageActions[extensionId]?.onClick?.invoke()
|
||||
}
|
||||
|
||||
private fun addOrUpdateAction(
|
||||
extension: WebExtensionState,
|
||||
defaultAction: Action,
|
||||
tabAction: Action?,
|
||||
isPageAction: Boolean = false,
|
||||
) {
|
||||
var action = defaultAction
|
||||
|
||||
// Apply tab-specific override of browser/page action
|
||||
tabAction?.let {
|
||||
action = action.copyWithOverride(it)
|
||||
}
|
||||
|
||||
CoroutineScope(iconJobDispatcher).launch {
|
||||
try {
|
||||
//TODO: make variable size
|
||||
val icon = action.loadIcon?.invoke(128)
|
||||
icon?.let {
|
||||
val imageBytes = icon.toWebPBytes()
|
||||
runOnUiThread {
|
||||
addonEvents.onUpdateWebExtensionIcon(
|
||||
timestampArg = System.currentTimeMillis(),
|
||||
extensionIdArg = extension.id,
|
||||
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
|
||||
iconArg = imageBytes
|
||||
) { }
|
||||
}
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
Log.log(
|
||||
Log.Priority.ERROR,
|
||||
"mozac-webextensions",
|
||||
throwable,
|
||||
"Failed to load browser action icon, falling back to default.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val actionMap = if (isPageAction) webExtensionPageActions else webExtensionBrowserActions
|
||||
// Add the global page/browser action if it doesn't exist
|
||||
val toolbarAction = actionMap.getOrPut(extension.id) {
|
||||
val toolbarAction = WebExtensionData(
|
||||
extensionId = extension.id,
|
||||
title = action.title,
|
||||
enabled = action.enabled,
|
||||
badgeText = action.badgeText,
|
||||
badgeTextColor = action.badgeTextColor?.toLong(),
|
||||
badgeBackgroundColor = action.badgeBackgroundColor?.toLong(),
|
||||
)
|
||||
|
||||
addonEvents.onUpsertWebExtensionAction(
|
||||
timestampArg = System.currentTimeMillis(),
|
||||
extensionIdArg = extension.id,
|
||||
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
|
||||
extensionDataArg = toolbarAction
|
||||
) { }
|
||||
|
||||
action
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-5
@@ -1,15 +1,18 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
package eu.lensai.flutter_mozilla_components.interceptor
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import eu.lensai.flutter_mozilla_components.GlobalComponents
|
||||
import mozilla.components.browser.errorpages.ErrorPages
|
||||
import mozilla.components.browser.errorpages.ErrorType
|
||||
import mozilla.components.concept.engine.EngineSession
|
||||
import mozilla.components.concept.engine.request.RequestInterceptor
|
||||
|
||||
class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override fun onLoadRequest(
|
||||
engineSession: EngineSession,
|
||||
uri: String,
|
||||
@@ -20,7 +23,6 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
||||
isDirectNavigation: Boolean,
|
||||
isSubframeRequest: Boolean,
|
||||
): RequestInterceptor.InterceptionResponse? {
|
||||
val components = GlobalComponents.components!!
|
||||
|
||||
return when (uri) {
|
||||
// "about:privatebrowsing" -> {
|
||||
@@ -37,7 +39,7 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
||||
// }
|
||||
|
||||
else -> {
|
||||
components.appLinksInterceptor.onLoadRequest(
|
||||
components.services.appLinksInterceptor.onLoadRequest(
|
||||
engineSession,
|
||||
uri,
|
||||
lastUri,
|
||||
+7
-6
@@ -29,6 +29,10 @@ import kotlin.reflect.typeOf
|
||||
* the thumbnail to the disk cache.
|
||||
*/
|
||||
class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Middleware<BrowserState, BrowserAction> {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
override fun invoke(
|
||||
context: MiddlewareContext<BrowserState, BrowserAction>,
|
||||
@@ -47,7 +51,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
|
||||
//UpdateReaderConnectRequiredAction seems to be the only event that is called predictable
|
||||
//after a hot reload
|
||||
is ReaderAction.UpdateReaderConnectRequiredAction -> {
|
||||
if(!GlobalComponents.components!!.engineReportedInitialized) {
|
||||
if(!components.engineReportedInitialized) {
|
||||
runOnUiThread {
|
||||
flutterEvents.onEngineReadyStateChange(
|
||||
System.currentTimeMillis(),
|
||||
@@ -55,7 +59,7 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
|
||||
) { _ -> }
|
||||
}
|
||||
|
||||
GlobalComponents.components!!.engineReportedInitialized = true
|
||||
components.engineReportedInitialized = true
|
||||
}
|
||||
}
|
||||
is TabListAction.AddTabAction -> {
|
||||
@@ -66,11 +70,8 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
is WebExtensionAction.UpdatePromptRequestWebExtensionAction -> {
|
||||
logger.debug("Event fired: " + action.javaClass.name)
|
||||
}
|
||||
else -> {
|
||||
logger.debug("Event fired: " + action.javaClass.name)
|
||||
//logger.debug("Event fired: " + action.javaClass.name)
|
||||
}
|
||||
}
|
||||
next(action)
|
||||
|
||||
+229
-53
@@ -1,4 +1,4 @@
|
||||
// Autogenerated from Pigeon (v22.5.0), do not edit directly.
|
||||
// Autogenerated from Pigeon (v22.6.0), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
@@ -143,6 +143,17 @@ enum class SelectionPattern(val raw: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
enum class WebExtensionActionType(val raw: Int) {
|
||||
BROWSER(0),
|
||||
PAGE(1);
|
||||
|
||||
companion object {
|
||||
fun ofRaw(raw: Int): WebExtensionActionType? {
|
||||
return values().firstOrNull { it.raw == raw }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translation options that map to the Gecko Translations Options.
|
||||
*
|
||||
@@ -924,6 +935,39 @@ data class CustomSelectionAction (
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class WebExtensionData (
|
||||
val extensionId: String,
|
||||
val title: String? = null,
|
||||
val enabled: Boolean? = null,
|
||||
val badgeText: String? = null,
|
||||
val badgeTextColor: Long? = null,
|
||||
val badgeBackgroundColor: Long? = null
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): WebExtensionData {
|
||||
val extensionId = pigeonVar_list[0] as String
|
||||
val title = pigeonVar_list[1] as String?
|
||||
val enabled = pigeonVar_list[2] as Boolean?
|
||||
val badgeText = pigeonVar_list[3] as String?
|
||||
val badgeTextColor = pigeonVar_list[4] as Long?
|
||||
val badgeBackgroundColor = pigeonVar_list[5] as Long?
|
||||
return WebExtensionData(extensionId, title, enabled, badgeText, badgeTextColor, badgeBackgroundColor)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
extensionId,
|
||||
title,
|
||||
enabled,
|
||||
badgeText,
|
||||
badgeTextColor,
|
||||
badgeBackgroundColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return when (type) {
|
||||
@@ -958,125 +1002,135 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
}
|
||||
}
|
||||
135.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
TranslationOptions.fromList(it)
|
||||
return (readValue(buffer) as Long?)?.let {
|
||||
WebExtensionActionType.ofRaw(it.toInt())
|
||||
}
|
||||
}
|
||||
136.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
ReaderState.fromList(it)
|
||||
TranslationOptions.fromList(it)
|
||||
}
|
||||
}
|
||||
137.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
LastMediaAccessState.fromList(it)
|
||||
ReaderState.fromList(it)
|
||||
}
|
||||
}
|
||||
138.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
HistoryMetadataKey.fromList(it)
|
||||
LastMediaAccessState.fromList(it)
|
||||
}
|
||||
}
|
||||
139.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
PackageCategoryValue.fromList(it)
|
||||
HistoryMetadataKey.fromList(it)
|
||||
}
|
||||
}
|
||||
140.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
ExternalPackage.fromList(it)
|
||||
PackageCategoryValue.fromList(it)
|
||||
}
|
||||
}
|
||||
141.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
LoadUrlFlagsValue.fromList(it)
|
||||
ExternalPackage.fromList(it)
|
||||
}
|
||||
}
|
||||
142.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
SourceValue.fromList(it)
|
||||
LoadUrlFlagsValue.fromList(it)
|
||||
}
|
||||
}
|
||||
143.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
TabState.fromList(it)
|
||||
SourceValue.fromList(it)
|
||||
}
|
||||
}
|
||||
144.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
RecoverableTab.fromList(it)
|
||||
TabState.fromList(it)
|
||||
}
|
||||
}
|
||||
145.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
RecoverableBrowserState.fromList(it)
|
||||
RecoverableTab.fromList(it)
|
||||
}
|
||||
}
|
||||
146.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
IconRequest.fromList(it)
|
||||
RecoverableBrowserState.fromList(it)
|
||||
}
|
||||
}
|
||||
147.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
ResourceSize.fromList(it)
|
||||
IconRequest.fromList(it)
|
||||
}
|
||||
}
|
||||
148.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
Resource.fromList(it)
|
||||
ResourceSize.fromList(it)
|
||||
}
|
||||
}
|
||||
149.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
IconResult.fromList(it)
|
||||
Resource.fromList(it)
|
||||
}
|
||||
}
|
||||
150.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
CookiePartitionKey.fromList(it)
|
||||
IconResult.fromList(it)
|
||||
}
|
||||
}
|
||||
151.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
Cookie.fromList(it)
|
||||
CookiePartitionKey.fromList(it)
|
||||
}
|
||||
}
|
||||
152.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
HistoryItem.fromList(it)
|
||||
Cookie.fromList(it)
|
||||
}
|
||||
}
|
||||
153.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
HistoryState.fromList(it)
|
||||
HistoryItem.fromList(it)
|
||||
}
|
||||
}
|
||||
154.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
ReaderableState.fromList(it)
|
||||
HistoryState.fromList(it)
|
||||
}
|
||||
}
|
||||
155.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
SecurityInfoState.fromList(it)
|
||||
ReaderableState.fromList(it)
|
||||
}
|
||||
}
|
||||
156.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
TabContentState.fromList(it)
|
||||
SecurityInfoState.fromList(it)
|
||||
}
|
||||
}
|
||||
157.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
FindResultState.fromList(it)
|
||||
TabContentState.fromList(it)
|
||||
}
|
||||
}
|
||||
158.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
FindResultState.fromList(it)
|
||||
}
|
||||
}
|
||||
159.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
CustomSelectionAction.fromList(it)
|
||||
}
|
||||
}
|
||||
160.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
WebExtensionData.fromList(it)
|
||||
}
|
||||
}
|
||||
else -> super.readValueOfType(type, buffer)
|
||||
}
|
||||
}
|
||||
@@ -1106,102 +1160,110 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
|
||||
stream.write(134)
|
||||
writeValue(stream, value.raw)
|
||||
}
|
||||
is TranslationOptions -> {
|
||||
is WebExtensionActionType -> {
|
||||
stream.write(135)
|
||||
writeValue(stream, value.toList())
|
||||
writeValue(stream, value.raw)
|
||||
}
|
||||
is ReaderState -> {
|
||||
is TranslationOptions -> {
|
||||
stream.write(136)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is LastMediaAccessState -> {
|
||||
is ReaderState -> {
|
||||
stream.write(137)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is HistoryMetadataKey -> {
|
||||
is LastMediaAccessState -> {
|
||||
stream.write(138)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is PackageCategoryValue -> {
|
||||
is HistoryMetadataKey -> {
|
||||
stream.write(139)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is ExternalPackage -> {
|
||||
is PackageCategoryValue -> {
|
||||
stream.write(140)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is LoadUrlFlagsValue -> {
|
||||
is ExternalPackage -> {
|
||||
stream.write(141)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is SourceValue -> {
|
||||
is LoadUrlFlagsValue -> {
|
||||
stream.write(142)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is TabState -> {
|
||||
is SourceValue -> {
|
||||
stream.write(143)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is RecoverableTab -> {
|
||||
is TabState -> {
|
||||
stream.write(144)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is RecoverableBrowserState -> {
|
||||
is RecoverableTab -> {
|
||||
stream.write(145)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is IconRequest -> {
|
||||
is RecoverableBrowserState -> {
|
||||
stream.write(146)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is ResourceSize -> {
|
||||
is IconRequest -> {
|
||||
stream.write(147)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is Resource -> {
|
||||
is ResourceSize -> {
|
||||
stream.write(148)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is IconResult -> {
|
||||
is Resource -> {
|
||||
stream.write(149)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is CookiePartitionKey -> {
|
||||
is IconResult -> {
|
||||
stream.write(150)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is Cookie -> {
|
||||
is CookiePartitionKey -> {
|
||||
stream.write(151)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is HistoryItem -> {
|
||||
is Cookie -> {
|
||||
stream.write(152)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is HistoryState -> {
|
||||
is HistoryItem -> {
|
||||
stream.write(153)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is ReaderableState -> {
|
||||
is HistoryState -> {
|
||||
stream.write(154)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is SecurityInfoState -> {
|
||||
is ReaderableState -> {
|
||||
stream.write(155)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is TabContentState -> {
|
||||
is SecurityInfoState -> {
|
||||
stream.write(156)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is FindResultState -> {
|
||||
is TabContentState -> {
|
||||
stream.write(157)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is CustomSelectionAction -> {
|
||||
is FindResultState -> {
|
||||
stream.write(158)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is CustomSelectionAction -> {
|
||||
stream.write(159)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is WebExtensionData -> {
|
||||
stream.write(160)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
@@ -1311,7 +1373,7 @@ interface GeckoSessionApi {
|
||||
fun crashRecovery(tabIds: List<String>?)
|
||||
fun purgeHistory()
|
||||
fun updateLastAccess(tabId: String?, lastAccess: Long?)
|
||||
fun requestScreenshot(callback: (Result<ByteArray?>) -> Unit)
|
||||
fun requestScreenshot(sendBack: Boolean, callback: (Result<ByteArray?>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by GeckoSessionApi. */
|
||||
@@ -1626,8 +1688,10 @@ interface GeckoSessionApi {
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.requestScreenshot$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.requestScreenshot{ result: Result<ByteArray?> ->
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val sendBackArg = args[0] as Boolean
|
||||
api.requestScreenshot(sendBackArg) { result: Result<ByteArray?> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(wrapError(error))
|
||||
@@ -2567,3 +2631,115 @@ class GeckoSelectionActionEvents(private val binaryMessenger: BinaryMessenger, p
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface GeckoAddonsApi {
|
||||
fun startAddonManagerActivity()
|
||||
fun invokeAddonAction(extensionId: String, actionType: WebExtensionActionType)
|
||||
|
||||
companion object {
|
||||
/** The codec used by GeckoAddonsApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
GeckoPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `GeckoAddonsApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoAddonsApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.startAddonManagerActivity$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
val wrapped: List<Any?> = try {
|
||||
api.startAddonManagerActivity()
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonsApi.invokeAddonAction$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val extensionIdArg = args[0] as String
|
||||
val actionTypeArg = args[1] as WebExtensionActionType
|
||||
val wrapped: List<Any?> = try {
|
||||
api.invokeAddonAction(extensionIdArg, actionTypeArg)
|
||||
listOf(null)
|
||||
} catch (exception: Throwable) {
|
||||
wrapError(exception)
|
||||
}
|
||||
reply.reply(wrapped)
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
|
||||
class GeckoAddonEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
|
||||
companion object {
|
||||
/** The codec used by GeckoAddonEvents. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
GeckoPigeonCodec()
|
||||
}
|
||||
}
|
||||
fun onUpsertWebExtensionAction(timestampArg: Long, extensionIdArg: String, actionTypeArg: WebExtensionActionType, extensionDataArg: WebExtensionData, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpsertWebExtensionAction$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(timestampArg, extensionIdArg, actionTypeArg, extensionDataArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
fun onRemoveWebExtensionAction(timestampArg: Long, extensionIdArg: String, actionTypeArg: WebExtensionActionType, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onRemoveWebExtensionAction$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(timestampArg, extensionIdArg, actionTypeArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
fun onUpdateWebExtensionIcon(timestampArg: Long, extensionIdArg: String, actionTypeArg: WebExtensionActionType, iconArg: ByteArray, callback: (Result<Unit>) -> Unit)
|
||||
{
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoAddonEvents.onUpdateWebExtensionIcon$separatedMessageChannelSuffix"
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
|
||||
channel.send(listOf(timestampArg, extensionIdArg, actionTypeArg, iconArg)) {
|
||||
if (it is List<*>) {
|
||||
if (it.size > 1) {
|
||||
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
|
||||
} else {
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
} else {
|
||||
callback(Result.failure(createConnectionError(channelName)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package eu.lensai.flutter_mozilla_components.services
|
||||
|
||||
import eu.lensai.flutter_mozilla_components.GlobalComponents
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.feature.downloads.AbstractFetchDownloadService
|
||||
import mozilla.components.support.base.android.NotificationsDelegate
|
||||
|
||||
class DownloadService : AbstractFetchDownloadService() {
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override val httpClient by lazy { components.core.client }
|
||||
override val store: BrowserStore by lazy { components.core.store }
|
||||
override val notificationsDelegate: NotificationsDelegate by lazy { components.notificationsDelegate }
|
||||
}
|
||||
+9
-4
@@ -1,5 +1,6 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
package eu.lensai.flutter_mozilla_components.services
|
||||
|
||||
import eu.lensai.flutter_mozilla_components.GlobalComponents
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.base.crash.CrashReporting
|
||||
import mozilla.components.feature.media.service.AbstractMediaSessionService
|
||||
@@ -9,7 +10,11 @@ import mozilla.components.support.base.android.NotificationsDelegate
|
||||
* See [AbstractMediaSessionService].
|
||||
*/
|
||||
class MediaSessionService : AbstractMediaSessionService() {
|
||||
override val crashReporter: CrashReporting? by lazy { GlobalComponents.components!!.crashReporter }
|
||||
override val store: BrowserStore by lazy { GlobalComponents.components!!.store }
|
||||
override val notificationsDelegate: NotificationsDelegate by lazy { GlobalComponents.components!!.notificationsDelegate }
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override val crashReporter: CrashReporting? by lazy { null }
|
||||
override val store: BrowserStore by lazy { components.core.store }
|
||||
override val notificationsDelegate: NotificationsDelegate by lazy { components.notificationsDelegate }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?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/. -->
|
||||
<resources>
|
||||
<string name="pref_key_autofill" translatable="false">pref_key_autofill</string>
|
||||
<string name="pref_key_sign_in" translatable="false">pref_key_sign_in</string>
|
||||
<string name="pref_key_pair_sign_in" translatable="false">pref_key_pair_sign_in</string>
|
||||
<string name="pref_key_sign_out" translatable="false">pref_key_sign_out</string>
|
||||
<string name="pref_key_sync_now" translatable="false">pref_key_sync_now</string>
|
||||
<string name="pref_key_sync_manage_account" translatable="false">pref_key_sync_manage_account</string>
|
||||
<string name="pref_key_firefox_account" translatable="false">pref_key_firefox_account</string>
|
||||
<string name="pref_key_telemetry" translatable="false">pref_key_telemetry</string>
|
||||
<string name="pref_key_make_default_browser" translatable="false">pref_key_make_default_browser</string>
|
||||
<string name="pref_key_sync_history" translatable="false">pref_key_sync_history</string>
|
||||
<string name="pref_key_sync_tabs" translatable="false">pref_key_sync_tabs</string>
|
||||
<string name="pref_key_sync_passwords" translatable="false">pref_key_sync_passwords</string>
|
||||
<string name="pref_key_remote_debugging" translatable="false">pref_key_remote_debugging</string>
|
||||
<string name="pref_key_testing_mode" translatable="false">pref_key_testing_mode</string>
|
||||
<string name="pref_key_about_page" translatable="false">pref_key_about_page</string>
|
||||
<string name="pref_key_privacy" translatable="false">pref_key_privacy</string>
|
||||
<string name="pref_key_tracking_protection_normal" translatable="false">pref_key_tracking_protection_normal</string>
|
||||
<string name="pref_key_tracking_protection_private" translatable="false">pref_key_tracking_protection_private</string>
|
||||
<string name="pref_key_global_privacy_control" translatable="false">pref_key_global_privacy_control</string>
|
||||
<string name="pref_key_launch_external_app" translatable="false">pref_key_launch_external_app</string>
|
||||
<string name="pref_key_override_amo_collection" translatable="false">pref_key_override_amo_collection</string>
|
||||
<string name="pref_key_override_amo_user" translatable="false">pref_key_override_amo_user</string>
|
||||
<string name="pref_key_compose_ui" translatable="false">pref_key_compose_ui</string>
|
||||
</resources>
|
||||
+1
-1
@@ -13,7 +13,7 @@ import org.mockito.Mockito
|
||||
* you can run them directly from IDEs that support JUnit such as Android Studio.
|
||||
*/
|
||||
|
||||
internal class FlutterMozillaComponentsPluginTest {
|
||||
internal class FlutterMozillaContextPluginTest {
|
||||
@Test
|
||||
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
|
||||
val plugin = FlutterMozillaComponentsPlugin()
|
||||
Reference in New Issue
Block a user