initial multi user feature

This commit is contained in:
Fabian Freund
2025-11-27 07:51:16 +01:00
parent 6d3c2757c3
commit 00d0599dde
49 changed files with 1341 additions and 258 deletions
@@ -16,7 +16,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.CallSuper
@@ -30,7 +29,6 @@ import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature
import eu.weblibre.flutter_mozilla_components.integration.ReaderViewIntegration
import eu.weblibre.flutter_mozilla_components.services.DownloadService
import io.flutter.Log
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.concept.engine.EngineView
@@ -242,7 +240,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
shareResourceFeature.set(
ShareResourceFeature(
context = requireContext().applicationContext,
context = components.profileApplicationContext,
httpClient = components.core.client,
store = components.core.store,
tabId = sessionId,
@@ -253,7 +251,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
downloadsFeature.set(
feature = DownloadsFeature(
requireContext().applicationContext,
components.profileApplicationContext,
store = components.core.store,
useCases = components.useCases.downloadsUseCases,
fragmentManager = childFragmentManager,
@@ -261,7 +259,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
Logger.debug("Download done. ID#$id $download with status $status")
},
downloadManager = FetchDownloadManager(
requireContext().applicationContext,
components.profileApplicationContext,
components.core.store,
DownloadService::class,
notificationsDelegate = components.notificationsDelegate,
@@ -449,7 +447,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
}
private fun openPopup(webExtensionState: WebExtensionState) {
val intent = Intent(requireContext().applicationContext, WebExtensionActionPopupActivity::class.java)
val intent = Intent(components.profileApplicationContext, WebExtensionActionPopupActivity::class.java)
intent.putExtra("web_extension_id", webExtensionState.id)
intent.putExtra("web_extension_name", webExtensionState.name)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@@ -6,29 +6,19 @@
package eu.weblibre.flutter_mozilla_components
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.annotation.CallSuper
import eu.weblibre.flutter_mozilla_components.addons.WebExtensionActionPopupActivity
import eu.weblibre.flutter_mozilla_components.feature.ReadabilityExtractFeature
import eu.weblibre.flutter_mozilla_components.feature.WebExtensionToolbarFeature
import eu.weblibre.flutter_mozilla_components.integration.ReaderViewIntegration
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.tabs.WindowFeature
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.webextensions.WebExtensionPopupObserver
/**
* Fragment used for browsing the web within the main app.
*/
class BrowserFragment() : BaseBrowserFragment(), UserInteractionHandler {
override fun createEngine(components: Components): EngineView {
return components.core.engine.createView(requireContext()).apply {
return components.core.engine.createView(components.profileApplicationContext).apply {
selectionActionDelegate = components.selectionAction
}
}
@@ -19,7 +19,6 @@ import eu.weblibre.flutter_mozilla_components.pigeons.BrowserExtensionEvents
import eu.weblibre.flutter_mozilla_components.pigeons.ContentBlocking
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
import mozilla.components.concept.engine.EngineView
@@ -32,7 +31,7 @@ import mozilla.components.feature.downloads.FileSizeFormatter
import mozilla.components.support.base.android.NotificationsDelegate
import mozilla.components.support.base.log.Log
class Components(private val context: Context,
class Components(val profileApplicationContext: Context,
val flutterEvents: GeckoStateEvents,
val readerViewController: ReaderViewController,
val selectionAction: SelectionActionDelegate,
@@ -43,24 +42,24 @@ class Components(private val context: Context,
private val tabContentEvents: GeckoTabContentEvents,
private val extensionEvents: BrowserExtensionEvents
) {
val core by lazy { Core(context, this, flutterEvents, extensionEvents) }
val core by lazy { Core(profileApplicationContext, this, flutterEvents, extensionEvents) }
val events by lazy { Events(flutterEvents) }
val useCases by lazy { UseCases(context, core.engine, core.store) }
val services by lazy { Services(context, useCases.tabsUseCases) }
val useCases by lazy { UseCases(profileApplicationContext, core.engine, core.store) }
val services by lazy { Services(profileApplicationContext, useCases.tabsUseCases) }
val features by lazy { Features(core.engine, core.store, addonEvents, tabContentEvents) }
val search by lazy { Search(context, core, useCases) }
val search by lazy { Search(profileApplicationContext, core, useCases) }
var engineView: EngineView? = null
var engineReportedInitialized = false
private val notificationManagerCompat = NotificationManagerCompat.from(context)
private val notificationManagerCompat = NotificationManagerCompat.from(profileApplicationContext)
val notificationsDelegate: NotificationsDelegate by lazy {
NotificationsDelegate(
notificationManagerCompat,
)
}
val fileSizeFormatter: FileSizeFormatter by lazy { DefaultFileSizeFormatter(context) }
val fileSizeFormatter: FileSizeFormatter by lazy { DefaultFileSizeFormatter(profileApplicationContext) }
val dateTimeProvider: DateTimeProvider by lazy { DefaultDateTimeProvider() }
@@ -37,14 +37,15 @@ object GlobalComponents {
get() = _components
@DelicateCoroutinesApi
private fun restoreBrowserState(newComponents: Components) = GlobalScope.launch(Dispatchers.Main) {
newComponents.useCases.tabsUseCases.restore(newComponents.core.sessionStorage)
private fun restoreBrowserState(newComponents: Components) =
GlobalScope.launch(Dispatchers.Main) {
newComponents.useCases.tabsUseCases.restore(newComponents.core.sessionStorage)
newComponents.core.sessionStorage.autoSave(newComponents.core.store)
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
.whenGoingToBackground()
.whenSessionsChange()
}
newComponents.core.sessionStorage.autoSave(newComponents.core.store)
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
.whenGoingToBackground()
.whenSessionsChange()
}
@DelicateCoroutinesApi
private fun restoreDownloads(newComponents: Components) = GlobalScope.launch(Dispatchers.Main) {
@@ -62,7 +63,7 @@ object GlobalComponents {
extensionEvents: BrowserExtensionEvents,
logLevel: Log.Priority,
contentBlocking: ContentBlocking,
addonCollection: AddonCollection?,
addonCollection: AddonCollection?
) {
Logger.debug("Creating new components")
@@ -77,7 +78,6 @@ object GlobalComponents {
addonEvents,
tabContentEvents,
extensionEvents,
)
_components = newComponents
@@ -103,16 +103,17 @@ object GlobalComponents {
WebExtensionSupport.initialize(
newComponents.core.engine,
newComponents.core.store,
onNewTabOverride = {
_, engineSession, url ->
newComponents.useCases.tabsUseCases.addTab(url, selectTab = true, engineSession = engineSession)
onNewTabOverride = { _, engineSession, url ->
newComponents.useCases.tabsUseCases.addTab(
url,
selectTab = true,
engineSession = engineSession
)
},
onCloseTabOverride = {
_, sessionId ->
onCloseTabOverride = { _, sessionId ->
newComponents.useCases.tabsUseCases.removeTab(sessionId)
},
onSelectTabOverride = {
_, sessionId ->
onSelectTabOverride = { _, sessionId ->
newComponents.useCases.tabsUseCases.selectTab(sessionId)
},
onUpdatePermissionRequest = newComponents.core.addonUpdater::onUpdatePermissionRequest,
@@ -0,0 +1,127 @@
package eu.weblibre.flutter_mozilla_components
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.ApplicationInfo
import android.os.Build
import androidx.annotation.RequiresApi
import java.io.File
class ProfileContext(private val base: Context, private val relativePath: String) :
ContextWrapper(base) {
private val subfolderRoot =
File(base.filesDir, relativePath) // /data/user/0/com.app/profiles/default
private var customFilesDir: File = File(subfolderRoot, "files")
private var customNoBackupFilesDir: File = File(subfolderRoot, "no_backup")
private var customObbDir: File = File(subfolderRoot, "obb")
private var customCacheDir: File = File(subfolderRoot, "cache")
private var customCodeCacheDir: File = File(subfolderRoot, "code_cache")
private var customDataDir: File = subfolderRoot
private var customExternalCacheDir: File? =
base.externalCacheDir?.parentFile?.let { File(File(it, relativePath), "cache") }
private var customExternalFilesDir: File? =
base.getExternalFilesDir(null)?.parentFile?.let { File(File(it, relativePath), "files") }
private val customApplicationInfo: ApplicationInfo by lazy {
val original = base.applicationInfo
ApplicationInfo(original).apply {
dataDir = customDataDir.absolutePath
sourceDir = original.sourceDir
publicSourceDir = original.publicSourceDir
nativeLibraryDir = original.nativeLibraryDir
deviceProtectedDataDir = customDataDir.absolutePath
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
deviceProtectedDataDir = customDataDir.absolutePath
}
}
}
init {
customFilesDir.mkdirs()
customNoBackupFilesDir.mkdirs()
customObbDir.mkdirs()
customCacheDir.mkdirs()
customCodeCacheDir.mkdirs()
customDataDir.mkdirs()
customExternalCacheDir?.mkdirs()
customExternalFilesDir?.mkdirs()
}
override fun getApplicationInfo(): ApplicationInfo {
return customApplicationInfo
}
override fun getFilesDir(): File {
return customFilesDir
}
override fun getFileStreamPath(name: String): File {
return File(customFilesDir, name)
}
override fun getNoBackupFilesDir(): File {
return customNoBackupFilesDir
}
override fun getObbDir(): File {
return customObbDir
}
override fun getCacheDir(): File {
return customCacheDir
}
override fun getCodeCacheDir(): File {
return customCodeCacheDir
}
@RequiresApi(Build.VERSION_CODES.N)
override fun getDataDir(): File {
return customDataDir
}
override fun getExternalCacheDir(): File? {
return customExternalCacheDir
}
override fun getExternalFilesDir(type: String?): File? {
return if (type == null) {
customExternalFilesDir
} else {
base.getExternalFilesDir(type)?.parentFile?.let {
File(File(it, relativePath), type)
}?.apply { mkdirs() }
}
}
override fun getExternalFilesDirs(type: String?): Array<File> {
return base.getExternalFilesDirs(type).map {
File(File(it.parentFile!!, relativePath), type ?: "files").apply { mkdirs() }
}.toTypedArray()
}
override fun getExternalCacheDirs(): Array<File> {
return base.externalCacheDirs.map {
File(File(it.parentFile!!, relativePath), "cache").apply { mkdirs() }
}.toTypedArray()
}
override fun getExternalMediaDirs(): Array<File> {
return base.externalMediaDirs.map {
File(File(it.parentFile!!, relativePath), "media").apply { mkdirs() }
}.toTypedArray()
}
override fun getDir(name: String, mode: Int): File {
return File(customDataDir, name).apply { mkdirs() }
}
override fun getDatabasePath(name: String): File {
return File(File(customDataDir, "databases"), name).apply {
parentFile?.mkdirs()
}
}
}
@@ -13,6 +13,7 @@ import androidx.fragment.app.FragmentActivity
import eu.weblibre.flutter_mozilla_components.BrowserFragment
import eu.weblibre.flutter_mozilla_components.GeckoViewFactory
import eu.weblibre.flutter_mozilla_components.GlobalComponents
import eu.weblibre.flutter_mozilla_components.ProfileContext
import eu.weblibre.flutter_mozilla_components.activities.NotificationActivity
import eu.weblibre.flutter_mozilla_components.feature.DefaultSelectionActionDelegate
import eu.weblibre.flutter_mozilla_components.pigeons.AddonCollection
@@ -51,7 +52,6 @@ import mozilla.components.browser.state.action.SystemAction
import mozilla.components.feature.addons.logger
import mozilla.components.support.base.ext.getStacktraceAsString
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.sink.AndroidLogSink
import mozilla.components.support.base.log.sink.LogSink
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
import org.mozilla.geckoview.BuildConfig as GeckoViewBuildConfig
@@ -71,7 +71,7 @@ class PriorityAwareLogSink(
return
}
val level = when(priority) {
val level = when (priority) {
Log.Priority.DEBUG -> LogLevel.DEBUG
Log.Priority.INFO -> LogLevel.INFO
Log.Priority.WARN -> LogLevel.WARN
@@ -110,7 +110,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
private var isPlatformViewRegistered = false
private lateinit var _flutterPluginBinding: FlutterPlugin.FlutterPluginBinding
private lateinit var _flutterEvents : GeckoStateEvents
private lateinit var _flutterEvents: GeckoStateEvents
fun attachBinding(flutterPluginBinding: FlutterPluginBinding) {
_flutterPluginBinding = flutterPluginBinding
@@ -143,15 +143,16 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
}
override fun initialize(
profileFolder: String,
logLevel: LogLevel,
contentBlocking: ContentBlocking,
addonCollection: AddonCollection?
) {
synchronized(this) {
if(!isGeckoInitialized) {
if (!isGeckoInitialized) {
val geckoLogging = GeckoLogging(_flutterPluginBinding.binaryMessenger)
val level = when(logLevel) {
val level = when (logLevel) {
LogLevel.DEBUG -> Log.Priority.DEBUG
LogLevel.INFO -> Log.Priority.INFO
LogLevel.WARN -> Log.Priority.WARN
@@ -160,7 +161,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
Log.addSink(PriorityAwareLogSink(level, geckoLogging))
setupGeckoEngine(level, contentBlocking, addonCollection)
setupGeckoEngine(profileFolder, level, contentBlocking, addonCollection)
isGeckoInitialized = true
}
}
@@ -177,19 +178,24 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
}
private fun setupGeckoEngine(
profileFolder: String,
logLevel: Log.Priority,
contentBlocking: ContentBlocking,
addonCollection: AddonCollection?
) {
val selectionActionEvents = GeckoSelectionActionEvents(_flutterPluginBinding.binaryMessenger)
val profileApplicationContext = ProfileContext(_flutterPluginBinding.applicationContext, profileFolder)
val selectionActionDelegate = DefaultSelectionActionDelegate(selectionActionEvents) { actions ->
val processTextAction = "android.intent.action.PROCESS_TEXT"
val withoutProcessText = actions.filter { it != processTextAction }.toTypedArray()
val processTextActions = actions.filter { it == processTextAction }.toTypedArray()
val selectionActionEvents =
GeckoSelectionActionEvents(_flutterPluginBinding.binaryMessenger)
withoutProcessText + processTextActions
}
val selectionActionDelegate =
DefaultSelectionActionDelegate(selectionActionEvents) { actions ->
val processTextAction = "android.intent.action.PROCESS_TEXT"
val withoutProcessText = actions.filter { it != processTextAction }.toTypedArray()
val processTextActions = actions.filter { it == processTextAction }.toTypedArray()
withoutProcessText + processTextActions
}
val readerViewController =
ReaderViewController(_flutterPluginBinding.binaryMessenger)
@@ -200,10 +206,13 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
val tabContentEvents = GeckoTabContentEvents(_flutterPluginBinding.binaryMessenger)
val suggestionEvents = GeckoSuggestionEvents(_flutterPluginBinding.binaryMessenger)
GeckoSuggestionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSuggestionApiImpl(suggestionEvents))
GeckoSuggestionApi.setUp(
_flutterPluginBinding.binaryMessenger,
GeckoSuggestionApiImpl(suggestionEvents)
)
GlobalComponents.setUp(
_flutterPluginBinding.applicationContext,
profileApplicationContext,
_flutterEvents,
readerViewController,
selectionActionDelegate,
@@ -215,22 +224,39 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
addonCollection
)
GeckoEngineSettingsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoEngineSettingsApiImpl())
GeckoAddonsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAddonsApiImpl(_flutterPluginBinding.applicationContext))
GeckoEngineSettingsApi.setUp(
_flutterPluginBinding.binaryMessenger,
GeckoEngineSettingsApiImpl()
)
GeckoAddonsApi.setUp(
_flutterPluginBinding.binaryMessenger,
GeckoAddonsApiImpl(profileApplicationContext)
)
GeckoSessionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSessionApiImpl())
GeckoTabsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTabsApiImpl())
GeckoIconsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoIconsApiImpl())
GeckoCookieApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoCookieApiImpl())
GeckoMlApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoMlApiImpl())
GeckoPrefApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPrefApiImpl())
GeckoContainerProxyApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoContainerProxyApiImpl())
GeckoContainerProxyApi.setUp(
_flutterPluginBinding.binaryMessenger,
GeckoContainerProxyApiImpl()
)
GeckoFindApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFindApiImpl())
GeckoSelectionActionController.setUp(_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl(
selectionActionDelegate
))
GeckoDeleteBrowsingDataController.setUp(_flutterPluginBinding.binaryMessenger, GeckoDeleteBrowsingDataControllerImpl())
GeckoSelectionActionController.setUp(
_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl(
selectionActionDelegate
)
)
GeckoDeleteBrowsingDataController.setUp(
_flutterPluginBinding.binaryMessenger,
GeckoDeleteBrowsingDataControllerImpl()
)
GeckoDownloadsApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoDownloadsApiImpl())
GeckoBrowserExtensionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoBrowserExtensionApiImpl())
GeckoBrowserExtensionApi.setUp(
_flutterPluginBinding.binaryMessenger,
GeckoBrowserExtensionApiImpl()
)
GeckoHistoryApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoHistoryApiImpl())
GeckoFetchApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoFetchApiImpl())
@@ -239,9 +265,10 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
components.events.readerViewEvents
)
val intent = Intent(_flutterPluginBinding.applicationContext, NotificationActivity::class.java)
val intent =
Intent(profileApplicationContext, NotificationActivity::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
_flutterPluginBinding.applicationContext.startActivity(intent)
profileApplicationContext.startActivity(intent)
}
private fun showFragmentCallback(): Boolean {
@@ -289,7 +316,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
return true
}
if(!view.isAttachedToWindow) {
if (!view.isAttachedToWindow) {
return true
}
@@ -3095,7 +3095,7 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoBrowserApi {
fun getGeckoVersion(): String
fun initialize(logLevel: LogLevel, contentBlocking: ContentBlocking, addonCollection: AddonCollection?)
fun initialize(profileFolder: String, logLevel: LogLevel, contentBlocking: ContentBlocking, addonCollection: AddonCollection?)
fun showNativeFragment(): Boolean
fun onTrimMemory(level: Long)
@@ -3128,11 +3128,12 @@ interface GeckoBrowserApi {
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val logLevelArg = args[0] as LogLevel
val contentBlockingArg = args[1] as ContentBlocking
val addonCollectionArg = args[2] as AddonCollection?
val profileFolderArg = args[0] as String
val logLevelArg = args[1] as LogLevel
val contentBlockingArg = args[2] as ContentBlocking
val addonCollectionArg = args[3] as AddonCollection?
val wrapped: List<Any?> = try {
api.initialize(logLevelArg, contentBlockingArg, addonCollectionArg)
api.initialize(profileFolderArg, logLevelArg, contentBlockingArg, addonCollectionArg)
listOf(null)
} catch (exception: Throwable) {
GeckoPigeonUtils.wrapError(exception)