This commit is contained in:
Fabian Freund
2025-02-03 15:09:04 +01:00
parent 36acd46318
commit 025ecaf2e3
132 changed files with 4431 additions and 1644 deletions
@@ -7,6 +7,7 @@ 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.GeckoDeleteBrowsingDataControllerImpl
import eu.lensai.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoFindApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl
@@ -20,6 +21,7 @@ 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.GeckoDeleteBrowsingDataController
import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoFindApi
import eu.lensai.flutter_mozilla_components.pigeons.GeckoIconsApi
@@ -98,6 +100,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
GeckoSelectionActionController.setUp(_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl(
selectionActionDelegate
))
GeckoDeleteBrowsingDataController.setUp(_flutterPluginBinding.binaryMessenger, GeckoDeleteBrowsingDataControllerImpl())
ReaderViewEvents.setUp(
_flutterPluginBinding.binaryMessenger,
@@ -0,0 +1,78 @@
/* 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 androidx.paging.DataSource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.concept.engine.permission.SitePermissionsStorage
import kotlin.coroutines.CoroutineContext
class PermissionStorage(
private val permissionsStorage: SitePermissionsStorage,
private val dispatcher: CoroutineContext = Dispatchers.IO,
) {
/**
* Persists the [sitePermissions] provided as a parameter.
*
* @param sitePermissions The [SitePermissions] to be stored.
* @param private Indicates if the [SitePermissions] belongs to a private session.
*/
suspend fun add(sitePermissions: SitePermissions, private: Boolean) = withContext(dispatcher) {
permissionsStorage.save(sitePermissions, private = private)
}
/**
* Finds all [SitePermissions] that match the [origin].
*
* @param origin The site to be used as filter in the search.
* @param private Indicates if the [origin] belongs to a private session.
*/
suspend fun findSitePermissionsBy(origin: String, private: Boolean): SitePermissions? =
withContext(dispatcher) {
permissionsStorage.findSitePermissionsBy(origin, private = private)
}
/**
* Replaces an existing SitePermissions with the values of provided [sitePermissions].
*
* @param sitePermissions The [SitePermissions] to be updated.
* @param private Indicates if the [SitePermissions] belongs to a private session.
*/
suspend fun updateSitePermissions(sitePermissions: SitePermissions, private: Boolean) =
withContext(dispatcher) {
permissionsStorage.update(sitePermissions, private = private)
}
/**
* Returns all saved [SitePermissions] instances as a [DataSource.Factory].
*
* A consuming app can transform the data source into a `LiveData<PagedList>` of when using RxJava2 into a
* `Flowable<PagedList>` or `Observable<PagedList>`, that can be observed.
*
* - https://developer.android.com/topic/libraries/architecture/paging/data
* - https://developer.android.com/topic/libraries/architecture/paging/ui
*/
suspend fun getSitePermissionsPaged(): DataSource.Factory<Int, SitePermissions> {
return permissionsStorage.getSitePermissionsPaged()
}
/**
* Deletes all sitePermissions that match the sitePermissions provided as a parameter.
* @param sitePermissions The [SitePermissions] to be deleted from the storage.
*/
suspend fun deleteSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) {
permissionsStorage.remove(sitePermissions, private = false)
}
/**
* Deletes all sitePermissions sitePermissions.
*/
suspend fun deleteAllSitePermissions() = withContext(dispatcher) {
permissionsStorage.removeAll()
}
}
@@ -282,11 +282,11 @@ class WebExtensionPromptFeature(
var title = context.getString(MozComp.string.mozac_feature_addons_cant_install_extension, "")
val message = when (exception) {
is WebExtensionInstallException.Blocklisted -> {
context.getString(MozComp.string.mozac_feature_addons_blocklisted_1, addonName)
context.getString(MozComp.string.mozac_feature_addons_blocklisted_2, addonName)
}
is WebExtensionInstallException.SoftBlocked -> {
context.getString(MozComp.string.mozac_feature_addons_soft_blocked, addonName)
context.getString(MozComp.string.mozac_feature_addons_soft_blocked_1, addonName)
}
is WebExtensionInstallException.UserCancelled -> {
@@ -0,0 +1,107 @@
package eu.lensai.flutter_mozilla_components.api
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoDeleteBrowsingDataController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.translate.ModelManagementOptions
import mozilla.components.concept.engine.translate.ModelOperation
import mozilla.components.concept.engine.translate.OperationLevel
class GeckoDeleteBrowsingDataControllerImpl : GeckoDeleteBrowsingDataController {
companion object {
private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
}
private val components by lazy {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
override fun deleteTabs(callback: (Result<Unit>) -> Unit) {
coroutineScope.launch {
withContext(Dispatchers.Main) {
components.useCases.tabsUseCases.removeAllTabs.invoke(false)
callback(Result.success(Unit))
}
}
}
override fun deleteBrowsingHistory(callback: (Result<Unit>) -> Unit) {
coroutineScope.launch {
withContext(Dispatchers.Main) {
components.core.historyStorage.deleteEverything()
components.core.store.dispatch(EngineAction.PurgeHistoryAction)
components.core.icons.clear()
components.core.store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
callback(Result.success(Unit))
}
}
}
override fun deleteCookiesAndSiteData(callback: (Result<Unit>) -> Unit) {
coroutineScope.launch {
withContext(Dispatchers.Main) {
components.core.engine.clearData(
Engine.BrowsingData.select(
Engine.BrowsingData.COOKIES,
Engine.BrowsingData.AUTH_SESSIONS,
),
)
components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
callback(Result.success(Unit))
}
}
}
override fun deleteCachedFiles(callback: (Result<Unit>) -> Unit) {
coroutineScope.launch {
withContext(Dispatchers.Main) {
components.core.engine.manageTranslationsLanguageModel(
options = ModelManagementOptions(
operation = ModelOperation.DELETE,
operationLevel = OperationLevel.CACHE,
),
onSuccess = { },
onError = { },
)
components.core.engine.clearData(
Engine.BrowsingData.select(Engine.BrowsingData.ALL_CACHES),
)
callback(Result.success(Unit))
}
}
}
override fun deleteSitePermissions(callback: (Result<Unit>) -> Unit) {
coroutineScope.launch {
withContext(Dispatchers.Main) {
components.core.engine.clearData(
Engine.BrowsingData.select(Engine.BrowsingData.ALL_SITE_SETTINGS),
)
components.core.permissionStorage.deleteAllSitePermissions()
callback(Result.success(Unit))
}
}
}
override fun deleteDownloads(callback: (Result<Unit>) -> Unit) {
coroutineScope.launch {
withContext(Dispatchers.Main) {
components.useCases.downloadsUseCases.removeAllDownloads.invoke()
callback(Result.success(Unit))
}
}
}
}
@@ -27,6 +27,24 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi {
requireNotNull(GlobalComponents.components) { "Components not initialized" }
}
private fun updateFingerprintingProtection(trackingProtectionPolicy: eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy) {
when(trackingProtectionPolicy) {
eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.STRICT -> {
components.core.engineSettings.fingerprintingProtection = true
components.core.engineSettings.fingerprintingProtectionPrivateBrowsing = true
}
eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.RECOMMENDED -> {
components.core.engineSettings.fingerprintingProtection = false
components.core.engineSettings.fingerprintingProtectionPrivateBrowsing = true
}
eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.CUSTOM -> TODO()
eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.NONE -> {
components.core.engineSettings.fingerprintingProtection = false
components.core.engineSettings.fingerprintingProtectionPrivateBrowsing = true
}
}
}
override fun setDefaultSettings(settings: GeckoEngineSettings) {
if(settings.javascriptEnabled != null) {
components.core.engineSettings.javascriptEnabled = settings.javascriptEnabled;
@@ -38,6 +56,8 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi {
eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.STRICT -> TrackingProtectionPolicy.strict()
eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.CUSTOM -> TODO()
}
updateFingerprintingProtection(settings.trackingProtectionPolicy)
}
if(settings.httpsOnlyMode != null) {
components.core.engineSettings.httpsOnlyMode = when(settings.httpsOnlyMode) {
@@ -89,13 +109,18 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi {
//First parse and set default values
setDefaultSettings(settings);
var reloadSession = false
//Then copy default settings into runtime
if(settings.javascriptEnabled != null) {
components.core.engine.settings.javascriptEnabled = components.core.engineSettings.javascriptEnabled
reloadSession = true
}
if(settings.trackingProtectionPolicy != null) {
components.useCases.settingsUseCases.updateTrackingProtection(components.core.engineSettings.trackingProtectionPolicy!!)
components.useCases.sessionUseCases.reload()
components.core.engine.settings.fingerprintingProtection = components.core.engineSettings.fingerprintingProtection
components.core.engine.settings.fingerprintingProtectionPrivateBrowsing = components.core.engineSettings.fingerprintingProtectionPrivateBrowsing
reloadSession = true
}
if(settings.httpsOnlyMode != null) {
components.core.engineSettings.httpsOnlyMode = components.core.engineSettings.httpsOnlyMode
@@ -105,12 +130,15 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi {
}
if(settings.preferredColorScheme != null) {
components.core.engineSettings.preferredColorScheme = components.core.engineSettings.preferredColorScheme
reloadSession = true
}
if(settings.cookieBannerHandlingMode != null) {
components.core.engineSettings.cookieBannerHandlingMode = components.core.engineSettings.cookieBannerHandlingMode
reloadSession = true
}
if(settings.cookieBannerHandlingModePrivateBrowsing != null) {
components.core.engineSettings.cookieBannerHandlingModePrivateBrowsing = components.core.engineSettings.cookieBannerHandlingModePrivateBrowsing
reloadSession = true
}
if(settings.cookieBannerHandlingGlobalRules != null) {
components.core.engineSettings.cookieBannerHandlingGlobalRules = components.core.engineSettings.cookieBannerHandlingGlobalRules
@@ -121,5 +149,9 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi {
if(settings.webContentIsolationStrategy != null) {
components.core.engineSettings.webContentIsolationStrategy = components.core.engineSettings.webContentIsolationStrategy
}
if(reloadSession) {
components.useCases.sessionUseCases.reload()
}
}
}
@@ -33,6 +33,7 @@ import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.LastMediaAccessState
import mozilla.components.browser.state.state.ReaderState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.state.recover.RecoverableTab
import mozilla.components.browser.state.state.recover.TabState
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
@@ -450,17 +451,33 @@ class GeckoTabsApiImpl : GeckoTabsApi {
}
}
override fun duplicateTab(selectTabId: String?, selectNewTab: Boolean): String {
override fun duplicateTab(
selectTabId: String?,
selectNewTab: Boolean,
newContextId: String?
): String {
try {
val tabState = selectTabId?.let { components.core.store.state.findTab(it) }
?: throw IllegalArgumentException("Tab not found")
return components.useCases.tabsUseCases.duplicateTab(
tab = tabState,
selectNewTab = selectNewTab
).also {
logger.debug("$TAG: Duplicated tab $selectTabId to new tab $it")
}
val duplicate = createTab(
url = tabState.content.url,
private = tabState.content.private,
contextId = newContextId,
parent = tabState,
engineSessionState = tabState.engineState.engineSessionState,
)
components.core.store.dispatch(
TabListAction.AddTabAction(
duplicate,
select = selectNewTab,
),
)
logger.debug("$TAG: Duplicated tab $selectTabId to new tab ${duplicate.id}")
return duplicate.id
} catch (e: Exception) {
logger.error("$TAG: Failed to duplicate tab", e)
throw e
@@ -8,6 +8,7 @@ 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.PermissionStorage
import eu.lensai.flutter_mozilla_components.services.MediaSessionService
import eu.lensai.flutter_mozilla_components.activities.NotificationActivity
import eu.lensai.flutter_mozilla_components.R
@@ -212,6 +213,8 @@ class Core(private val context: Context,
*/
val historyStorage by lazy { lazyHistoryStorage.value }
val permissionStorage by lazy { PermissionStorage( geckoSitePermissionsStorage ) }
/**
* Constructs a [TrackingProtectionPolicy] based on current preferences.
*
@@ -2017,7 +2017,7 @@ interface GeckoTabsApi {
fun selectOrAddTabByHistory(url: String, historyMetadata: HistoryMetadataKey): String
/** Selects an already existing tab displaying [url] or otherwise creates a new tab. */
fun selectOrAddTabByUrl(url: String, private: Boolean, source: SourceValue, flags: LoadUrlFlagsValue, ignoreFragment: Boolean): String
fun duplicateTab(selectTabId: String?, selectNewTab: Boolean): String
fun duplicateTab(selectTabId: String?, selectNewTab: Boolean, newContextId: String?): String
fun moveTabs(tabIds: List<String>, targetTabId: String, placeAfter: Boolean)
fun migratePrivateTabUseCase(tabId: String, alternativeUrl: String?): String
@@ -2287,8 +2287,9 @@ interface GeckoTabsApi {
val args = message as List<Any?>
val selectTabIdArg = args[0] as String?
val selectNewTabArg = args[1] as Boolean
val newContextIdArg = args[2] as String?
val wrapped: List<Any?> = try {
listOf(api.duplicateTab(selectTabIdArg, selectNewTabArg))
listOf(api.duplicateTab(selectTabIdArg, selectNewTabArg, newContextIdArg))
} catch (exception: Throwable) {
wrapError(exception)
}
@@ -3213,3 +3214,126 @@ class GeckoTabContentEvents(private val binaryMessenger: BinaryMessenger, privat
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoDeleteBrowsingDataController {
fun deleteTabs(callback: (Result<Unit>) -> Unit)
fun deleteBrowsingHistory(callback: (Result<Unit>) -> Unit)
fun deleteCookiesAndSiteData(callback: (Result<Unit>) -> Unit)
fun deleteCachedFiles(callback: (Result<Unit>) -> Unit)
fun deleteSitePermissions(callback: (Result<Unit>) -> Unit)
fun deleteDownloads(callback: (Result<Unit>) -> Unit)
companion object {
/** The codec used by GeckoDeleteBrowsingDataController. */
val codec: MessageCodec<Any?> by lazy {
GeckoPigeonCodec()
}
/** Sets up an instance of `GeckoDeleteBrowsingDataController` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoDeleteBrowsingDataController?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteTabs$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.deleteTabs{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
reply.reply(wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteBrowsingHistory$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.deleteBrowsingHistory{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
reply.reply(wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteCookiesAndSiteData$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.deleteCookiesAndSiteData{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
reply.reply(wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteCachedFiles$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.deleteCachedFiles{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
reply.reply(wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteSitePermissions$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.deleteSitePermissions{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
reply.reply(wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteDownloads$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.deleteDownloads{ result: Result<Unit> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(wrapError(error))
} else {
reply.reply(wrapResult(null))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}