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,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)
}
}