intermediate

This commit is contained in:
Fabian Freund
2024-12-12 09:44:33 +01:00
parent 0710116feb
commit 6b2ae28828
108 changed files with 5531 additions and 2265 deletions
@@ -3,7 +3,7 @@ version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "1.9.22"
ext.mozillaComponentsVersion = '133.0.20241024211615'
ext.mozillaComponentsVersion = '134.0.20241030214633'
repositories {
google()
@@ -74,48 +74,6 @@ android {
viewBinding true
}
dependencies {
implementation "org.mozilla.components:support-utils:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-ktx:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-webextensions:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-locale:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-fetch-httpurlconnection:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-crash:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-dataprotect:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-engine:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-fetch:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-engine-gecko:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-state:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-session-storage:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-icons:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-autofill:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-media:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-tabs:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-webnotifications:$mozillaComponentsVersion"
implementation "org.mozilla.components:service-digitalassetlinks:$mozillaComponentsVersion"
implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.google.android.material:material:1.12.0'
//https://stackoverflow.com/questions/73782320/onbackinvokedcallback-is-not-enabled-for-the-application-in-set-androidenableo
implementation 'androidx.activity:activity:1.9.3'
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.1.0")
}
testOptions {
unitTests.all {
useJUnitPlatform()
@@ -129,6 +87,66 @@ android {
}
}
dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
configurations.configureEach {
exclude group: "org.mozilla.telemetry", module: "glean-native"
}
// Select the Glean from GeckoView.
// `service-sync-logins` requires Glean, which pulls in glean-native,
// but that's also provided by geckoview-omni, so now we need to select which one to use.
project.configurations.configureEach {
resolutionStrategy.capabilitiesResolution.withCapability("org.mozilla.telemetry:glean-native") {
def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module.contains('geckoview') }
if (toBeSelected != null) {
select(toBeSelected)
}
because 'use GeckoView Glean instead of standalone Glean'
}
}
dependencies {
implementation "org.mozilla.components:support-utils:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-ktx:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-webextensions:$mozillaComponentsVersion"
implementation "org.mozilla.components:support-locale:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-fetch-httpurlconnection:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-crash:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
implementation "org.mozilla.components:lib-dataprotect:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-engine:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-fetch:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-toolbar:$mozillaComponentsVersion"
implementation "org.mozilla.components:concept-awesomebar:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-engine-gecko:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-state:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-session-storage:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-storage-sync:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-icons:$mozillaComponentsVersion"
implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-autofill:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-awesomebar:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-media:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-tabs:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion"
implementation "org.mozilla.components:feature-webnotifications:$mozillaComponentsVersion"
implementation "org.mozilla.components:service-digitalassetlinks:$mozillaComponentsVersion"
implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'com.google.android.material:material:1.12.0'
//https://stackoverflow.com/questions/73782320/onbackinvokedcallback-is-not-enabled-for-the-application-in-set-androidenableo
implementation 'androidx.activity:activity-ktx:1.9.3'
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.2.0")
}
@@ -0,0 +1,47 @@
const port = browser.runtime.connectNative("mozacReaderExtract");
let domChanged = false;
let lastContent = '';
function parseContent() {
const article = window.parseReaderable(document);
if (article && lastContent !== article.textContent) {
port.postMessage(article);
lastContent = article.textContent;
}
domChanged = false;
}
port.onMessage.addListener((message) => {
if (message.action === "parseContent") {
if (domChanged) {
parseContent();
}
}
});
const observer = new MutationObserver(() => {
domChanged = true;
});
const observerConfig = {
childList: true,
subtree: true,
characterData: true,
attributes: false
};
// Wait for page to be fully loaded
if (document.readyState === 'complete') {
parseContent();
observer.observe(document.body, observerConfig);
} else {
window.addEventListener('load', parseContent);
}
window.addEventListener('unload', () => {
observer.disconnect();
});
@@ -0,0 +1,29 @@
{
"manifest_version": 2,
"browser_specific_settings": {
"gecko": {
"id": "readability-extract@movenext.me"
}
},
"name": "Extract text of Websites",
"version": "1.0",
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": [
"readability.min.js",
"content.js"
]
}
],
"permissions": [
"mozillaAddons",
"geckoViewAddons",
"nativeMessaging",
"nativeMessagingFromContent",
"storage",
"tabs",
"webNavigation",
"<all_urls>"
]
}
@@ -0,0 +1 @@
../../../../../../javascript/readability/dist/readability.min.js
@@ -15,7 +15,9 @@ import androidx.fragment.app.Fragment
import eu.lensai.flutter_mozilla_components.addons.WebExtensionPromptFeature
import eu.lensai.flutter_mozilla_components.databinding.FragmentBrowserBinding
import eu.lensai.flutter_mozilla_components.ext.getPreferenceKey
import eu.lensai.flutter_mozilla_components.pip.PictureInPictureIntegration
import eu.lensai.flutter_mozilla_components.services.DownloadService
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.app.links.AppLinksFeature
import mozilla.components.feature.downloads.DownloadsFeature
@@ -51,6 +53,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
private val pictureInPictureIntegration = ViewBoundFeatureWrapper<PictureInPictureIntegration>()
private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
@@ -251,16 +254,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
fullScreenFeature.set(
feature = FullScreenFeature(
components.core.store,
components.useCases.sessionUseCases,
sessionId,
) { inFullScreen ->
if (inFullScreen) {
activity?.enterImmersiveMode()
} else {
activity?.exitImmersiveMode()
}
},
store = components.core.store,
sessionUseCases = components.useCases.sessionUseCases,
tabId = sessionId,
fullScreenChanged = ::fullScreenChanged,
),
owner = this,
view = binding.root,
)
@@ -275,6 +273,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
view = binding.root,
)
pictureInPictureIntegration.set(
feature = PictureInPictureIntegration(
components.core.store,
requireActivity(),
sessionId,
),
owner = this,
view = view,
)
secureWindowFeature.set(
feature = SecureWindowFeature(
window = requireActivity().window,
@@ -312,11 +320,34 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
return binding.root
}
private fun fullScreenChanged(enabled: Boolean) {
if (enabled) {
activity?.enterImmersiveMode()
} else {
activity?.exitImmersiveMode()
}
}
@CallSuper
override fun onBackPressed(): Boolean {
return backButtonHandler.any { it.onBackPressed() }
}
final override fun onPause() {
pictureInPictureIntegration.get()?.onHomePressed() ?: false
super.onPause()
}
final override fun onPictureInPictureModeChanged(enabled: Boolean) {
val session = components.core.store.state.selectedTab
val fullScreenMode = session?.content?.fullScreen ?: false
// If we're exiting PIP mode and we're in fullscreen mode, then we should exit fullscreen mode as well.
if (!enabled && fullScreenMode) {
onBackPressed()
fullScreenChanged(false)
}
}
@CallSuper
override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
return activityResultHandler.any { it.onActivityResult(requestCode, data, resultCode) }
@@ -6,6 +6,7 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import eu.lensai.flutter_mozilla_components.addons.WebExtensionActionPopupActivity
import eu.lensai.flutter_mozilla_components.feature.ReadabilityExtractFeature
import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature
import eu.lensai.flutter_mozilla_components.integration.ReaderViewIntegration
import mozilla.components.browser.state.state.WebExtensionState
@@ -19,15 +20,16 @@ import mozilla.components.support.webextensions.WebExtensionPopupObserver
/**
* Fragment used for browsing the web within the main app.
*/
class BrowserFragment(private val context: Context) : BaseBrowserFragment(), UserInteractionHandler {
class BrowserFragment() : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
private val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewIntegration>()
private val readabilityExtractFeature = ViewBoundFeatureWrapper<ReadabilityExtractFeature>()
private val webExtensionPopupObserver = ViewBoundFeatureWrapper<WebExtensionPopupObserver>()
private val webExtToolbarFeature = ViewBoundFeatureWrapper<WebExtensionToolbarFeature>()
override fun createEngine(components: Components): EngineView {
return components.core.engine.createView(context).apply {
return components.core.engine.createView(requireContext()).apply {
selectionActionDelegate = components.selectionAction
}
}
@@ -49,6 +51,12 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use
view = view,
)
readabilityExtractFeature.set(
feature = components.features.readabilityExtractFeature,
owner = this,
view = view,
)
windowFeature.set(
feature = WindowFeature(components.core.store, components.useCases.tabsUseCases),
owner = this,
@@ -72,6 +80,8 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use
owner = this,
view = view,
)
components.core.historyStorage.registerStorageMaintenanceWorker()
}
private fun openPopup(webExtensionState: WebExtensionState) {
@@ -86,7 +96,7 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use
readerViewFeature.onBackPressed() || super.onBackPressed()
companion object {
fun create(context: Context, sessionId: String? = null) = BrowserFragment(context).apply {
fun create(sessionId: String? = null) = BrowserFragment().apply {
arguments = Bundle().apply {
putSessionId(sessionId)
}
@@ -5,10 +5,13 @@ 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.Search
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.GeckoSuggestionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.selection.SelectionActionDelegate
@@ -18,13 +21,15 @@ class Components(private val context: Context,
val flutterEvents: GeckoStateEvents,
val readerViewController: ReaderViewController,
val selectionAction: SelectionActionDelegate,
val addonEvents: GeckoAddonEvents
val addonEvents: GeckoAddonEvents,
val tabContentEvents: GeckoTabContentEvents
) {
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) }
val features by lazy { Features(core.engine, core.store, addonEvents, tabContentEvents) }
val search by lazy { Search(context, core, useCases) }
var engineView: EngineView? = null
var engineReportedInitialized = false
@@ -32,6 +32,8 @@ object EngineProvider {
// About config it's no longer enabled by default
builder.aboutConfigEnabled(true)
builder.extensionsWebAPIEnabled(true)
builder.consoleOutput(true)
runtime = GeckoRuntime.create(context, builder.build())
}
@@ -12,6 +12,7 @@ import eu.lensai.flutter_mozilla_components.api.GeckoFindApiImpl
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.GeckoSuggestionApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoTabsApiImpl
import eu.lensai.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
@@ -25,6 +26,9 @@ import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionControll
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSessionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabsApi
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewEvents
@@ -63,13 +67,18 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
ReaderViewController(_flutterPluginBinding.binaryMessenger)
val addonEvents = GeckoAddonEvents(_flutterPluginBinding.binaryMessenger)
val tabContentEvents = GeckoTabContentEvents(_flutterPluginBinding.binaryMessenger)
val suggestionEvents = GeckoSuggestionEvents(_flutterPluginBinding.binaryMessenger)
GeckoSuggestionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSuggestionApiImpl(suggestionEvents))
GlobalComponents.setUp(
flutterPluginBinding.applicationContext,
_flutterEvents,
readerViewController,
selectionActionDelegate,
addonEvents
addonEvents,
tabContentEvents,
)
GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl {
@@ -104,7 +113,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
}
// Replace this with your actual Fragment
val nativeFragment = BrowserFragment.create(activity!!)
val nativeFragment = BrowserFragment.create()
val fm = (activity as FragmentActivity).supportFragmentManager
fm.beginTransaction()
@@ -3,11 +3,14 @@ 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.GeckoSuggestionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.lensai.flutter_mozilla_components.pigeons.ReaderViewController
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
import mozilla.components.concept.engine.selection.SelectionActionDelegate
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
import mozilla.components.support.base.facts.Facts
@@ -40,7 +43,8 @@ object GlobalComponents {
flutterEvents: GeckoStateEvents,
readerViewController: ReaderViewController,
selectionAction: SelectionActionDelegate,
addonEvents: GeckoAddonEvents
addonEvents: GeckoAddonEvents,
tabContentEvents: GeckoTabContentEvents
) {
Logger.debug("Creating new components")
@@ -50,6 +54,7 @@ object GlobalComponents {
readerViewController,
selectionAction,
addonEvents,
tabContentEvents
)
//newComponents.crashReporter.install(applicationContext)
@@ -65,6 +70,8 @@ object GlobalComponents {
//newComponents.useCases.downloadsUseCases.restoreDownloads()
try {
GlobalPlacesDependencyProvider.initialize(newComponents.core.historyStorage)
GlobalAddonDependencyProvider.initialize(
newComponents.core.addonManager,
newComponents.core.addonUpdater,
@@ -22,7 +22,6 @@ import eu.lensai.flutter_mozilla_components.pigeons.TranslationOptions as Pigeon
class GeckoSessionApiImpl : GeckoSessionApi {
companion object {
private const val TAG = "GeckoSessionApi"
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
}
private val components by lazy {
@@ -0,0 +1,57 @@
package eu.lensai.flutter_mozilla_components.api
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestion
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoSuggestionType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
class GeckoSuggestionApiImpl(
private val suggestionEvents: GeckoSuggestionEvents
) : GeckoSuggestionApi {
companion object {
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
}
private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
override fun onInputChanged(text: String, providers: List<GeckoSuggestionType>) {
for(provider in providers) {
coroutineScope.launch {
val results = when(provider) {
GeckoSuggestionType.SESSION -> components.search.sessionSuggestions.onInputChanged(text)
GeckoSuggestionType.CLIPBOARD -> components.search.clipboardSuggestions.onInputChanged(text)
GeckoSuggestionType.HISTORY -> components.search.historySuggestions.onInputChanged(text)
}
val mappedResults = results.map { result ->
GeckoSuggestion(
id = result.id,
title = result.title,
score = result.score.toLong(),
description = result.description,
type = provider,
editSuggestion = result.editSuggestion,
icon = result.icon?.toWebPBytes()
)
}
runOnUiThread {
suggestionEvents.onSuggestionResult(
System.currentTimeMillis(),
provider,
mappedResults
) { }
}
}
}
}
}
@@ -20,6 +20,7 @@ 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.storage.sync.PlacesHistoryStorage
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
import mozilla.components.concept.engine.DefaultSettings
@@ -35,6 +36,7 @@ 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.HistoryDelegate
import mozilla.components.feature.session.middleware.LastAccessMiddleware
import mozilla.components.feature.sitepermissions.OnDiskSitePermissionsStorage
import mozilla.components.feature.webnotifications.WebNotificationFeature
@@ -51,12 +53,13 @@ class Core(private val context: Context,
PreferenceManager.getDefaultSharedPreferences(context)
}
val engineSettings by lazy {
private 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)
historyTrackingDelegate = HistoryDelegate(lazyHistoryStorage)
trackingProtectionPolicy = createTrackingProtectionPolicy(prefs)
httpsOnlyMode = Engine.HttpsOnlyMode.ENABLED
globalPrivacyControlEnabled = prefs.getBoolean(
@@ -159,6 +162,17 @@ class Core(private val context: Context,
SessionStorage(context, engine)
}
/**
* The storage component to persist browsing history (with the exception of
* private sessions).
*/
val lazyHistoryStorage = lazy { PlacesHistoryStorage(context) }
/**
* A convenience accessor to the [PlacesHistoryStorage].
*/
val historyStorage by lazy { lazyHistoryStorage.value }
/**
* Constructs a [TrackingProtectionPolicy] based on current preferences.
*
@@ -1,12 +1,17 @@
package eu.lensai.flutter_mozilla_components.components
import eu.lensai.flutter_mozilla_components.feature.ReadabilityExtractFeature
import eu.lensai.flutter_mozilla_components.feature.WebExtensionToolbarFeature
import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
class Features(
private val engine: Engine,
private val store: BrowserStore,
private val addonEvents: GeckoAddonEvents
private val addonEvents: GeckoAddonEvents,
private var tabContentEvents: GeckoTabContentEvents
) {
val webExtensionToolbarFeature by lazy {
WebExtensionToolbarFeature(
@@ -14,4 +19,12 @@ class Features(
addonEvents
)
}
val readabilityExtractFeature by lazy {
ReadabilityExtractFeature(
engine,
store,
tabContentEvents
)
}
}
@@ -0,0 +1,36 @@
package eu.lensai.flutter_mozilla_components.components
import android.content.Context
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
class Search(
private val context: Context,
private val core: Core,
private val useCases: UseCases,
) {
val sessionSuggestions by lazy {
SessionSuggestionProvider(
context.resources,
core.store,
useCases.tabsUseCases.selectTab,
)
}
val clipboardSuggestions by lazy {
ClipboardSuggestionProvider(
context,
useCases.sessionUseCases.loadUrl,
)
}
val historySuggestions by lazy {
HistoryStorageSuggestionProvider(
core.historyStorage,
useCases.sessionUseCases.loadUrl,
core.icons,
core.engine,
)
}
}
@@ -0,0 +1,123 @@
package eu.lensai.flutter_mozilla_components.feature
import eu.lensai.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.lensai.flutter_mozilla_components.pigeons.TabContent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.webextension.MessageHandler
import mozilla.components.concept.engine.webextension.Port
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.org.json.tryGetString
import mozilla.components.support.ktx.kotlinx.coroutines.flow.filterChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.webextensions.WebExtensionController
import org.json.JSONObject
class ReadabilityExtractFeature(
private var engine: Engine,
private var store: BrowserStore,
private var tabContentEvents: GeckoTabContentEvents
): LifecycleAwareFeature {
companion object {
private val logger = Logger("ReadabilityExtract")
internal const val READER_EXTRACT_EXTENSION_ID = "readability-extract@movenext.me"
internal const val READER_EXTRACT_CONTENT_PORT = "mozacReaderExtract"
internal const val READER_EXTRACT_EXTENSION_URL =
"resource://android/assets/extensions/readability_extract/"
}
private class ContentMessageHandler(
private var tabContentEvents: GeckoTabContentEvents,
private var tabId: String
) : MessageHandler {
override fun onPortConnected(port: Port) {
logger.debug("Port connected: ${port.name()}")
}
override fun onPortMessage(message: Any, port: Port) {
if (message is JSONObject) {
tabContentEvents.onContentUpdate(
System.currentTimeMillis(),
contentArg = TabContent(
tabId = tabId,
fullContentMarkdown = message.tryGetString("fullContentMarkdown"),
fullContentPlain = message.tryGetString("fullContentPlain"),
extractedContentMarkdown = message.tryGetString("extractedContentMarkdown"),
extractedContentPlain = message.tryGetString("extractedContentPlain")
)
) {}
}
}
}
private var sessionScope: CoroutineScope? = null
private var historyScope: CoroutineScope? = null
private var extensionController = WebExtensionController(
READER_EXTRACT_EXTENSION_ID,
READER_EXTRACT_EXTENSION_URL,
READER_EXTRACT_CONTENT_PORT,
)
override fun start() {
ensureExtensionInstalled()
}
override fun stop() {
sessionScope?.cancel()
historyScope?.cancel()
}
private fun ensureExtensionInstalled() {
extensionController.install(
engine,
onSuccess = { extension ->
sessionScope = store.flowScoped { flow ->
flow.map { it.tabs }
.filterChanged { it.engineState.engineSession }
.collect { state ->
val engineSession = state.engineState.engineSession ?: return@collect
if (extension.hasContentMessageHandler(engineSession, READER_EXTRACT_CONTENT_PORT)) {
return@collect
}
val handler = ContentMessageHandler(tabContentEvents, state.id)
extension.registerContentMessageHandler(engineSession, READER_EXTRACT_CONTENT_PORT, handler)
}
}
historyScope = store.flowScoped { flow ->
flow.map { it.tabs }
.filterChanged { it.content.history }
.ifAnyChanged { arrayOf(it.content.history.currentIndex) }
.collect { state ->
val engineSession = state.engineState.engineSession ?: return@collect
if(state.content.loading) {
return@collect
}
if (extension.hasContentMessageHandler(engineSession, READER_EXTRACT_CONTENT_PORT)) {
if(extensionController.portConnected(engineSession)) {
extensionController.sendContentMessage(
JSONObject().put("action", "parseContent"),
engineSession
)
}
}
}
}
},
onError = { throwable ->
Logger.error("Could not install $READER_EXTRACT_EXTENSION_ID extension", throwable)
},
)
}
}
@@ -119,7 +119,7 @@ class WebExtensionToolbarFeature(
extension.browserAction?.let { browserAction ->
addOrUpdateAction(
extension = extension,
defaultAction = browserAction,
globalAction = browserAction,
tabAction = tab?.extensionState?.get(extension.id)?.browserAction,
)
}
@@ -132,7 +132,7 @@ class WebExtensionToolbarFeature(
if (pageAction.copyWithOverride(tabPageAction).enabled == true) {
addOrUpdateAction(
extension = extension,
defaultAction = pageAction,
globalAction = pageAction,
tabAction = tabPageAction,
isPageAction = true,
)
@@ -156,62 +156,60 @@ class WebExtensionToolbarFeature(
private fun addOrUpdateAction(
extension: WebExtensionState,
defaultAction: Action,
globalAction: Action,
tabAction: Action?,
isPageAction: Boolean = false,
) {
var action = defaultAction
val actionMap = if (isPageAction) webExtensionPageActions else webExtensionBrowserActions
// Add the global page/browser action if it doesn't exist
var toolbarAction = actionMap.getOrPut(extension.id) {
CoroutineScope(iconJobDispatcher).launch {
try {
//TODO: make variable size
val icon = globalAction.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.",
)
}
}
globalAction
}
// Apply tab-specific override of browser/page action
tabAction?.let {
action = action.copyWithOverride(it)
toolbarAction = toolbarAction.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 data = WebExtensionData(
extensionId = extension.id,
title = toolbarAction.title,
enabled = toolbarAction.enabled,
badgeText = toolbarAction.badgeText,
badgeTextColor = toolbarAction.badgeTextColor?.toLong(),
badgeBackgroundColor = toolbarAction.badgeBackgroundColor?.toLong(),
)
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
}
addonEvents.onUpsertWebExtensionAction(
timestampArg = System.currentTimeMillis(),
extensionIdArg = extension.id,
actionTypeArg = if (isPageAction) WebExtensionActionType.PAGE else WebExtensionActionType.BROWSER,
extensionDataArg = data
) { }
}
}
@@ -30,12 +30,12 @@ class ReaderViewIntegration(
if (enabled) {
feature.showReaderView()
readerViewController.appearanceButtonVisibility(true) { _ -> };
readerViewController.appearanceButtonVisibility(System.currentTimeMillis(),true) { _ -> };
} else {
feature.hideReaderView()
feature.hideControls()
readerViewController.appearanceButtonVisibility(false) { _ -> };
readerViewController.appearanceButtonVisibility(System.currentTimeMillis(),false) { _ -> };
}
}
@@ -154,6 +154,18 @@ enum class WebExtensionActionType(val raw: Int) {
}
}
enum class GeckoSuggestionType(val raw: Int) {
SESSION(0),
CLIPBOARD(1),
HISTORY(2);
companion object {
fun ofRaw(raw: Int): GeckoSuggestionType? {
return values().firstOrNull { it.raw == raw }
}
}
}
/**
* Translation options that map to the Gecko Translations Options.
*
@@ -968,6 +980,72 @@ data class WebExtensionData (
)
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class GeckoSuggestion (
val id: String,
val type: GeckoSuggestionType,
val score: Long,
val title: String? = null,
val description: String? = null,
val editSuggestion: String? = null,
val icon: ByteArray? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): GeckoSuggestion {
val id = pigeonVar_list[0] as String
val type = pigeonVar_list[1] as GeckoSuggestionType
val score = pigeonVar_list[2] as Long
val title = pigeonVar_list[3] as String?
val description = pigeonVar_list[4] as String?
val editSuggestion = pigeonVar_list[5] as String?
val icon = pigeonVar_list[6] as ByteArray?
return GeckoSuggestion(id, type, score, title, description, editSuggestion, icon)
}
}
fun toList(): List<Any?> {
return listOf(
id,
type,
score,
title,
description,
editSuggestion,
icon,
)
}
}
/** Generated class from Pigeon that represents data sent in messages. */
data class TabContent (
val tabId: String,
val fullContentMarkdown: String? = null,
val fullContentPlain: String? = null,
val extractedContentMarkdown: String? = null,
val extractedContentPlain: String? = null
)
{
companion object {
fun fromList(pigeonVar_list: List<Any?>): TabContent {
val tabId = pigeonVar_list[0] as String
val fullContentMarkdown = pigeonVar_list[1] as String?
val fullContentPlain = pigeonVar_list[2] as String?
val extractedContentMarkdown = pigeonVar_list[3] as String?
val extractedContentPlain = pigeonVar_list[4] as String?
return TabContent(tabId, fullContentMarkdown, fullContentPlain, extractedContentMarkdown, extractedContentPlain)
}
}
fun toList(): List<Any?> {
return listOf(
tabId,
fullContentMarkdown,
fullContentPlain,
extractedContentMarkdown,
extractedContentPlain,
)
}
}
private open class GeckoPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
@@ -1007,130 +1085,145 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
}
}
136.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
TranslationOptions.fromList(it)
return (readValue(buffer) as Long?)?.let {
GeckoSuggestionType.ofRaw(it.toInt())
}
}
137.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
ReaderState.fromList(it)
TranslationOptions.fromList(it)
}
}
138.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
LastMediaAccessState.fromList(it)
ReaderState.fromList(it)
}
}
139.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HistoryMetadataKey.fromList(it)
LastMediaAccessState.fromList(it)
}
}
140.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
PackageCategoryValue.fromList(it)
HistoryMetadataKey.fromList(it)
}
}
141.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
ExternalPackage.fromList(it)
PackageCategoryValue.fromList(it)
}
}
142.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
LoadUrlFlagsValue.fromList(it)
ExternalPackage.fromList(it)
}
}
143.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
SourceValue.fromList(it)
LoadUrlFlagsValue.fromList(it)
}
}
144.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
TabState.fromList(it)
SourceValue.fromList(it)
}
}
145.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
RecoverableTab.fromList(it)
TabState.fromList(it)
}
}
146.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
RecoverableBrowserState.fromList(it)
RecoverableTab.fromList(it)
}
}
147.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
IconRequest.fromList(it)
RecoverableBrowserState.fromList(it)
}
}
148.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
ResourceSize.fromList(it)
IconRequest.fromList(it)
}
}
149.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
Resource.fromList(it)
ResourceSize.fromList(it)
}
}
150.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
IconResult.fromList(it)
Resource.fromList(it)
}
}
151.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
CookiePartitionKey.fromList(it)
IconResult.fromList(it)
}
}
152.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
Cookie.fromList(it)
CookiePartitionKey.fromList(it)
}
}
153.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HistoryItem.fromList(it)
Cookie.fromList(it)
}
}
154.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
HistoryState.fromList(it)
HistoryItem.fromList(it)
}
}
155.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
ReaderableState.fromList(it)
HistoryState.fromList(it)
}
}
156.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
SecurityInfoState.fromList(it)
ReaderableState.fromList(it)
}
}
157.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
TabContentState.fromList(it)
SecurityInfoState.fromList(it)
}
}
158.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
FindResultState.fromList(it)
TabContentState.fromList(it)
}
}
159.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
CustomSelectionAction.fromList(it)
FindResultState.fromList(it)
}
}
160.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
CustomSelectionAction.fromList(it)
}
}
161.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
WebExtensionData.fromList(it)
}
}
162.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
GeckoSuggestion.fromList(it)
}
}
163.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
TabContent.fromList(it)
}
}
else -> super.readValueOfType(type, buffer)
}
}
@@ -1164,106 +1257,118 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
stream.write(135)
writeValue(stream, value.raw)
}
is TranslationOptions -> {
is GeckoSuggestionType -> {
stream.write(136)
writeValue(stream, value.toList())
writeValue(stream, value.raw)
}
is ReaderState -> {
is TranslationOptions -> {
stream.write(137)
writeValue(stream, value.toList())
}
is LastMediaAccessState -> {
is ReaderState -> {
stream.write(138)
writeValue(stream, value.toList())
}
is HistoryMetadataKey -> {
is LastMediaAccessState -> {
stream.write(139)
writeValue(stream, value.toList())
}
is PackageCategoryValue -> {
is HistoryMetadataKey -> {
stream.write(140)
writeValue(stream, value.toList())
}
is ExternalPackage -> {
is PackageCategoryValue -> {
stream.write(141)
writeValue(stream, value.toList())
}
is LoadUrlFlagsValue -> {
is ExternalPackage -> {
stream.write(142)
writeValue(stream, value.toList())
}
is SourceValue -> {
is LoadUrlFlagsValue -> {
stream.write(143)
writeValue(stream, value.toList())
}
is TabState -> {
is SourceValue -> {
stream.write(144)
writeValue(stream, value.toList())
}
is RecoverableTab -> {
is TabState -> {
stream.write(145)
writeValue(stream, value.toList())
}
is RecoverableBrowserState -> {
is RecoverableTab -> {
stream.write(146)
writeValue(stream, value.toList())
}
is IconRequest -> {
is RecoverableBrowserState -> {
stream.write(147)
writeValue(stream, value.toList())
}
is ResourceSize -> {
is IconRequest -> {
stream.write(148)
writeValue(stream, value.toList())
}
is Resource -> {
is ResourceSize -> {
stream.write(149)
writeValue(stream, value.toList())
}
is IconResult -> {
is Resource -> {
stream.write(150)
writeValue(stream, value.toList())
}
is CookiePartitionKey -> {
is IconResult -> {
stream.write(151)
writeValue(stream, value.toList())
}
is Cookie -> {
is CookiePartitionKey -> {
stream.write(152)
writeValue(stream, value.toList())
}
is HistoryItem -> {
is Cookie -> {
stream.write(153)
writeValue(stream, value.toList())
}
is HistoryState -> {
is HistoryItem -> {
stream.write(154)
writeValue(stream, value.toList())
}
is ReaderableState -> {
is HistoryState -> {
stream.write(155)
writeValue(stream, value.toList())
}
is SecurityInfoState -> {
is ReaderableState -> {
stream.write(156)
writeValue(stream, value.toList())
}
is TabContentState -> {
is SecurityInfoState -> {
stream.write(157)
writeValue(stream, value.toList())
}
is FindResultState -> {
is TabContentState -> {
stream.write(158)
writeValue(stream, value.toList())
}
is CustomSelectionAction -> {
is FindResultState -> {
stream.write(159)
writeValue(stream, value.toList())
}
is WebExtensionData -> {
is CustomSelectionAction -> {
stream.write(160)
writeValue(stream, value.toList())
}
is WebExtensionData -> {
stream.write(161)
writeValue(stream, value.toList())
}
is GeckoSuggestion -> {
stream.write(162)
writeValue(stream, value.toList())
}
is TabContent -> {
stream.write(163)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
}
}
@@ -2553,12 +2658,12 @@ class ReaderViewController(private val binaryMessenger: BinaryMessenger, private
GeckoPigeonCodec()
}
}
fun appearanceButtonVisibility(visibleArg: Boolean, callback: (Result<Unit>) -> Unit)
fun appearanceButtonVisibility(timestampArg: Long, visibleArg: Boolean, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.ReaderViewController.appearanceButtonVisibility$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(visibleArg)) {
channel.send(listOf(timestampArg, 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?)))
@@ -2743,3 +2848,90 @@ class GeckoAddonEvents(private val binaryMessenger: BinaryMessenger, private val
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoSuggestionApi {
fun onInputChanged(text: String, providers: List<GeckoSuggestionType>)
companion object {
/** The codec used by GeckoSuggestionApi. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
/** Sets up an instance of `GeckoSuggestionApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoSuggestionApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionApi.onInputChanged$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val textArg = args[0] as String
val providersArg = args[1] as List<GeckoSuggestionType>
val wrapped: List<Any?> = try {
api.onInputChanged(textArg, providersArg)
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 GeckoSuggestionEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by GeckoSuggestionEvents. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
}
fun onSuggestionResult(timestampArg: Long, suggestionTypeArg: GeckoSuggestionType, suggestionsArg: List<GeckoSuggestion>, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoSuggestionEvents.onSuggestionResult$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(timestampArg, suggestionTypeArg, suggestionsArg)) {
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 GeckoTabContentEvents(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by GeckoTabContentEvents. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
}
fun onContentUpdate(timestampArg: Long, contentArg: TabContent, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoTabContentEvents.onContentUpdate$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(timestampArg, contentArg)) {
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,50 @@
/* 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.pip
import android.app.Activity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.session.PictureInPictureFeature
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
class PictureInPictureIntegration(
private val store: BrowserStore,
activity: Activity,
private val customTabId: String?,
private val whiteList: List<String> = listOf("youtube.com/tv"),
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
private val pictureFeature = PictureInPictureFeature(store, activity)
private var whiteListed = false
override fun start() {
scope = store.flowScoped { flow ->
flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabId) }
.distinctUntilChangedBy { it.content.url }
.collect { whiteListed = isWhitelisted(it.content.url) }
}
}
override fun stop() {
scope?.cancel()
}
fun onHomePressed() = if (whiteListed) {
pictureFeature.enterPipModeCompat()
} else {
pictureFeature.onHomePressed()
}
private fun isWhitelisted(url: String): Boolean {
val exists = whiteList.firstOrNull { url.contains(it) }
return exists != null
}
}