mainly refactoring & addons

This commit is contained in:
Fabian Freund
2024-10-29 10:03:24 +01:00
parent 91a4319b28
commit 0710116feb
127 changed files with 4234 additions and 1815 deletions
@@ -128,3 +128,7 @@ android {
}
}
}
dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
}
@@ -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
}
}
@@ -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 {
@@ -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,
)
}
}
@@ -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)
// }
//}
@@ -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 }
}
@@ -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)
}
}
@@ -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)
@@ -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) { _ -> }
}
@@ -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
}
}
@@ -1,2 +0,0 @@
package eu.lensai.flutter_mozilla_components
@@ -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()
}
}
@@ -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()
}
}
@@ -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)
}
@@ -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 {
@@ -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(
@@ -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),
)
}
@@ -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)
}
}
}
@@ -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)
}
}
}
}
}
@@ -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)
}
}
}
@@ -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)
}
}
}
}
@@ -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
}
@@ -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
@@ -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)
}
}
}
@@ -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))
}
}
}
@@ -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
}
}
}
}
@@ -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()
}
}
}
@@ -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,
) }
) { _ -> }
}
}
}
}
@@ -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
)
}
}
@@ -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)
},
)
}
}
@@ -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) }
}
@@ -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)
@@ -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
}
}
}
@@ -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,
@@ -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)
@@ -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)))
}
}
}
}
@@ -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 }
}
@@ -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>
@@ -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()