migrate mozilla components into project

This commit is contained in:
Fabian Freund
2024-09-07 08:49:38 +02:00
parent 10a49def87
commit 3b047b5d8a
67 changed files with 5840 additions and 0 deletions
@@ -0,0 +1,31 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
build/
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "6fe09872b12acb5747d6e01e84987120cabf31df"
channel: "master"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 6fe09872b12acb5747d6e01e84987120cabf31df
base_revision: 6fe09872b12acb5747d6e01e84987120cabf31df
- platform: android
create_revision: 6fe09872b12acb5747d6e01e84987120cabf31df
base_revision: 6fe09872b12acb5747d6e01e84987120cabf31df
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.
@@ -0,0 +1 @@
TODO: Add your license here.
@@ -0,0 +1,15 @@
# flutter_mozilla_components
A new Flutter plugin project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx
@@ -0,0 +1,112 @@
group = "eu.lensai.flutter_mozilla_components"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "1.9.20"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.6.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
maven {
url "https://maven.mozilla.org/maven2/"
}
}
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
if (project.android.hasProperty("namespace")) {
namespace = "eu.lensai.flutter_mozilla_components"
}
compileSdk = 35
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
sourceSets {
main.java.srcDirs += "src/main/kotlin"
test.java.srcDirs += "src/test/kotlin"
}
defaultConfig {
minSdk = 21
}
buildFeatures {
viewBinding true
}
dependencies {
implementation 'org.mozilla.components:support-utils:131.0b1'
implementation 'org.mozilla.components:support-ktx:131.0b1'
implementation 'org.mozilla.components:support-webextensions:131.0b1'
implementation 'org.mozilla.components:support-locale:131.0b1'
implementation 'org.mozilla.components:lib-fetch-httpurlconnection:131.0b1'
implementation 'org.mozilla.components:lib-crash:131.0b1'
implementation 'org.mozilla.components:lib-publicsuffixlist:131.0b1'
implementation 'org.mozilla.components:lib-dataprotect:131.0b1'
implementation 'org.mozilla.components:concept-engine:131.0b1'
implementation 'org.mozilla.components:concept-fetch:131.0b1'
implementation 'org.mozilla.components:browser-engine-gecko:131.0b1'
implementation 'org.mozilla.components:browser-state:131.0b1'
implementation 'org.mozilla.components:browser-session-storage:131.0b1'
implementation 'org.mozilla.components:browser-icons:131.0b1'
//implementation 'org.mozilla.components:browser-storage-sync:131.0b1'
implementation 'org.mozilla.components:browser-thumbnails:131.0b1'
implementation 'org.mozilla.components:feature-addons:131.0b1'
implementation 'org.mozilla.components:feature-app-links:131.0b1'
implementation 'org.mozilla.components:feature-autofill:131.0b1'
implementation 'org.mozilla.components:feature-downloads:131.0b1'
implementation 'org.mozilla.components:feature-media:131.0b1'
implementation 'org.mozilla.components:feature-tabs:131.0b1'
implementation 'org.mozilla.components:feature-prompts:131.0b1'
implementation 'org.mozilla.components:feature-session:131.0b1'
implementation 'org.mozilla.components:feature-readerview:131.0b1'
implementation 'org.mozilla.components:feature-privatemode:131.0b1'
implementation 'org.mozilla.components:feature-sitepermissions:131.0b1'
implementation 'org.mozilla.components:feature-webcompat:131.0b1'
implementation 'org.mozilla.components:feature-webnotifications:131.0b1'
implementation 'org.mozilla.components:service-digitalassetlinks:131.0b1'
implementation 'org.mozilla.components:ui-widgets:131.0b1'
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.1'
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
}
testOptions {
unitTests.all {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}
@@ -0,0 +1 @@
rootProject.name = 'flutter_mozilla_components'
@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.lensai.flutter_mozilla_components">
</manifest>
@@ -0,0 +1,64 @@
package eu.lensai.flutter_mozilla_components
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
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 {
override fun onLoadRequest(
engineSession: EngineSession,
uri: String,
lastUri: String?,
hasUserGesture: Boolean,
isSameDomain: Boolean,
isRedirect: Boolean,
isDirectNavigation: Boolean,
isSubframeRequest: Boolean,
): RequestInterceptor.InterceptionResponse? {
val components = GlobalComponents.components!!
return when (uri) {
// "about:privatebrowsing" -> {
// val page = PrivatePage.createPrivateBrowsingPage(context, uri)
// RequestInterceptor.InterceptionResponse.Content(page, encoding = "base64")
// }
// "about:crashes" -> {
// val intent = Intent(context, CrashListActivity::class.java)
// intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
// context.startActivity(intent)
//
// RequestInterceptor.InterceptionResponse.Url("about:blank")
// }
else -> {
components.appLinksInterceptor.onLoadRequest(
engineSession,
uri,
lastUri,
hasUserGesture,
isSameDomain,
isRedirect,
isDirectNavigation,
isSubframeRequest,
)
}
}
}
override fun onErrorRequest(
session: EngineSession,
errorType: ErrorType,
uri: String?,
): RequestInterceptor.ErrorResponse {
val errorPage = ErrorPages.createUrlEncodedErrorPage(context, errorType, uri)
return RequestInterceptor.ErrorResponse(errorPage)
}
override fun interceptsAppInitiatedRequests() = true
}
@@ -0,0 +1,273 @@
package eu.lensai.flutter_mozilla_components
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.annotation.CallSuper
import androidx.fragment.app.Fragment
import eu.lensai.flutter_mozilla_components.databinding.FragmentBrowserBinding
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.privatemode.feature.SecureWindowFeature
import mozilla.components.feature.prompts.PromptFeature
import mozilla.components.feature.session.SessionFeature
import mozilla.components.feature.session.SwipeRefreshFeature
import mozilla.components.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.feature.sitepermissions.SitePermissionsRules
import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
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.locale.ActivityContextWrapper
import mozilla.components.support.utils.ext.requestInPlacePermissions
/**
* Base fragment extended by [BrowserFragment] and [ExternalAppBrowserFragment].
* This class only contains shared code focused on the main browsing content.
* UI code specific to the app or to custom tabs can be found in the subclasses.
*/
@SuppressWarnings("LargeClass")
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ActivityResultHandler {
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
private val promptFeature = ViewBoundFeatureWrapper<PromptFeature>()
private val sitePermissionsFeature = ViewBoundFeatureWrapper<SitePermissionsFeature>()
private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
protected val sessionId: String?
get() = arguments?.getString(SESSION_ID_KEY)
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
@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)
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)
sessionFeature.set(
feature = SessionFeature(
components.store,
components.sessionUseCases.goBack,
components.sessionUseCases.goForward,
engineView,
sessionId,
),
owner = this,
view = binding.root,
)
swipeRefreshFeature.set(
feature = SwipeRefreshFeature(
components.store,
components.sessionUseCases.reload,
binding.swipeToRefresh,
),
owner = this,
view = binding.root,
)
downloadsFeature.set(
feature = DownloadsFeature(
requireContext().applicationContext,
store = components.store,
useCases = components.downloadsUseCases,
fragmentManager = childFragmentManager,
onDownloadStopped = { download, id, status ->
Logger.debug("Download done. ID#$id $download with status $status")
},
downloadManager = FetchDownloadManager(
requireContext().applicationContext,
components.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(),
)
}
},
),
owner = this,
view = binding.root,
)
appLinksFeature.set(
feature = AppLinksFeature(
context = requireContext(),
store = components.store,
sessionId = sessionId,
fragmentManager = parentFragmentManager,
launchInApp = { components.preferences.getBoolean(DefaultComponents.PREF_LAUNCH_EXTERNAL_APP, false) },
loadUrlUseCase = components.sessionUseCases.loadUrl,
),
owner = this,
view = binding.root,
)
promptFeature.set(
feature = PromptFeature(
fragment = this,
store = components.store,
customTabId = sessionId,
tabsUseCases = components.tabsUseCases,
fragmentManager = parentFragmentManager,
fileUploadsDirCleaner = components.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(),
)
}
},
),
owner = this,
view = binding.root,
)
sitePermissionsFeature.set(
feature = SitePermissionsFeature(
context = requireContext(),
sessionId = sessionId,
storage = components.permissionStorage,
fragmentManager = parentFragmentManager,
sitePermissionsRules = SitePermissionsRules(
autoplayAudible = AutoplayAction.BLOCKED,
autoplayInaudible = AutoplayAction.BLOCKED,
camera = SitePermissionsRules.Action.ASK_TO_ALLOW,
location = SitePermissionsRules.Action.ASK_TO_ALLOW,
notification = SitePermissionsRules.Action.ASK_TO_ALLOW,
microphone = SitePermissionsRules.Action.ASK_TO_ALLOW,
persistentStorage = SitePermissionsRules.Action.ASK_TO_ALLOW,
mediaKeySystemAccess = SitePermissionsRules.Action.ASK_TO_ALLOW,
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(),
)
}
},
onShouldShowRequestPermissionRationale = { shouldShowRequestPermissionRationale(it) },
store = components.store,
),
owner = this,
view = binding.root,
)
val secureWindowFeature = SecureWindowFeature(
window = requireActivity().window,
store = components.store,
customTabId = sessionId,
)
// Observe the lifecycle for supported features
lifecycle.addObservers(
secureWindowFeature,
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// consumeFlow(components.store) { flow ->
// flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(sessionId) }
// .ifAnyChanged { tab ->
// arrayOf(
// tab.content.loading,
// tab.content.canGoBack,
// tab.content.canGoForward,
// )
// }
// .collect {
// binding.toolbar.invalidateActions()
// }
// }
}
@CallSuper
override fun onBackPressed(): Boolean =
listOf(sessionFeature).any { it.onBackPressed() }
@CallSuper
override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
return activityResultHandler.any { it.onActivityResult(requestCode, data, resultCode) }
}
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)
}
}
override fun onDestroyView() {
super.onDestroyView()
engineView.setActivityContext(null)
_binding = null
}
}
@@ -0,0 +1,102 @@
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 android.widget.FrameLayout
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
/**
* Fragment used for browsing the web within the main app.
*/
class BrowserFragment(private val context: Context) : BaseBrowserFragment(), UserInteractionHandler {
private val thumbnailsFeature = ViewBoundFeatureWrapper<BrowserThumbnails>()
private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
private val mediaSessionFullscreenFeature =
ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
override fun createEngine(components: Components): EngineView {
return components.engine.createView(context).apply {
// selectionActionDelegate = DefaultSelectionActionDelegate(
// store = components.store,
// context = context,
// )
}
}
@Suppress("LongMethod")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
super.onCreateView(inflater, container, savedInstanceState)
val binding = super.binding
fullScreenFeature.set(
feature = FullScreenFeature(
components.store,
components.sessionUseCases,
sessionId,
) { inFullScreen ->
if (inFullScreen) {
activity?.enterImmersiveMode()
} else {
activity?.exitImmersiveMode()
}
},
owner = this,
view = binding.root,
)
mediaSessionFullscreenFeature.set(
feature = MediaSessionFullscreenFeature(
requireActivity(),
components.store,
sessionId,
),
owner = this,
view = binding.root,
)
thumbnailsFeature.set(
feature = BrowserThumbnails(requireContext(), engineView, components.store),
owner = this,
view = binding.root,
)
val windowFeature = WindowFeature(components.store, components.tabsUseCases)
lifecycle.addObserver(windowFeature)
return binding.root
}
override fun onBackPressed(): Boolean {
return when {
fullScreenFeature.onBackPressed() -> true
else -> super.onBackPressed()
}
}
companion object {
fun create(context: Context, sessionId: String? = null) = BrowserFragment(context).apply {
arguments = Bundle().apply {
putSessionId(sessionId)
}
}
}
}
@@ -0,0 +1,255 @@
package eu.lensai.flutter_mozilla_components
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import eu.lensai.flutter_mozilla_components.middleware.FlutterEventMiddleware
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
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.selector.selectedTab
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.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.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) {
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"
}
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 }
}
val store by lazy {
BrowserStore(
middleware = listOf(
FlutterEventMiddleware(),
DownloadMiddleware(applicationContext, DownloadService::class.java),
ReaderViewMiddleware(),
ThumbnailsMiddleware(thumbnailStorage),
UndoMiddleware(),
RecordingDevicesMiddleware(applicationContext, notificationsDelegate),
LastAccessMiddleware(),
PromptMiddleware(),
SessionPrioritizationMiddleware(),
) + EngineMiddleware.create(engine),
).apply {
this.flowScoped { flow ->
flow.mapNotNull { state -> state.selectedTab }
.distinctUntilChangedBy {
it.content.url
}
.collect { tab ->
Log.d("URL_CHANGE", tab.content.url)
}
}
this.flowScoped { flow ->
flow.mapNotNull { state -> state.tabs }
.filterChanged {
it.content
}
.ifAnyChanged { arrayOf(it.content.url) }
.collect { tab ->
Log.d("URL_CHANGE2", tab.content.url)
}
}
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) }
// 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)
}
}
@@ -0,0 +1,11 @@
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,136 @@
package eu.lensai.flutter_mozilla_components
import GeckoBrowserApi
import GeckoSessionApi
import GeckoTabsApi
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.annotation.NonNull
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoSessionApiImpl
import eu.lensai.flutter_mozilla_components.api.GeckoTabsApiImpl
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
import mozilla.components.concept.engine.Engine
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.logger.Logger
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) : DefaultComponents(applicationContext) {
private val runtime by lazy {
// Allow for exfiltrating Gecko metrics through the Glean SDK.
val builder = GeckoRuntimeSettings.Builder().aboutConfigEnabled(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)
//WebCompatReporterFeature.install(it)
}
}
override val client by lazy { GeckoViewFetchClient(applicationContext, runtime) }
}
/** FlutterMozillaComponentsPlugin */
class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware {
private var activity: Activity? = null
private lateinit var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding;
init {
Log.addSink(AndroidLogSink())
}
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
_flutterPluginBinding = flutterPluginBinding
GlobalComponents.setUp(flutterPluginBinding.applicationContext)
val intent = Intent(flutterPluginBinding.applicationContext, NotificationActivity::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
flutterPluginBinding.applicationContext.startActivity(intent)
GeckoBrowserApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserApiImpl {
showNativeFragment()
})
GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl())
GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl())
}
private fun showNativeFragment() {
if (activity == null) {
//result.error("ACTIVITY_NOT_ATTACHED", "Activity is not attached", null)
return
}
// Replace this with your actual Fragment
val nativeFragment = BrowserFragment.create(activity!!)
val fm = (activity as FragmentActivity).supportFragmentManager
fm.beginTransaction()
.replace(FRAGMENT_CONTAINER_ID, nativeFragment)
.commitAllowingStateLoss()
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
this.activity = binding.activity
_flutterPluginBinding.platformViewRegistry.registerViewFactory(
"eu.lensai/gecko", GeckoViewFactory(binding.activity, FRAGMENT_CONTAINER_ID))
}
override fun onDetachedFromActivityForConfigChanges() {
onDetachedFromActivity()
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
onAttachedToActivity(binding)
}
override fun onDetachedFromActivity() {
this.activity = null
}
companion object {
private const val FRAGMENT_CONTAINER_ID = 0xBEEF
}
}
@@ -0,0 +1,42 @@
package eu.lensai.flutter_mozilla_components
import android.view.View
import android.app.Activity
import android.content.Context
import android.view.ViewGroup
import android.widget.FrameLayout
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(
StandardMessageCodec.INSTANCE) {
override fun create(context: Context?, id: Int, args: Any?): PlatformView {
return NativeFragmentView(this.activity, this.containerId)
}
}
private class NativeFragmentView(
private val activity: Activity?,
private val containerId: Int
) : PlatformView {
private val container: View
init {
val vParams: ViewGroup.LayoutParams =
FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
)
container = FrameLayout(activity!!)
container.layoutParams = vParams
container.id = containerId
}
override fun getView(): View {
return container
}
override fun dispose() {
// Clean up if needed
}
}
@@ -0,0 +1,78 @@
package eu.lensai.flutter_mozilla_components
import android.content.Context
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
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.logger.Logger
import mozilla.components.support.webextensions.WebExtensionSupport
import java.util.concurrent.TimeUnit
object GlobalComponents {
private var _components: Components? = null
val components: Components?
get() = _components
@DelicateCoroutinesApi
private fun restoreBrowserState(newComponents: Components) = GlobalScope.launch(Dispatchers.Main) {
newComponents.tabsUseCases.restore(newComponents.sessionStorage)
newComponents.sessionStorage.autoSave(newComponents.store)
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
.whenGoingToBackground()
.whenSessionsChange()
}
fun setUp(applicationContext: Context) {
val newComponents = Components(applicationContext)
newComponents.crashReporter.install(applicationContext)
Facts.registerProcessor(LogFactProcessor())
newComponents.engine.warmUp()
restoreBrowserState(newComponents)
newComponents.downloadsUseCases.restoreDownloads()
try {
GlobalAddonDependencyProvider.initialize(
newComponents.addonManager,
newComponents.addonUpdater,
)
WebExtensionSupport.initialize(
newComponents.engine,
newComponents.store,
onNewTabOverride = {
_, engineSession, url ->
newComponents.tabsUseCases.addTab(url, selectTab = true, engineSession = engineSession)
},
onCloseTabOverride = {
_, sessionId ->
newComponents.tabsUseCases.removeTab(sessionId)
},
onSelectTabOverride = {
_, sessionId ->
newComponents.tabsUseCases.selectTab(sessionId)
},
onUpdatePermissionRequest = newComponents.addonUpdater::onUpdatePermissionRequest,
onExtensionsLoaded = { extensions ->
newComponents.addonUpdater.registerForFutureUpdates(extensions)
newComponents.supportedAddonsChecker.registerForChecks()
},
)
} catch (e: UnsupportedOperationException) {
// Web extension support is only available for engine gecko
Logger.error("Failed to initialize web extension support", e)
}
newComponents.tabsUseCases.addTab.invoke("https://google.com", selectTab = true)
_components = newComponents
}
}
@@ -0,0 +1,2 @@
package eu.lensai.flutter_mozilla_components
@@ -0,0 +1,15 @@
package eu.lensai.flutter_mozilla_components
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.feature.media.service.AbstractMediaSessionService
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 }
}
@@ -0,0 +1,15 @@
package eu.lensai.flutter_mozilla_components
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import eu.lensai.flutter_mozilla_components.GlobalComponents
class NotificationActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GlobalComponents.components!!.notificationsDelegate.bindToActivity(this)
finish()
}
}
@@ -0,0 +1,11 @@
package eu.lensai.flutter_mozilla_components.api
import GeckoBrowserApi
class GeckoBrowserApiImpl(
private val showFragmentCallback: () -> Unit
) : GeckoBrowserApi {
override fun showNativeFragment() {
showFragmentCallback.invoke();
}
}
@@ -0,0 +1,137 @@
package eu.lensai.flutter_mozilla_components.api
import GeckoSessionApi
import LoadUrlFlagsValue
import android.util.Log
import eu.lensai.flutter_mozilla_components.GlobalComponents
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.translate.TranslationOptions
import TranslationOptions as PigeonTranslationOptions
import mozilla.components.feature.session.SessionUseCases
class GeckoSessionApiImpl() : GeckoSessionApi {
private val sessionUseCases: SessionUseCases by lazy { GlobalComponents.components!!.sessionUseCases }
private val state: BrowserState by lazy { GlobalComponents.components!!.store.state }
override fun loadUrl(
tabId: String?,
url: String,
flags: LoadUrlFlagsValue,
additionalHeaders: Map<String, String>?
) {
sessionUseCases.loadUrl(
url = url,
sessionId = tabId,
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()),
additionalHeaders = additionalHeaders
)
}
override fun loadData(tabId: String?, data: String, mimeType: String, encoding: String) {
sessionUseCases.loadData(
data = data,
tabId = tabId ?: state.selectedTabId,
mimeType = mimeType,
encoding = encoding,
)
}
override fun reload(tabId: String?, flags: LoadUrlFlagsValue) {
sessionUseCases.reload(
tabId = tabId ?: state.selectedTabId,
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()),
)
}
override fun stopLoading(tabId: String?) {
sessionUseCases.stopLoading(
tabId = tabId
)
}
override fun goBack(tabId: String?, userInteraction: Boolean) {
sessionUseCases.goBack(
tabId = tabId ?: state.selectedTabId,
userInteraction = userInteraction
)
}
override fun goForward(tabId: String?, userInteraction: Boolean) {
sessionUseCases.goForward(
tabId = tabId ?: state.selectedTabId,
userInteraction = userInteraction
)
}
override fun goToHistoryIndex(index: Long, tabId: String?) {
sessionUseCases.goToHistoryIndex(
tabId = tabId ?: state.selectedTabId,
index = index.toInt()
)
}
override fun requestDesktopSite(tabId: String?, enable: Boolean) {
sessionUseCases.requestDesktopSite(
tabId = tabId ?: state.selectedTabId,
enable = enable,
)
}
override fun exitFullscreen(tabId: String?) {
sessionUseCases.exitFullscreen(
tabId = tabId ?: state.selectedTabId,
)
}
override fun saveToPdf(tabId: String?) {
sessionUseCases.saveToPdf(
tabId = tabId ?: state.selectedTabId,
)
}
override fun printContent(tabId: String?) {
sessionUseCases.printContent(
tabId = tabId ?: state.selectedTabId,
)
}
override fun translate(
tabId: String?,
fromLanguage: String,
toLanguage: String,
options: PigeonTranslationOptions?
) {
sessionUseCases.translate(
tabId = tabId ?: state.selectedTabId,
fromLanguage = fromLanguage,
toLanguage = toLanguage,
options = if (options != null) TranslationOptions(downloadModel = options.downloadModel) else null,
)
}
override fun translateRestore(tabId: String?) {
sessionUseCases.translateRestore(
tabId = tabId ?: state.selectedTabId,
)
}
override fun crashRecovery(tabIds: List<String>?) {
if(tabIds != null) {
sessionUseCases.crashRecovery.invoke(tabIds = tabIds)
} else {
sessionUseCases.crashRecovery.invoke()
}
}
override fun purgeHistory() {
sessionUseCases.purgeHistory()
}
override fun updateLastAccess(tabId: String?, lastAccess: Long?) {
sessionUseCases.updateLastAccess(
tabId = tabId ?: state.selectedTabId,
lastAccess = lastAccess ?: System.currentTimeMillis()
)
}
}
@@ -0,0 +1,221 @@
package eu.lensai.flutter_mozilla_components.api
import GeckoTabsApi
import HistoryMetadataKey as PigeonHistoryMetadataKey
import LoadUrlFlagsValue
import RecoverableBrowserState as PigeonRecoverableBrowserState
import RestoreLocation as PigeonRestoreLocation
import RecoverableTab as PigeonRecoverableTab
import SourceValue
import eu.lensai.flutter_mozilla_components.GlobalComponents
import mozilla.components.browser.session.storage.RecoverableBrowserState
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab
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.recover.RecoverableTab
import mozilla.components.browser.state.state.recover.TabState
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
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 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 {
return RecoverableTab(
engineSessionState = if(t.engineSessionStateJson != null)
engine.createSessionState(JSONObject(t.engineSessionStateJson))
else
null,
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,
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(),
),
lastAccess = t.state.lastAccess,
createdAt = t.state.createdAt,
lastMediaAccessState = LastMediaAccessState(
lastMediaUrl = t.state.lastMediaAccessState.lastMediaUrl,
lastMediaAccess = t.state.lastMediaAccessState.lastMediaAccess,
mediaSessionActive = t.state.lastMediaAccessState.mediaSessionActive
),
private = t.state.private,
historyMetadata = if(t.state.historyMetadata != null)
HistoryMetadataKey(
url = t.state.historyMetadata.url,
searchTerm = t.state.historyMetadata.searchTerm,
referrerUrl = t.state.historyMetadata.referrerUrl
)
else null,
source = restoreSource(t.state.source),
index = t.state.index.toInt(),
hasFormData = t.state.hasFormData,
)
)
}
private fun mapRestoreLocation(t: PigeonRestoreLocation) : TabListAction.RestoreAction.RestoreLocation {
return TabListAction.RestoreAction.RestoreLocation.entries.first { it.ordinal == t.ordinal }
}
override fun selectTab(tabId: String) {
tabsUseCases.selectTab(tabId = tabId)
}
override fun removeTab(tabId: String) {
tabsUseCases.removeTab(tabId = tabId)
}
override fun addTab(
url: String,
selectTab: Boolean,
startLoading: Boolean,
parentId: String?,
flags: LoadUrlFlagsValue,
contextId: String?,
source: SourceValue,
private: Boolean,
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
)
}
override fun removeAllTabs(recoverable: Boolean) {
tabsUseCases.removeAllTabs(recoverable = recoverable)
}
override fun removeTabs(ids: List<String>) {
tabsUseCases.removeTabs(ids = ids)
}
override fun removeNormalTabs() {
tabsUseCases.removeNormalTabs()
}
override fun removePrivateTabs() {
tabsUseCases.removePrivateTabs()
}
override fun undo() {
tabsUseCases.undo()
}
override fun restoreTabsByList(
tabs: List<PigeonRecoverableTab>,
selectTabId: String?,
restoreLocation: PigeonRestoreLocation
) {
tabsUseCases.restore(
tabs = tabs.map { t -> mapTab(t) },
restoreLocation = mapRestoreLocation(restoreLocation),
selectTabId = selectTabId,
)
}
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),
)
}
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 selectOrAddTabByUrl(
url: String,
private: Boolean,
source: SourceValue,
flags: LoadUrlFlagsValue,
ignoreFragment: Boolean
): String {
return tabsUseCases.selectOrAddTab(
url = url,
private = private,
source = restoreSource(source),
flags = EngineSession.LoadUrlFlags.select(flags.value.toInt()),
ignoreFragment = ignoreFragment,
)
}
override fun duplicateTab(selectTabId: String?, selectNewTab: Boolean): String {
val tabState = if(selectTabId != null) state.findTab(selectTabId) else null
return tabsUseCases.duplicateTab(
tab = tabState!!,
selectNewTab = selectNewTab,
)
}
override fun moveTabs(tabIds: List<String>, targetTabId: String, placeAfter: Boolean) {
tabsUseCases.moveTabs(
tabIds = tabIds,
targetTabId = targetTabId,
placeAfter = placeAfter,
)
}
override fun migratePrivateTabUseCase(tabId: String, alternativeUrl: String?): String {
return tabsUseCases.migratePrivateTabUseCase(
tabId = tabId,
alternativeUrl = alternativeUrl
)
}
}
@@ -0,0 +1,37 @@
/* 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.middleware
import android.util.Log
import mozilla.components.browser.state.action.BrowserAction
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
/**
* [Middleware] implementation for handling [ContentAction.UpdateThumbnailAction] and storing
* the thumbnail to the disk cache.
*/
class FlutterEventMiddleware() : Middleware<BrowserState, BrowserAction> {
@Suppress("ComplexMethod")
override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit,
action: BrowserAction,
) {
when (action) {
is ContentAction.UpdateThumbnailAction -> {
Log.d("UpdateThumbnailAction", action.sessionId)
}
is ContentAction.UpdateTitleAction -> {
Log.d("UpdateTitleAction", action.title)
}
else -> {
// no-op
}
}
next(action)
}
}
@@ -0,0 +1,19 @@
<!-- 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/. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
android:tint="#FFFFFF">
<group android:scaleX="2.9232"
android:scaleY="2.9232"
android:translateX="18.9216"
android:translateY="18.9216">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/>
</group>
</vector>
@@ -0,0 +1,25 @@
<?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/. -->
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<mozilla.components.ui.widgets.VerticalSwipeRefreshLayout
android:id="@+id/swipeToRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Inject engine here -->
</mozilla.components.ui.widgets.VerticalSwipeRefreshLayout>
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.AppCompat.Translucent" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowNoTitle">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
</style>
</resources>
@@ -0,0 +1,27 @@
package eu.lensai.flutter_mozilla_components
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlin.test.Test
import org.mockito.Mockito
/*
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
*
* Once you have built the plugin's example app, you can run these tests from the command
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
* you can run them directly from IDEs that support JUnit such as Android Studio.
*/
internal class FlutterMozillaComponentsPluginTest {
@Test
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
val plugin = FlutterMozillaComponentsPlugin()
val call = MethodCall("getPlatformVersion", null)
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
plugin.onMethodCall(call, mockResult)
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
}
}
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
@@ -0,0 +1,16 @@
# flutter_mozilla_components_example
Demonstrates how to use the flutter_mozilla_components plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
@@ -0,0 +1,44 @@
plugins {
id "com.android.application"
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "eu.lensai.flutter_mozilla_components_example"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "eu.lensai.flutter_mozilla_components_example"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,93 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- This is needed because the android.permission.CAMERA above automatically
adds a requirements for camera hardware and we don't want add those restrictions -->
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Needed for uploading media files on devices with Android 13 and later. -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<application
android:label="flutter_mozilla_components_example"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity android:name="eu.lensai.flutter_mozilla_components.NotificationActivity" android:theme="@style/Theme.AppCompat.Translucent" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<service
android:name="eu.lensai.flutter_mozilla_components.DownloadService"
android:foregroundServiceType="dataSync" />
<service android:name="eu.lensai.flutter_mozilla_components.MediaSessionService"
android:foregroundServiceType="mediaPlayback"
android:exported="false" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package eu.lensai.flutter_mozilla_components_example
import io.flutter.embedding.android.FlutterFragmentActivity
class MainActivity: FlutterFragmentActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,18 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
@@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.9.20" apply false
}
include ":app"
@@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Native Fragment Example'),
actions: [
MenuAnchor(
builder: (context, controller, child) => IconButton(
onPressed: () {
controller.open();
},
icon: const Icon(Icons.menu)),
menuChildren: [
MenuItemButton(
onPressed: () async {
await GeckoSessionService.forCurrentTab().reload();
},
child: const Text('Reload'),
),
MenuItemButton(
onPressed: () async {
await GeckoSessionService.forCurrentTab().goBack();
},
child: const Text('Back'),
)
])
],
),
body: const SafeArea(
child: Center(
child: GeckoView(),
),
),
),
);
}
}
@@ -0,0 +1,85 @@
name: flutter_mozilla_components_example
description: "Demonstrates how to use the flutter_mozilla_components plugin."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: ^3.6.0-198.0.dev
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
flutter_mozilla_components:
# When depending on this package from a real application you should use:
# flutter_mozilla_components: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^4.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
@@ -0,0 +1,4 @@
# melos_managed_dependency_overrides: flutter_mozilla_components
dependency_overrides:
flutter_mozilla_components:
path: ..
@@ -0,0 +1,27 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_mozilla_components_example/main.dart';
void main() {
testWidgets('Verify Platform version', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that platform version is retrieved.
expect(
find.byWidgetPredicate(
(Widget widget) => widget is Text &&
widget.data!.startsWith('Running on:'),
),
findsOneWidget,
);
});
}
@@ -0,0 +1,4 @@
export 'src/geckoview_widget.dart';
//export 'src/domain/services/gecko_browser.dart';
export 'src/domain/services/gecko_session.dart';
export 'src/domain/services/gecko_tabs.dart';
@@ -0,0 +1,46 @@
// ignore_for_file: constant_identifier_names, non_constant_identifier_names
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
/// Describes a combination of flags provided to the engine when loading a URL.
class LoadUrlFlags {
final int value;
const LoadUrlFlags._(this.value);
LoadUrlFlagsValue toValue() {
return LoadUrlFlagsValue(value: value);
}
static const LoadUrlFlags NONE = LoadUrlFlags._(0);
static const LoadUrlFlags BYPASS_CACHE = LoadUrlFlags._(1 << 0);
static const LoadUrlFlags BYPASS_PROXY = LoadUrlFlags._(1 << 1);
static const LoadUrlFlags EXTERNAL = LoadUrlFlags._(1 << 2);
static const LoadUrlFlags ALLOW_POPUPS = LoadUrlFlags._(1 << 3);
static const LoadUrlFlags BYPASS_CLASSIFIER = LoadUrlFlags._(1 << 4);
static const LoadUrlFlags LOAD_FLAGS_FORCE_ALLOW_DATA_URI =
LoadUrlFlags._(1 << 5);
static const LoadUrlFlags LOAD_FLAGS_REPLACE_HISTORY = LoadUrlFlags._(1 << 6);
static const LoadUrlFlags LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE =
LoadUrlFlags._(1 << 7);
static const LoadUrlFlags ALLOW_ADDITIONAL_HEADERS = LoadUrlFlags._(1 << 15);
static const LoadUrlFlags ALLOW_JAVASCRIPT_URL = LoadUrlFlags._(1 << 16);
static LoadUrlFlags ALL = LoadUrlFlags._(
BYPASS_CACHE.value |
BYPASS_PROXY.value |
EXTERNAL.value |
ALLOW_POPUPS.value |
BYPASS_CLASSIFIER.value |
LOAD_FLAGS_FORCE_ALLOW_DATA_URI.value |
LOAD_FLAGS_REPLACE_HISTORY.value |
LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE.value |
ALLOW_ADDITIONAL_HEADERS.value |
ALLOW_JAVASCRIPT_URL.value,
);
static LoadUrlFlags select(List<LoadUrlFlags> flags) {
int combinedValue = flags.fold(0, (sum, flag) => sum | flag.value);
return LoadUrlFlags._(combinedValue);
}
}
@@ -0,0 +1,168 @@
import 'package:flutter_mozilla_components/src/helper/helper.dart';
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
/// Describes a category of an external package.
enum PackageCategory {
unknown(-1),
game(0),
audio(1),
video(2),
image(3),
social(4),
news(5),
maps(6),
productivity(7);
final int id;
const PackageCategory(this.id);
PackageCategoryValue toValue() {
return PackageCategoryValue(value: id);
}
}
/// Represents the origin of a session to describe how and why it was created.
abstract class Source {
final int id;
const Source(this.id);
SourceValue toValue();
/// Initializes a [Source] of a correct type from its component properties.
/// Intended use is for restoring persisted state.
static Source restore(
int? sourceId, String? packageId, int? packageCategory) {
final caller = packageId != null
? ExternalPackage(
packageId: packageId,
category: packageCategoryfromInt(packageCategory).toValue())
: null;
switch (sourceId) {
case 1:
return External.actionSend(caller);
case 2:
return External.actionView(caller);
case 3:
return External.actionSearch(caller);
case 4:
return External.customTab(caller);
case 5:
return Internal.homeScreen;
case 6:
return Internal.menu;
case 7:
return Internal.newTab;
case 8:
return Internal.none;
case 9:
return Internal.textSelection;
case 10:
return Internal.userEntered;
case 11:
return Internal.customTab;
default:
return Internal.none;
}
}
}
/// Describes sessions of external origins, i.e. from outside of the application.
abstract class External extends Source {
final ExternalPackage? caller;
const External(super.id, this.caller);
@override
SourceValue toValue() {
return SourceValue(id: id, caller: caller);
}
/// Created to handle an ACTION_SEND (share) intent.
factory External.actionSend(ExternalPackage? caller) = ActionSend;
/// Created to handle an ACTION_VIEW intent.
factory External.actionView(ExternalPackage? caller) = ActionView;
/// Created to handle an ACTION_SEARCH and ACTION_WEB_SEARCH intent.
factory External.actionSearch(ExternalPackage? caller) = ActionSearch;
/// Created to handle a CustomTabs intent of external origin.
factory External.customTab(ExternalPackage? caller) = CustomTab;
}
class ActionSend extends External {
const ActionSend(ExternalPackage? caller) : super(1, caller);
}
class ActionView extends External {
const ActionView(ExternalPackage? caller) : super(2, caller);
}
class ActionSearch extends External {
const ActionSearch(ExternalPackage? caller) : super(3, caller);
}
class CustomTab extends External {
const CustomTab(ExternalPackage? caller) : super(4, caller);
}
/// Describes sessions of internal origin, i.e. from within of the application.
abstract class Internal extends Source {
const Internal(super.id);
@override
SourceValue toValue() {
return SourceValue(id: id);
}
/// User interacted with the home screen.
static const homeScreen = _HomeScreen();
/// User interacted with a menu.
static const menu = _Menu();
/// User opened a new tab.
static const newTab = _NewTab();
/// Default value and for testing purposes.
static const none = _None();
/// User selected text.
static const textSelection = _TextSelection();
/// User entered a URL or search term.
static const userEntered = _UserEntered();
/// Created to handle a CustomTabs intent of internal origin.
static const customTab = _CustomTab();
}
class _HomeScreen extends Internal {
const _HomeScreen() : super(5);
}
class _Menu extends Internal {
const _Menu() : super(6);
}
class _NewTab extends Internal {
const _NewTab() : super(7);
}
class _None extends Internal {
const _None() : super(8);
}
class _TextSelection extends Internal {
const _TextSelection() : super(9);
}
class _UserEntered extends Internal {
const _UserEntered() : super(10);
}
class _CustomTab extends Internal {
const _CustomTab() : super(11);
}
@@ -0,0 +1,11 @@
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
class GeckoBrowserService {
final GeckoBrowserApi _api;
GeckoBrowserService({GeckoBrowserApi? api}) : _api = api ?? GeckoBrowserApi();
Future<void> showNativeFragment() {
return _api.showNativeFragment();
}
}
@@ -0,0 +1,128 @@
import 'package:flutter_mozilla_components/src/data/models/load_url_flags.dart';
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
class GeckoSessionService {
final String? tabId;
final GeckoSessionApi _api;
GeckoSessionService.forCurrentTab({GeckoSessionApi? api})
: _api = api ?? GeckoSessionApi(),
tabId = null;
GeckoSessionService({required String this.tabId, GeckoSessionApi? api})
: _api = api ?? GeckoSessionApi();
Future<void> loadUrl({
required Uri url,
LoadUrlFlags flags = LoadUrlFlags.NONE,
Map<String, String>? additionalHeaders,
}) {
return _api.loadUrl(
tabId: tabId,
url: url.toString(),
flags: flags.toValue(),
additionalHeaders: additionalHeaders,
);
}
Future<void> loadData({
required String data,
required String mimeType,
String encoding = "UTF-8",
}) {
return _api.loadData(
tabId: tabId,
data: data,
mimeType: mimeType,
encoding: encoding,
);
}
Future<void> reload({LoadUrlFlags flags = LoadUrlFlags.NONE}) {
return _api.reload(
tabId: tabId,
flags: flags.toValue(),
);
}
Future<void> stopLoading() {
return _api.stopLoading(tabId: tabId);
}
Future<void> goBack({bool userInteraction = true}) {
return _api.goBack(
tabId: tabId,
userInteraction: userInteraction,
);
}
Future<void> goForward({bool userInteraction = true}) {
return _api.goForward(
tabId: tabId,
userInteraction: userInteraction,
);
}
Future<void> goToHistoryIndex({required int index}) {
return _api.goToHistoryIndex(
index: index,
tabId: tabId,
);
}
Future<void> requestDesktopSite({required bool enable}) {
return _api.requestDesktopSite(
enable: enable,
tabId: tabId,
);
}
Future<void> exitFullscreen() {
return _api.exitFullscreen(tabId: tabId);
}
Future<void> saveToPdf() {
return _api.saveToPdf(tabId: tabId);
}
Future<void> printContent() {
return _api.printContent(tabId: tabId);
}
Future<void> translate({
required String fromLanguage,
required String toLanguage,
TranslationOptions? options,
}) {
return _api.translate(
tabId: tabId,
fromLanguage: fromLanguage,
toLanguage: toLanguage,
options: options,
);
}
Future<void> translateRestore() {
return _api.translateRestore(tabId: tabId);
}
Future<void> crashRecovery({
List<String>? tabIds,
}) {
return _api.crashRecovery(tabIds: tabIds);
}
Future<void> purgeHistory() {
return _api.purgeHistory();
}
Future<void> updateLastAccess({
String? tabId, //If null = current tab
DateTime? lastAccess, //If null datetime.now
}) {
return _api.updateLastAccess(
tabId: tabId,
lastAccess: (lastAccess ?? DateTime.now()).millisecondsSinceEpoch,
);
}
}
@@ -0,0 +1,135 @@
import 'package:flutter_mozilla_components/src/data/models/load_url_flags.dart';
import 'package:flutter_mozilla_components/src/data/models/source.dart';
import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
class GeckoTabsService {
final GeckoTabsApi _api;
GeckoTabsService({GeckoTabsApi? api}) : _api = api ?? GeckoTabsApi();
Future<void> selectTab({required String tabId}) {
return _api.selectTab(tabId: tabId);
}
Future<void> removeTab({required String tabId}) {
return _api.removeTab(tabId: tabId);
}
Future<String> addTab({
Uri? url,
bool selectTab = true,
bool startLoading = true,
String? parentId,
LoadUrlFlags flags = LoadUrlFlags.NONE,
String? contextId,
Source source = Internal.newTab,
bool private = false,
HistoryMetadataKey? historyMetadata,
Map<String, String>? additionalHeaders,
}) {
return _api.addTab(
url: (url ?? Uri.parse('about:blank')).toString(),
selectTab: selectTab,
startLoading: startLoading,
parentId: parentId,
flags: flags.toValue(),
contextId: contextId,
source: source.toValue(),
private: private,
historyMetadata: historyMetadata,
additionalHeaders: additionalHeaders,
);
}
Future<void> removeAllTabs({required bool recoverable}) {
return _api.removeAllTabs(recoverable: recoverable);
}
Future<void> removeTabs({required List<String> ids}) {
return _api.removeTabs(ids: ids);
}
Future<void> removeNormalTabs() {
return _api.removeNormalTabs();
}
Future<void> removePrivateTabs() {
return _api.removePrivateTabs();
}
Future<void> undo() {
return _api.undo();
}
//restoreTabs invokes splitted
Future<void> restoreTabsByList({
required List<RecoverableTab> tabs,
String? selectTabId,
RestoreLocation restoreLocation = RestoreLocation.end,
}) {
return _api.restoreTabsByList(
tabs: tabs, selectTabId: selectTabId, restoreLocation: restoreLocation);
}
Future<void> restoreTabsByBrowserState({
required RecoverableBrowserState state,
RestoreLocation restoreLocation = RestoreLocation.end,
}) {
return _api.restoreTabsByBrowserState(
state: state, restoreLocation: restoreLocation);
}
//The calls with engin storage for restore are not supported at the moment
//selectOrAddTab invokes splitted
/// Selects an already existing tab with the matching [HistoryMetadataKey] or otherwise
/// creates a new tab with the given [url].
Future<String> selectOrAddTabByHistory({
required Uri url,
required HistoryMetadataKey historyMetadata,
}) {
return _api.selectOrAddTabByHistory(
url: url.toString(), historyMetadata: historyMetadata);
}
/// Selects an already existing tab displaying [url] or otherwise creates a new tab.
Future<String> selectOrAddTabByUrl({
required Uri url,
bool private = false,
Source source = Internal.newTab,
LoadUrlFlags flags = LoadUrlFlags.NONE,
bool ignoreFragment = false,
}) {
return _api.selectOrAddTabByUrl(
url: url.toString(),
private: private,
source: source.toValue(),
flags: flags.toValue(),
ignoreFragment: ignoreFragment,
);
}
Future<String> duplicateTab({
String? selectTabId,
bool selectNewTab = true,
}) {
return _api.duplicateTab(
selectTabId: selectTabId, selectNewTab: selectNewTab);
}
Future<void> moveTabs({
required List<String> tabIds,
required String targetTabId,
required bool placeAfter,
}) {
return _api.moveTabs(
tabIds: tabIds, targetTabId: targetTabId, placeAfter: placeAfter);
}
Future<String> migratePrivateTabUseCase({
required String tabId,
Uri? alternativeUrl,
}) {
return _api.migratePrivateTabUseCase(
tabId: tabId, alternativeUrl: alternativeUrl?.toString());
}
}
@@ -0,0 +1,45 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mozilla_components/src/domain/services/gecko_browser.dart';
class GeckoView extends StatelessWidget {
const GeckoView({super.key});
@override
Widget build(BuildContext context) {
return PlatformViewLink(
viewType: 'eu.lensai/gecko',
surfaceFactory: (
context,
controller,
) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (PlatformViewCreationParams params) {
return PlatformViewsService.initExpensiveAndroidView(
id: params.id,
viewType: 'eu.lensai/gecko',
layoutDirection: TextDirection.ltr,
creationParams: {},
creationParamsCodec: const StandardMessageCodec(),
)
..addOnPlatformViewCreatedListener((value) async {
params.onPlatformViewCreated(value);
SchedulerBinding.instance.addPostFrameCallback((_) async {
await GeckoBrowserService().showNativeFragment();
});
})
..create();
},
);
}
}
@@ -0,0 +1,9 @@
import 'package:flutter_mozilla_components/src/data/models/source.dart';
/// Maps an int category (as it can be obtained from a package manager) to our internal representation.
PackageCategory packageCategoryfromInt(int? id) {
return PackageCategory.values.firstWhere(
(category) => category.id == id,
orElse: () => PackageCategory.unknown,
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,414 @@
import 'package:pigeon/pigeon.dart';
/// Translation options that map to the Gecko Translations Options.
///
/// @property downloadModel If the necessary models should be downloaded on request. If false, then
/// the translation will not complete and throw an exception if the models are not already available.
class TranslationOptions {
final bool downloadModel;
TranslationOptions({this.downloadModel = true});
}
/// Value type that represents the state of reader mode/view.
class ReaderState {
/// Whether or not the current page can be transformed to
/// be displayed in a reader view.
final bool readerable;
/// Whether or not reader view is active.
final bool active;
/// Whether or not a readerable check is required for the
/// current page.
final bool checkRequired;
/// Whether or not a new connection to the reader view
/// content script is required.
final bool connectRequired;
/// The base URL of the reader view extension page.
final String? baseUrl;
/// The URL of the page currently displayed in reader view.
final String? activeUrl;
/// The vertical scroll position of the page currently
/// displayed in reader view.
final int? scrollY;
const ReaderState({
this.readerable = false,
this.active = false,
this.checkRequired = false,
this.connectRequired = false,
this.baseUrl,
this.activeUrl,
this.scrollY,
});
}
/// Details about the last playing media in this tab.
class LastMediaAccessState {
/// [ContentState.url] when media started playing.
/// This is not the URL of the media but of the page when media started.
/// Defaults to "" (an empty String) if media hasn't started playing.
/// This value is only updated when media starts playing.
/// Can be used as a backup to [mediaSessionActive] for knowing the user is still on the same website
/// on which media was playing before media started playing in another tab.
final String lastMediaUrl;
/// The last time media started playing in the current web document.
/// Defaults to [0] if media hasn't started playing.
/// This value is only updated when media starts playing.
final int lastMediaAccess;
/// Whether or not the last accessed media is still active.
/// Can be used as a backup to [lastMediaUrl] on websites which allow media to continue playing
/// even when the users accesses another page (with another URL) in that same HTML document.
final bool mediaSessionActive;
const LastMediaAccessState({
this.lastMediaUrl = "",
this.lastMediaAccess = 0,
this.mediaSessionActive = false,
});
}
/// Represents a set of history metadata values that uniquely identify a record. Note that
/// when recording observations, the same set of values may or may not cause a new record to be
/// created, depending on the de-bouncing logic of the underlying storage i.e. recording history
/// metadata observations with the exact same values may be combined into a single record.
class HistoryMetadataKey {
/// A url of the page.
final String url;
/// An optional search term if this record was
/// created as part of a search by the user.
final String? searchTerm;
/// An optional url of the parent/referrer if
/// this record was created in response to a user opening
/// a page in a new tab.
final String? referrerUrl;
const HistoryMetadataKey({
required this.url,
this.searchTerm,
this.referrerUrl,
});
}
class PackageCategoryValue {
final int value;
const PackageCategoryValue(this.value);
}
/// Describes an external package.
class ExternalPackage {
/// An Android package id.
final String packageId;
/// A [PackageCategory] as defined by the application.
final PackageCategoryValue category;
const ExternalPackage(this.packageId, this.category);
}
class LoadUrlFlagsValue {
final int value;
const LoadUrlFlagsValue(this.value);
}
class SourceValue {
final int id;
final ExternalPackage? caller;
const SourceValue(this.id, this.caller);
}
/// A tab that is no longer open and in the list of tabs, but that can be restored (recovered) at
/// any time if it's combined with an [EngineSessionState] to form a [RecoverableTab].
///
/// The values of this data class are usually filled with the values of a [TabSessionState] when
/// getting closed.
class TabState {
/// Unique ID identifying this tab.
final String id;
/// The last URL of this tab.
final String url;
/// The unique ID of the parent tab if this tab was opened from another tab (e.g. via
/// the context menu).
final String? parentId;
/// The last title of this tab (or an empty String).
final String title;
/// The last used search terms, or an empty string if no
/// search was executed for this session.
final String searchTerm;
/// The context ID ("container") this tab used (or null).
final String? contextId;
/// The last [ReaderState] of the tab.
final ReaderState readerState;
/// The last time this tab was selected.
final int lastAccess;
/// Timestamp of the tab's creation.
final int createdAt;
/// Details about the last time was playing in this tab.
final LastMediaAccessState lastMediaAccessState;
/// If tab was private.
final bool private;
/// The last [HistoryMetadataKey] of the tab.
final HistoryMetadataKey? historyMetadata;
/// The last [Source] of the tab.
final SourceValue source;
/// The index the tab should be restored at.
final int index;
/// Whether the tab has form data.
final bool hasFormData;
TabState({
required this.id,
required this.url,
this.parentId,
this.title = "",
this.searchTerm = "",
this.contextId,
this.readerState = const ReaderState(),
this.lastAccess = 0,
this.createdAt = 0,
this.lastMediaAccessState = const LastMediaAccessState(),
this.private = false,
this.historyMetadata,
required this.source, //= Internal.none,
this.index = -1,
this.hasFormData = false,
});
}
/// A recoverable version of [TabState].
class RecoverableTab {
/// The [EngineSessionState] needed for restoring the previous state of this tab.
final String? engineSessionStateJson;
/// A [TabState] instance containing basic tab state.
final TabState state;
const RecoverableTab({
this.engineSessionStateJson,
required this.state,
});
}
/// A restored browser state, read from disk.
class RecoverableBrowserState {
/// The list of restored tabs.
final List<RecoverableTab?> tabs;
/// The ID of the selected tab in [tabs]. Or `null` if no selection was restored.
final String? selectedTabId;
const RecoverableBrowserState({
required this.tabs,
this.selectedTabId,
});
}
/// Indicates what location the tabs should be restored at
enum RestoreLocation {
/// Restore tabs at the beginning of the tab list
beginning,
/// Restore tabs at the end of the tab list
end,
/// Restore tabs at a specific index in the tab list
atIndex,
}
@ConfigurePigeon(PigeonOptions(
dartOut: 'lib/src/pigeons/gecko.g.dart',
dartOptions: DartOptions(),
kotlinOut:
'android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt',
kotlinOptions: KotlinOptions(),
dartPackageName: 'flutter_mozilla_components',
))
@HostApi()
abstract class GeckoBrowserApi {
void showNativeFragment();
}
@HostApi()
abstract class GeckoSessionApi {
void loadUrl({
required String? tabId, //If null = current tab
required String url,
required LoadUrlFlagsValue flags, //== LoadUrlFlagsBuilder.NONE,
required Map<String, String>? additionalHeaders,
});
void loadData({
required String? tabId, //If null = current tab
required String data,
required String mimeType,
required String encoding,
});
void reload({
required String? tabId, //If null = current tab
required LoadUrlFlagsValue flags, //== LoadUrlFlagsBuilder.NONE,
});
void stopLoading({
required String? tabId, //If null = current tab
});
void goBack({
required String? tabId, //If null = current tab
required bool userInteraction,
});
void goForward({
required String? tabId, //If null = current tab
required bool userInteraction,
});
void goToHistoryIndex({
required int index,
required String? tabId, //If null = current tab
});
void requestDesktopSite({
required String? tabId, //If null = current tab
required bool enable,
});
void exitFullscreen({
required String? tabId, //If null = current tab
});
void saveToPdf({
required String? tabId, //If null = current tab
});
void printContent({
required String? tabId, //If null = current tab
});
void translate({
required String? tabId, //If null = current tab
required String fromLanguage,
required String toLanguage,
required TranslationOptions? options,
});
void translateRestore({
required String? tabId, //If null = current tab
});
void crashRecovery({
required List<String>? tabIds,
});
void purgeHistory();
void updateLastAccess({
required String? tabId, //If null = current tab
required int? lastAccess, //If null datetime.now
});
}
@HostApi()
abstract class GeckoTabsApi {
void selectTab({required String tabId});
void removeTab({required String tabId});
String addTab({
required String url,
required bool selectTab,
required bool startLoading,
required String? parentId,
required LoadUrlFlagsValue flags,
required String? contextId,
//engineSession: EngineSession? = null,
required SourceValue source, //Internal.NewTab
//searchTerms: String = "",
required bool private,
required HistoryMetadataKey? historyMetadata,
//isSearch: Boolean = false,
//searchEngineName: String? = null,
required Map<String, String>? additionalHeaders,
});
void removeAllTabs({required bool recoverable});
void removeTabs({required List<String> ids});
void removeNormalTabs();
void removePrivateTabs();
void undo();
//restoreTabs invokes splitted
void restoreTabsByList({
required List<RecoverableTab> tabs,
required String? selectTabId,
required RestoreLocation restoreLocation,
});
void restoreTabsByBrowserState({
required RecoverableBrowserState state,
required RestoreLocation restoreLocation,
});
//The calls with engin storage for restore are not supported at the moment
//selectOrAddTab invokes splitted
/// Selects an already existing tab with the matching [HistoryMetadataKey] or otherwise
/// creates a new tab with the given [url].
String selectOrAddTabByHistory({
required String url,
required HistoryMetadataKey historyMetadata,
});
/// Selects an already existing tab displaying [url] or otherwise creates a new tab.
String selectOrAddTabByUrl({
required String url,
required bool private,
required SourceValue source, // = Internal.newTab,
required LoadUrlFlagsValue flags,
required bool ignoreFragment,
});
String duplicateTab({
required String? selectTabId,
required bool selectNewTab,
});
void moveTabs({
required List<String> tabIds,
required String targetTabId,
required bool placeAfter,
});
String migratePrivateTabUseCase({
required String tabId,
required String? alternativeUrl,
});
}
@@ -0,0 +1,71 @@
name: flutter_mozilla_components
description: "A new Flutter plugin project."
version: 0.0.1
homepage:
environment:
sdk: ^3.6.0-198.0.dev
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
pigeon: ^22.3.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: eu.lensai.flutter_mozilla_components
pluginClass: FlutterMozillaComponentsPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package