getting there

This commit is contained in:
Fabian Freund
2024-10-15 11:13:32 +02:00
parent 10d6a3de1d
commit c4d93e8fe7
56 changed files with 1830 additions and 762 deletions
@@ -68,6 +68,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
_components = GlobalComponents.components
_engineView = createEngine(components)
_components?.engineView = _engineView
val engineNativeView = engineView.asView()
// Set layout parameters
@@ -17,6 +17,8 @@ import eu.lensai.flutter_mozilla_components.pigeons.ReaderableState
import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState
import eu.lensai.flutter_mozilla_components.pigeons.SelectionAction
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
@@ -31,10 +33,12 @@ 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.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
@@ -73,7 +77,7 @@ private const val DAY_IN_MINUTES = 24 * 60L
@Suppress("LargeClass")
open class DefaultComponents(
private val applicationContext: Context,
private val flutterEvents: GeckoStateEvents,
val flutterEvents: GeckoStateEvents,
val readerViewController: ReaderViewController,
val selectionAction: SelectionAction,
) {
@@ -83,6 +87,8 @@ open class DefaultComponents(
const val PREF_GLOBAL_PRIVACY_CONTROL = "sample_browser_global_privacy_control"
}
var engineView: EngineView? = null
val preferences: SharedPreferences =
applicationContext.getSharedPreferences(SAMPLE_BROWSER_PREFERENCES, Context.MODE_PRIVATE)
@@ -140,6 +146,7 @@ open class DefaultComponents(
FileUploadsDirCleaner { applicationContext.cacheDir }
}
@OptIn(FlowPreview::class)
val store by lazy {
BrowserStore(
middleware = listOf(
@@ -156,6 +163,7 @@ open class DefaultComponents(
).apply {
this.flowScoped { flow ->
flow.map { state -> state.selectedTabId }
.distinctUntilChanged()
.collect { tabId ->
flutterEvents.onSelectedTabChange(
tabId
@@ -166,8 +174,10 @@ open class DefaultComponents(
this.flowScoped { flow ->
flow.mapNotNull { state -> state.tabs }
.filterChanged {
it.content.icon
it.content
}
.ifAnyChanged { arrayOf (it.content.icon) }
.debounce { 50 }
.collect { tab ->
val iconBytes = tab.content.icon?.toWebPBytes()
flutterEvents.onIconChange(
@@ -182,6 +192,7 @@ open class DefaultComponents(
.filterChanged {
it.content.securityInfo
}
.debounce { 50 }
.collect { tab ->
flutterEvents.onSecurityInfoStateChange(
tab.id,
@@ -200,10 +211,11 @@ open class DefaultComponents(
it.readerState
}
.ifAnyChanged { arrayOf(
it.readerState.readerable,
it.readerState.active,
)
it.readerState.readerable,
it.readerState.active,
)
}
.debounce { 50 }
.collect { tab ->
flutterEvents.onReaderableStateChange(
tab.id,
@@ -226,6 +238,7 @@ open class DefaultComponents(
it.content.canGoForward,
)
}
.debounce { 50 }
.collect { tab ->
flutterEvents.onHistoryStateChange(
tab.id,
@@ -261,8 +274,11 @@ open class DefaultComponents(
it.content.private,
it.content.fullScreen,
it.content.progress,
it.content.loading) }
it.content.loading)
}
.debounce { 50 }
.collect { tab ->
logger.info("title: ${tab.content.title} ${tab.content.url}")
flutterEvents.onTabContentStateChange(
TabContentState(
id = tab.id,
@@ -283,6 +299,7 @@ open class DefaultComponents(
.filterChanged {
it.content.findResults
}
.distinctUntilChanged()
.collect { tab ->
tab.content.findResults
flutterEvents.onFindResults(
@@ -296,6 +313,8 @@ open class DefaultComponents(
}
}
icons.install(engine, this)
WebNotificationFeature(
applicationContext,
engine,
@@ -87,6 +87,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
private var activity: Activity? = null
private lateinit var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding;
private lateinit var _flutterEvents : GeckoStateEvents
init {
Log.addSink(AndroidLogSink())
@@ -95,14 +96,15 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
_flutterPluginBinding = flutterPluginBinding
val flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger)
_flutterEvents = GeckoStateEvents(_flutterPluginBinding.binaryMessenger)
val readerViewController =
ReaderViewController(_flutterPluginBinding.binaryMessenger)
val selectionActionDelegate = SelectionAction(_flutterPluginBinding.binaryMessenger)
GlobalComponents.setUp(
flutterPluginBinding.applicationContext,
flutterEvents,
_flutterEvents,
readerViewController,
selectionActionDelegate
)
@@ -141,7 +143,6 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
fm.beginTransaction()
.replace(FRAGMENT_CONTAINER_ID, nativeFragment)
.commitAllowingStateLoss()
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
@@ -152,7 +153,12 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
this.activity = binding.activity
_flutterPluginBinding.platformViewRegistry.registerViewFactory(
"eu.lensai/gecko", GeckoViewFactory(binding.activity, FRAGMENT_CONTAINER_ID))
"eu.lensai/gecko", GeckoViewFactory(
binding.activity,
FRAGMENT_CONTAINER_ID,
_flutterEvents
)
)
}
override fun onDetachedFromActivityForConfigChanges() {
@@ -5,20 +5,26 @@ import android.app.Activity
import android.content.Context
import android.view.ViewGroup
import android.widget.FrameLayout
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
class GeckoViewFactory(private val activity: Activity, private val containerId: Int) : PlatformViewFactory(
class GeckoViewFactory(
private val activity: Activity,
private val containerId: Int,
private val flutterEvents: GeckoStateEvents
) : PlatformViewFactory(
StandardMessageCodec.INSTANCE) {
override fun create(context: Context?, id: Int, args: Any?): PlatformView {
return NativeFragmentView(this.activity, this.containerId)
return NativeFragmentView(this.activity, this.containerId, this.flutterEvents)
}
}
private class NativeFragmentView(
private val activity: Activity?,
private val containerId: Int
activity: Activity?,
containerId: Int,
private val flutterEvents: GeckoStateEvents
) : PlatformView {
private val container: View
@@ -32,6 +38,11 @@ private class NativeFragmentView(
container.id = containerId
}
override fun onFlutterViewAttached(flutterView: View) {
super.onFlutterViewAttached(flutterView)
flutterEvents.onFragmentReadyStateChange(true) { _ -> }
}
override fun getView(): View {
return container
}
@@ -2,17 +2,35 @@ 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 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
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 }
override fun loadUrl(
tabId: String?,
@@ -124,4 +142,21 @@ class GeckoSessionApiImpl : GeckoSessionApi {
lastAccess = lastAccess ?: System.currentTimeMillis()
)
}
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))
}
}
} else {
callback(Result.failure(Exception("No selected tab for screenshot")))
}
}
}
@@ -8,7 +8,18 @@ import eu.lensai.flutter_mozilla_components.pigeons.RestoreLocation as PigeonRes
import eu.lensai.flutter_mozilla_components.pigeons.RecoverableTab as PigeonRecoverableTab
import eu.lensai.flutter_mozilla_components.pigeons.SourceValue
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
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.ReaderableState
import eu.lensai.flutter_mozilla_components.pigeons.RestoreLocation
import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState
import eu.lensai.flutter_mozilla_components.pigeons.TabContentState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.browser.session.storage.RecoverableBrowserState
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab
@@ -18,16 +29,21 @@ import mozilla.components.browser.state.state.ReaderState
import mozilla.components.browser.state.state.SessionState
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
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 events: GeckoStateEvents by lazy { GlobalComponents.components!!.flutterEvents }
private fun restoreSource(source: SourceValue ) : SessionState.Source {
return SessionState.Source.restore(
@@ -88,6 +104,135 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
}
}
override fun syncEvents(
onSelectedTabChange: Boolean,
onTabListChange: Boolean,
onTabContentStateChange: Boolean,
onIconChange: Boolean,
onSecurityInfoStateChange: Boolean,
onReaderableStateChange: Boolean,
onHistoryStateChange: Boolean,
onFindResults: Boolean,
onThumbnailChange: Boolean,
) {
if(onSelectedTabChange) {
events.onSelectedTabChange(
state.selectedTabId
) { _ -> }
}
if(onTabListChange) {
events.onTabListChange(state.tabs.map {tab -> tab.id}) { _ -> }
}
if(onTabContentStateChange) {
state.tabs.forEach { tab ->
events.onTabContentStateChange(
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) {
state.tabs.forEach { tab ->
if(tab.content.icon != null) {
val iconBytes = tab.content.icon?.toWebPBytes()
events.onIconChange(
tab.id,
iconBytes
) { _ -> }
}
}
}
if(onSecurityInfoStateChange) {
state.tabs.forEach { tab ->
val iconBytes = tab.content.icon?.toWebPBytes()
events.onSecurityInfoStateChange(
tab.id,
SecurityInfoState(
tab.content.securityInfo.secure,
tab.content.securityInfo.host,
tab.content.securityInfo.issuer,
)
) { _ -> }
}
}
if(onReaderableStateChange) {
state.tabs.forEach { tab ->
events.onReaderableStateChange(
tab.id,
ReaderableState(
tab.readerState.readerable,
tab.readerState.active,
)
) { _ -> }
}
}
if(onHistoryStateChange) {
state.tabs.forEach { tab ->
events.onHistoryStateChange(
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) {
state.tabs.forEach { tab ->
events.onFindResults(
tab.id,
tab.content.findResults.map { result -> FindResultState(
activeMatchOrdinal = result.activeMatchOrdinal.toLong(),
numberOfMatches = result.numberOfMatches.toLong(),
isDoneCounting = result.isDoneCounting,
) }
) { _ -> }
}
}
if(onThumbnailChange) {
state.tabs.forEach { tab ->
CoroutineScope(Dispatchers.Default).launch {
val bitmap = thumbnailStorage.loadThumbnail(
ImageLoadRequest(
id = tab.id,
//TODO: make this configurable
size = 600,
isPrivate = tab.content.private
)
).await()
if(bitmap != null) {
val bytes = bitmap.toWebPBytes()
runOnUiThread {
events.onThumbnailChange(tab.id, bytes) { _ -> }
}
}
}
}
}
}
override fun selectTab(tabId: String) {
tabsUseCases.selectTab(tabId = tabId)
}
@@ -12,6 +12,7 @@ import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
import java.io.ByteArrayOutputStream
@@ -30,7 +31,9 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
when (action) {
is ContentAction.UpdateThumbnailAction -> {
val bytes = action.thumbnail.toWebPBytes()
flutterEvents.onThumbnailChange(action.sessionId, bytes) { _ -> }
runOnUiThread {
flutterEvents.onThumbnailChange(action.sessionId, bytes) { _ -> }
}
}
else -> {
// no-op
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v22.4.2), do not edit directly.
// Autogenerated from Pigeon (v22.5.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -1239,6 +1239,7 @@ interface GeckoSessionApi {
fun crashRecovery(tabIds: List<String>?)
fun purgeHistory()
fun updateLastAccess(tabId: String?, lastAccess: Long?)
fun requestScreenshot(callback: (Result<ByteArray?>) -> Unit)
companion object {
/** The codec used by GeckoSessionApi. */
@@ -1550,11 +1551,30 @@ interface GeckoSessionApi {
channel.setMessageHandler(null)
}
}
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?> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoTabsApi {
fun syncEvents(onSelectedTabChange: Boolean, onTabListChange: Boolean, onTabContentStateChange: Boolean, onIconChange: Boolean, onSecurityInfoStateChange: Boolean, onReaderableStateChange: Boolean, onHistoryStateChange: Boolean, onFindResults: Boolean, onThumbnailChange: Boolean)
fun selectTab(tabId: String)
fun removeTab(tabId: String)
fun addTab(url: String, selectTab: Boolean, startLoading: Boolean, parentId: String?, flags: LoadUrlFlagsValue, contextId: String?, source: SourceValue, private: Boolean, historyMetadata: HistoryMetadataKey?, additionalHeaders: Map<String, String>?): String
@@ -1585,6 +1605,32 @@ interface GeckoTabsApi {
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoTabsApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.syncEvents$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val onSelectedTabChangeArg = args[0] as Boolean
val onTabListChangeArg = args[1] as Boolean
val onTabContentStateChangeArg = args[2] as Boolean
val onIconChangeArg = args[3] as Boolean
val onSecurityInfoStateChangeArg = args[4] as Boolean
val onReaderableStateChangeArg = args[5] as Boolean
val onHistoryStateChangeArg = args[6] as Boolean
val onFindResultsArg = args[7] as Boolean
val onThumbnailChangeArg = args[8] as Boolean
val wrapped: List<Any?> = try {
api.syncEvents(onSelectedTabChangeArg, onTabListChangeArg, onTabContentStateChangeArg, onIconChangeArg, onSecurityInfoStateChangeArg, onReaderableStateChangeArg, onHistoryStateChangeArg, onFindResultsArg, onThumbnailChangeArg)
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.GeckoTabsApi.selectTab$separatedMessageChannelSuffix", codec)
if (api != null) {
@@ -2107,6 +2153,23 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
GeckoPigeonCodec()
}
}
fun onFragmentReadyStateChange(stateArg: Boolean, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFragmentReadyStateChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(stateArg)) {
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 onTabListChange(tabIdsArg: List<String>, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
@@ -2337,23 +2400,6 @@ class ReaderViewController(private val binaryMessenger: BinaryMessenger, private
}
}
}
fun readerViewButtonVisibility(visibleArg: Boolean, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.readerViewButtonVisibility$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(visibleArg)) {
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)))
}
}
}
}
/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
class SelectionAction(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {