migrate mozilla components into project
This commit is contained in:
@@ -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>
|
||||
+64
@@ -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
|
||||
}
|
||||
+273
@@ -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
|
||||
}
|
||||
}
|
||||
+102
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+255
@@ -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)
|
||||
}
|
||||
}
|
||||
+11
@@ -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 }
|
||||
}
|
||||
+136
@@ -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
|
||||
}
|
||||
}
|
||||
+42
@@ -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
|
||||
}
|
||||
}
|
||||
+78
@@ -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
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
package eu.lensai.flutter_mozilla_components
|
||||
|
||||
+15
@@ -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 }
|
||||
}
|
||||
+15
@@ -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()
|
||||
}
|
||||
}
|
||||
+11
@@ -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();
|
||||
}
|
||||
}
|
||||
+137
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
+221
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+37
@@ -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)
|
||||
}
|
||||
}
|
||||
+1258
File diff suppressed because it is too large
Load Diff
+19
@@ -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>
|
||||
+27
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user