sync initial
This commit is contained in:
@@ -82,6 +82,8 @@ android {
|
||||
|
||||
configurations.configureEach {
|
||||
exclude group: "org.mozilla.telemetry", module: "glean-native"
|
||||
// Exclude standalone tink to avoid duplicate classes with tink-android
|
||||
exclude group: "com.google.crypto.tink", module: "tink"
|
||||
}
|
||||
|
||||
// Select the Glean from GeckoView.
|
||||
@@ -114,6 +116,8 @@ dependencies {
|
||||
implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-accounts:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-accounts-push:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-awesomebar:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-customtabs:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion"
|
||||
@@ -122,6 +126,7 @@ dependencies {
|
||||
implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-syncedtabs:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion"
|
||||
@@ -131,6 +136,8 @@ dependencies {
|
||||
implementation "org.mozilla.components:feature-intent:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:service-firefox-accounts:$mozillaComponentsVersion"
|
||||
implementation "org.mozilla.components:support-appservices:$mozillaComponentsVersion"
|
||||
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0'
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2025 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.weblibre.flutter_mozilla_components
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
|
||||
object ActiveProfile {
|
||||
@Volatile
|
||||
var prefix: String? = null
|
||||
|
||||
/** SharedPreference names used by mozilla-components FxA/sync that need profile isolation */
|
||||
val FXA_SHARED_PREFERENCE_NAMES = setOf(
|
||||
"fxaAppState", // FxA account state (SharedPrefAccountStorage)
|
||||
"fxaStatePrefAC", // Sentinel flag for SecureAbove22 account state presence
|
||||
"fxaStateAC_kp_pre_m", // SecureAbove22 encrypted account state (API < 23 fallback)
|
||||
"fxaStateAC_kp_post_m", // SecureAbove22 encrypted account state (API >= 23)
|
||||
"fxa_abnormalities", // Tracks FxA account abnormalities
|
||||
"mozac_feature_accounts_push", // Push subscription scope + verification state
|
||||
"SyncAuthInfoCache", // Cached sync auth tokens
|
||||
"FxaDeviceSettingsCache", // Cached device settings (ID, name, type)
|
||||
"syncEngines", // Per-engine enabled/disabled state
|
||||
"syncPrefs", // Last-synced timestamp + persisted sync state
|
||||
)
|
||||
|
||||
/**
|
||||
* Resolve the active profile prefix from disk.
|
||||
* Called in Application.onCreate() to handle cold-start WorkManager scenarios.
|
||||
*/
|
||||
fun resolveFromDisk(context: Context) {
|
||||
val profileFile = File(context.filesDir, PwaConstants.CURRENT_PROFILE_FILE)
|
||||
if (!profileFile.exists()) return
|
||||
val uuid = profileFile.readText().trim().ifEmpty { return }
|
||||
val relativePath = "${PwaConstants.PROFILES_DIR_NAME}/${PwaConstants.PROFILE_DIR_PREFIX}$uuid"
|
||||
prefix = File(relativePath).name
|
||||
}
|
||||
}
|
||||
+16
@@ -37,6 +37,8 @@ import io.flutter.Log
|
||||
import mozilla.components.browser.state.state.WebExtensionState
|
||||
import mozilla.components.browser.thumbnails.BrowserThumbnails
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
import mozilla.components.feature.accounts.FxaCapability
|
||||
import mozilla.components.feature.accounts.FxaWebChannelFeature
|
||||
import mozilla.components.feature.app.links.AppLinksFeature
|
||||
import mozilla.components.feature.downloads.DownloadsFeature
|
||||
import mozilla.components.feature.downloads.manager.FetchDownloadManager
|
||||
@@ -85,6 +87,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
|
||||
|
||||
private val webAuthnFeature = ViewBoundFeatureWrapper<WebAuthnFeature>()
|
||||
private val fxaWebChannelFeature = ViewBoundFeatureWrapper<FxaWebChannelFeature>()
|
||||
|
||||
private var pictureInPictureFeature: PictureInPictureFeature? = null
|
||||
|
||||
@@ -450,6 +453,19 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
|
||||
view = view
|
||||
)
|
||||
|
||||
fxaWebChannelFeature.set(
|
||||
feature = FxaWebChannelFeature(
|
||||
customTabSessionId = sessionId,
|
||||
runtime = components.core.engine,
|
||||
store = components.core.store,
|
||||
accountManager = components.backgroundServices.accountManager,
|
||||
serverConfig = components.backgroundServices.serverConfig,
|
||||
fxaCapabilities = setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC),
|
||||
),
|
||||
owner = this,
|
||||
view = view,
|
||||
)
|
||||
|
||||
readerViewFeature.set(
|
||||
feature = ReaderViewIntegration(
|
||||
profileContext,
|
||||
|
||||
+29
-3
@@ -9,6 +9,7 @@ package eu.weblibre.flutter_mozilla_components
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import eu.weblibre.flutter_mozilla_components.components.Core
|
||||
import eu.weblibre.flutter_mozilla_components.components.BackgroundServices
|
||||
import eu.weblibre.flutter_mozilla_components.components.Events
|
||||
import eu.weblibre.flutter_mozilla_components.components.Features
|
||||
import eu.weblibre.flutter_mozilla_components.components.Search
|
||||
@@ -19,6 +20,7 @@ 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.GeckoSyncStateEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.ReaderViewController
|
||||
import mozilla.components.concept.engine.EngineView
|
||||
@@ -38,14 +40,38 @@ class Components(val profileApplicationContext: ProfileContext,
|
||||
val logLevel: Log.Priority,
|
||||
val contentBlocking: ContentBlocking,
|
||||
val addonCollection: AddonCollection?,
|
||||
val fxaServerOverride: String?,
|
||||
val syncTokenServerOverride: String?,
|
||||
val addonEvents: GeckoAddonEvents,
|
||||
private val tabContentEvents: GeckoTabContentEvents,
|
||||
private val extensionEvents: BrowserExtensionEvents
|
||||
private val extensionEvents: BrowserExtensionEvents,
|
||||
private val syncStateEvents: GeckoSyncStateEvents?,
|
||||
) {
|
||||
val core by lazy { Core(profileApplicationContext, this, flutterEvents, extensionEvents) }
|
||||
val backgroundServices by lazy {
|
||||
BackgroundServices(
|
||||
context = profileApplicationContext,
|
||||
browserStore = lazy { core.store },
|
||||
historyStorage = core.lazyHistoryStorage,
|
||||
bookmarkStorage = core.lazyBookmarksStorage,
|
||||
remoteTabsStorage = core.lazyRemoteTabsStorage,
|
||||
fxaServerOverride = fxaServerOverride,
|
||||
syncTokenServerOverride = syncTokenServerOverride,
|
||||
syncStateEvents = syncStateEvents,
|
||||
)
|
||||
}
|
||||
val events by lazy { Events(flutterEvents) }
|
||||
val useCases by lazy { UseCases(profileApplicationContext, core.engine, core.store, core.webAppShortcutManager) }
|
||||
val services by lazy { Services(profileApplicationContext, core.store, useCases.tabsUseCases) }
|
||||
val services by lazy {
|
||||
Services(
|
||||
profileApplicationContext,
|
||||
core.store,
|
||||
useCases.tabsUseCases,
|
||||
backgroundServices.accountManager,
|
||||
core.engine,
|
||||
backgroundServices.serverConfig,
|
||||
)
|
||||
}
|
||||
val features by lazy { Features(core.engine, core.store, addonEvents, tabContentEvents) }
|
||||
val search by lazy { Search(profileApplicationContext, core, useCases) }
|
||||
|
||||
@@ -68,4 +94,4 @@ class Components(val profileApplicationContext: ProfileContext,
|
||||
val dateTimeProvider: DateTimeProvider by lazy { DefaultDateTimeProvider() }
|
||||
|
||||
val downloadEstimator: DownloadEstimator by lazy { DownloadEstimator(dateTimeProvider = dateTimeProvider) }
|
||||
}
|
||||
}
|
||||
|
||||
+33
-1
@@ -15,6 +15,7 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoAddonEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSelectionActionEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncStateEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.QueryParameterStripping
|
||||
@@ -26,6 +27,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.browser.storage.sync.GlobalPlacesDependencyProvider
|
||||
import mozilla.components.browser.state.action.CustomTabListAction
|
||||
import mozilla.components.browser.state.selector.findCustomTab
|
||||
@@ -114,9 +116,12 @@ object GlobalComponents {
|
||||
addonEvents: GeckoAddonEvents,
|
||||
tabContentEvents: GeckoTabContentEvents,
|
||||
extensionEvents: BrowserExtensionEvents,
|
||||
syncStateEvents: GeckoSyncStateEvents?,
|
||||
logLevel: Log.Priority,
|
||||
contentBlocking: ContentBlocking,
|
||||
addonCollection: AddonCollection?,
|
||||
fxaServerOverride: String?,
|
||||
syncTokenServerOverride: String?,
|
||||
mode: ComponentsMode = ComponentsMode.FULL,
|
||||
) {
|
||||
Logger.debug("Creating new components")
|
||||
@@ -137,18 +142,30 @@ object GlobalComponents {
|
||||
logLevel,
|
||||
contentBlocking,
|
||||
addonCollection,
|
||||
fxaServerOverride,
|
||||
syncTokenServerOverride,
|
||||
addonEvents,
|
||||
tabContentEvents,
|
||||
extensionEvents,
|
||||
syncStateEvents,
|
||||
)
|
||||
_components = newComponents
|
||||
currentMode = mode
|
||||
|
||||
previousComponents?.let {
|
||||
runCatching {
|
||||
it.backgroundServices.accountManager.close()
|
||||
}
|
||||
}
|
||||
|
||||
//newComponents.crashReporter.install(applicationContext)
|
||||
|
||||
//Facts.registerProcessor(LogFactProcessor())
|
||||
|
||||
//RustHttpConfig.setClient(lazy { newComponents.core.client })
|
||||
val megazordNetworkSetup = MegazordSetup.setupMegazordNetwork(
|
||||
context = newComponents.profileApplicationContext,
|
||||
client = lazy { newComponents.core.client },
|
||||
)
|
||||
|
||||
if (mode == ComponentsMode.FULL) {
|
||||
newComponents.core.engine.warmUp()
|
||||
@@ -167,6 +184,12 @@ object GlobalComponents {
|
||||
}
|
||||
|
||||
if (mode == ComponentsMode.FULL) {
|
||||
if (!megazordNetworkSetup.isCompleted) {
|
||||
runBlocking {
|
||||
megazordNetworkSetup.await()
|
||||
}
|
||||
}
|
||||
|
||||
val restoreJob = restoreBrowserState(newComponents)
|
||||
if (previousCustomTabs.isNotEmpty()) {
|
||||
restoreJob.invokeOnCompletion {
|
||||
@@ -215,6 +238,12 @@ object GlobalComponents {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
newComponents.core.fileUploadsDirCleaner.cleanUploadsDirectory()
|
||||
}
|
||||
|
||||
// Eagerly initialize account manager so sync starts
|
||||
newComponents.backgroundServices.accountManager
|
||||
|
||||
// Start FxA web channel feature for OAuth redirect handling
|
||||
newComponents.services.fxaWebChannelFeature.start()
|
||||
} else {
|
||||
restorePreviousCustomTabs()
|
||||
}
|
||||
@@ -256,9 +285,12 @@ object GlobalComponents {
|
||||
addonEvents = GeckoAddonEvents(messenger),
|
||||
tabContentEvents = GeckoTabContentEvents(messenger),
|
||||
extensionEvents = BrowserExtensionEvents(messenger),
|
||||
syncStateEvents = null,
|
||||
logLevel = logLevel,
|
||||
contentBlocking = contentBlocking,
|
||||
addonCollection = null,
|
||||
fxaServerOverride = null,
|
||||
syncTokenServerOverride = null,
|
||||
mode = ComponentsMode.EXTERNAL,
|
||||
)
|
||||
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package eu.weblibre.flutter_mozilla_components
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import mozilla.components.concept.fetch.Client
|
||||
import mozilla.components.support.AppServicesInitializer
|
||||
import mozilla.components.support.AppServicesInitializer.Config as AppServicesConfig
|
||||
import mozilla.components.support.rusthttp.RustHttpConfig
|
||||
|
||||
object MegazordSetup {
|
||||
fun setupEarlyMainProcess() {
|
||||
AppServicesInitializer.init(AppServicesConfig(null))
|
||||
}
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
fun setupMegazordNetwork(context: Context, client: Lazy<Client>): Deferred<Unit> =
|
||||
GlobalScope.async(Dispatchers.IO) {
|
||||
val isDebuggable =
|
||||
(context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||
|
||||
if (isDebuggable) {
|
||||
RustHttpConfig.allowEmulatorLoopback()
|
||||
}
|
||||
|
||||
RustHttpConfig.setClient(client)
|
||||
}
|
||||
}
|
||||
+1
@@ -43,6 +43,7 @@ class ProfileContext(private val base: Context, val relativePath: String) :
|
||||
}
|
||||
|
||||
init {
|
||||
ActiveProfile.prefix = profilePrefix
|
||||
customFilesDir.mkdirs()
|
||||
customNoBackupFilesDir.mkdirs()
|
||||
customObbDir.mkdirs()
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package eu.weblibre.flutter_mozilla_components.activities
|
||||
|
||||
import mozilla.components.concept.sync.AccountObserver
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
|
||||
class AuthCustomTabActivity : ExternalAppBrowserActivity() {
|
||||
private val accountStateObserver = object : AccountObserver {
|
||||
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
GlobalComponents.components
|
||||
?.backgroundServices
|
||||
?.accountManager
|
||||
?.register(accountStateObserver, this, true)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
GlobalComponents.components
|
||||
?.backgroundServices
|
||||
?.accountManager
|
||||
?.unregister(accountStateObserver)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package eu.weblibre.flutter_mozilla_components.activities
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import mozilla.components.feature.customtabs.CustomTabIntentProcessor
|
||||
import mozilla.components.feature.intent.ext.getSessionId
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
|
||||
class AuthIntentReceiverActivity : Activity() {
|
||||
companion object {
|
||||
private const val TAG = "AuthIntentReceiver"
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val sourceIntent = intent?.let { Intent(it) } ?: Intent()
|
||||
|
||||
if (GlobalComponents.components == null && !GlobalComponents.ensureExternalComponents(applicationContext)) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val components = GlobalComponents.components
|
||||
if (components == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val processed = CustomTabIntentProcessor(
|
||||
components.useCases.customTabsUseCases.add,
|
||||
resources,
|
||||
isPrivate = false,
|
||||
).process(sourceIntent)
|
||||
|
||||
if (processed) {
|
||||
val sessionId = sourceIntent.getSessionId() ?: components.core.store.state.customTabs.lastOrNull()?.id
|
||||
if (sessionId != null) {
|
||||
val authIntent = ExternalAppBrowserActivity
|
||||
.createIntent(this, sessionId)
|
||||
.setClassName(this, AuthCustomTabActivity::class.java.name)
|
||||
startActivity(authIntent)
|
||||
} else {
|
||||
Log.w(TAG, "Auth intent processed but no custom tab session id found")
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Auth custom tab intent was not processed")
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -34,7 +34,7 @@ import mozilla.components.support.base.log.logger.Logger
|
||||
*
|
||||
* Uses an empty taskAffinity so Custom Tabs appear as a separate task from the main app.
|
||||
*/
|
||||
class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
open class ExternalAppBrowserActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private const val TAG = "ExternalAppBrowserActivity"
|
||||
|
||||
|
||||
+23
-4
@@ -47,6 +47,8 @@ import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSessionApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoStateEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSuggestionEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncStateEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabContentEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoTabsApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoViewportApi
|
||||
@@ -155,7 +157,9 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||
profileFolder: String,
|
||||
logLevel: LogLevel,
|
||||
contentBlocking: ContentBlocking,
|
||||
addonCollection: AddonCollection?
|
||||
addonCollection: AddonCollection?,
|
||||
fxaServerOverride: String?,
|
||||
syncTokenServerOverride: String?,
|
||||
) {
|
||||
synchronized(this) {
|
||||
if (!isGeckoInitialized) {
|
||||
@@ -170,7 +174,14 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||
|
||||
Log.addSink(PriorityAwareLogSink(level, geckoLogging))
|
||||
|
||||
setupGeckoEngine(profileFolder, level, contentBlocking, addonCollection)
|
||||
setupGeckoEngine(
|
||||
profileFolder,
|
||||
level,
|
||||
contentBlocking,
|
||||
addonCollection,
|
||||
fxaServerOverride,
|
||||
syncTokenServerOverride,
|
||||
)
|
||||
isGeckoInitialized = true
|
||||
}
|
||||
}
|
||||
@@ -190,7 +201,9 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||
profileFolder: String,
|
||||
logLevel: Log.Priority,
|
||||
contentBlocking: ContentBlocking,
|
||||
addonCollection: AddonCollection?
|
||||
addonCollection: AddonCollection?,
|
||||
fxaServerOverride: String?,
|
||||
syncTokenServerOverride: String?,
|
||||
) {
|
||||
val profileApplicationContext = ProfileContext(_flutterPluginBinding.applicationContext, profileFolder)
|
||||
|
||||
@@ -220,6 +233,8 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||
GeckoSuggestionApiImpl(suggestionEvents)
|
||||
)
|
||||
|
||||
val syncStateEvents = GeckoSyncStateEvents(_flutterPluginBinding.binaryMessenger)
|
||||
|
||||
GlobalComponents.setUp(
|
||||
profileApplicationContext,
|
||||
_flutterEvents,
|
||||
@@ -228,9 +243,12 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||
addonEvents,
|
||||
tabContentEvents,
|
||||
extensionEvents,
|
||||
syncStateEvents,
|
||||
logLevel,
|
||||
contentBlocking,
|
||||
addonCollection
|
||||
addonCollection,
|
||||
fxaServerOverride,
|
||||
syncTokenServerOverride,
|
||||
)
|
||||
|
||||
val engineSettingsApiImpl = GeckoEngineSettingsApiImpl()
|
||||
@@ -275,6 +293,7 @@ class GeckoBrowserApiImpl : GeckoBrowserApi {
|
||||
GeckoPublicSuffixListApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPublicSuffixListApiImpl(profileApplicationContext))
|
||||
GeckoTrackingProtectionApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoTrackingProtectionApiImpl())
|
||||
GeckoAppLinksApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoAppLinksApiImpl(profileApplicationContext))
|
||||
GeckoSyncApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoSyncApiImpl())
|
||||
|
||||
// PWA API for web app installation and management
|
||||
GeckoPwaApi.setUp(_flutterPluginBinding.binaryMessenger, GeckoPwaApiImpl(profileApplicationContext))
|
||||
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
package eu.weblibre.flutter_mozilla_components.api
|
||||
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.components.WebLibreFxAEntryPoint
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncApi
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.SyncAccountInfo
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.SyncDevice
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.SyncDeviceTabs
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.SyncEngineStatus
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.SyncEngineValue
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.SyncIncomingTab
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.SyncRemoteTab
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.concept.sync.DeviceCapability
|
||||
import mozilla.components.concept.sync.DeviceCommandOutgoing
|
||||
import mozilla.components.concept.sync.TabData
|
||||
import mozilla.components.service.fxa.SyncEngine
|
||||
import mozilla.components.service.fxa.manager.SCOPE_PROFILE
|
||||
import mozilla.components.service.fxa.manager.SCOPE_SYNC
|
||||
import mozilla.components.service.fxa.manager.SyncEnginesStorage
|
||||
import mozilla.components.service.fxa.sync.SyncReason
|
||||
import mozilla.components.service.fxa.sync.getLastSynced
|
||||
|
||||
class GeckoSyncApiImpl : GeckoSyncApi {
|
||||
companion object {
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
}
|
||||
|
||||
private val components by lazy {
|
||||
requireNotNull(GlobalComponents.components) { "Components not initialized" }
|
||||
}
|
||||
|
||||
override fun getAccountInfo(callback: (Result<SyncAccountInfo>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val accountManager = components.backgroundServices.accountManager
|
||||
val account = accountManager.authenticatedAccount()
|
||||
val needsReauth = accountManager.accountNeedsReauth()
|
||||
val profile = account?.getProfile()
|
||||
val engineStorage = SyncEnginesStorage(components.profileApplicationContext)
|
||||
val engineStatus = engineStorage.getStatus()
|
||||
|
||||
callback(
|
||||
Result.success(
|
||||
SyncAccountInfo(
|
||||
authenticated = account != null && !needsReauth,
|
||||
syncing = accountManager.isSyncActive(),
|
||||
needsReauth = needsReauth,
|
||||
email = profile?.email,
|
||||
displayName = profile?.displayName,
|
||||
lastSyncedAt = getLastSynced(components.profileApplicationContext)
|
||||
.takeIf { it > 0L },
|
||||
engines = listOf(
|
||||
SyncEngineStatus(
|
||||
engine = SyncEngineValue.HISTORY,
|
||||
enabled = engineStatus[SyncEngine.History] ?: true,
|
||||
),
|
||||
SyncEngineStatus(
|
||||
engine = SyncEngineValue.BOOKMARKS,
|
||||
enabled = engineStatus[SyncEngine.Bookmarks] ?: true,
|
||||
),
|
||||
SyncEngineStatus(
|
||||
engine = SyncEngineValue.TABS,
|
||||
enabled = engineStatus[SyncEngine.Tabs] ?: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun beginAuthentication(callback: (Result<Unit>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
components.backgroundServices.awaitStarted()
|
||||
components.services.accountsAuthFeature.beginAuthentication(
|
||||
context = components.profileApplicationContext,
|
||||
entrypoint = WebLibreFxAEntryPoint.Settings,
|
||||
scopes = setOf(SCOPE_PROFILE, SCOPE_SYNC),
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(Unit)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun beginPairingAuthentication(
|
||||
pairingUrl: String,
|
||||
callback: (Result<Unit>) -> Unit,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
components.backgroundServices.awaitStarted()
|
||||
components.services.accountsAuthFeature.beginPairingAuthentication(
|
||||
context = components.profileApplicationContext,
|
||||
pairingUrl = pairingUrl,
|
||||
entrypoint = WebLibreFxAEntryPoint.Settings,
|
||||
scopes = setOf(SCOPE_PROFILE, SCOPE_SYNC),
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(Unit)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun logout(callback: (Result<Unit>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
components.backgroundServices.accountManager.logout()
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(Unit)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun syncNow(callback: (Result<Unit>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
components.backgroundServices.accountManager.syncNow(SyncReason.User)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(Unit)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setEngineEnabled(
|
||||
engine: SyncEngineValue,
|
||||
enabled: Boolean,
|
||||
callback: (Result<Unit>) -> Unit,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
val storage = SyncEnginesStorage(components.profileApplicationContext)
|
||||
val mapped = when (engine) {
|
||||
SyncEngineValue.HISTORY -> SyncEngine.History
|
||||
SyncEngineValue.BOOKMARKS -> SyncEngine.Bookmarks
|
||||
SyncEngineValue.TABS -> SyncEngine.Tabs
|
||||
}
|
||||
|
||||
storage.setStatus(mapped, enabled)
|
||||
components.backgroundServices.accountManager.syncNow(SyncReason.EngineChange)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(Unit)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSyncedTabs(callback: (Result<List<SyncDeviceTabs>>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
val deviceNames = loadDeviceDisplayNames()
|
||||
components.core.remoteTabsStorage.getAll().entries.mapNotNull { (client, tabs) ->
|
||||
val deviceName = deviceNames[client.id] ?: return@mapNotNull null
|
||||
SyncDeviceTabs(
|
||||
deviceId = client.id,
|
||||
deviceName = deviceName,
|
||||
tabs = tabs.map { tab ->
|
||||
val active = tab.active()
|
||||
SyncRemoteTab(
|
||||
title = active.title,
|
||||
url = active.url,
|
||||
iconUrl = active.iconUrl,
|
||||
lastUsed = tab.lastUsed,
|
||||
inactive = tab.inactive,
|
||||
)
|
||||
}.sortedByDescending { it.lastUsed },
|
||||
)
|
||||
}.sortedBy { it.deviceName.lowercase() }
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDevices(callback: (Result<List<SyncDevice>>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
components.backgroundServices.awaitStarted()
|
||||
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||
?: return@runCatching emptyList()
|
||||
|
||||
val constellation = account.deviceConstellation()
|
||||
constellation.refreshDevices()
|
||||
val state = constellation.state()
|
||||
val currentDevice = state?.currentDevice
|
||||
val otherDevices = state?.otherDevices.orEmpty()
|
||||
|
||||
(listOfNotNull(currentDevice) + otherDevices).map { device ->
|
||||
SyncDevice(
|
||||
deviceId = device.id,
|
||||
displayName = device.displayName,
|
||||
isCurrentDevice = device.isCurrentDevice,
|
||||
canSendTab = device.capabilities.contains(DeviceCapability.SEND_TAB),
|
||||
)
|
||||
}.sortedBy { it.displayName.lowercase() }
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendTabToDevice(
|
||||
deviceId: String,
|
||||
title: String,
|
||||
url: String,
|
||||
callback: (Result<Boolean>) -> Unit,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
components.backgroundServices.awaitStarted()
|
||||
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||
?: return@runCatching false
|
||||
|
||||
val constellation = account.deviceConstellation()
|
||||
constellation.refreshDevices()
|
||||
val state = constellation.state()
|
||||
val target = state?.otherDevices?.firstOrNull {
|
||||
it.id == deviceId && it.capabilities.contains(DeviceCapability.SEND_TAB)
|
||||
} ?: return@runCatching false
|
||||
|
||||
constellation.sendCommandToDevice(
|
||||
target.id,
|
||||
DeviceCommandOutgoing.SendTab(
|
||||
title = title,
|
||||
url = url,
|
||||
),
|
||||
)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun refreshDevices(callback: (Result<Unit>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
components.backgroundServices.awaitStarted()
|
||||
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||
?: return@runCatching
|
||||
|
||||
account.deviceConstellation().refreshDevices()
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(Unit)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun pollDeviceCommands(callback: (Result<Unit>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
components.backgroundServices.awaitStarted()
|
||||
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||
?: return@runCatching
|
||||
|
||||
account.deviceConstellation().pollForCommands()
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(Unit)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun drainIncomingTabs(callback: (Result<List<SyncIncomingTab>>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
components.backgroundServices.drainIncomingTabs().map {
|
||||
SyncIncomingTab(
|
||||
title = it.title,
|
||||
url = it.url,
|
||||
fromDeviceId = it.fromDeviceId,
|
||||
fromDeviceName = it.fromDeviceName,
|
||||
)
|
||||
}
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDeviceName(callback: (Result<String?>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
components.backgroundServices.awaitStarted()
|
||||
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||
?: return@runCatching null
|
||||
|
||||
val constellation = account.deviceConstellation()
|
||||
constellation.refreshDevices()
|
||||
constellation.state()?.currentDevice?.displayName
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setDeviceName(newName: String, callback: (Result<Boolean>) -> Unit) {
|
||||
coroutineScope.launch {
|
||||
runCatching {
|
||||
val trimmed = newName.trim()
|
||||
if (trimmed.isEmpty()) {
|
||||
return@runCatching false
|
||||
}
|
||||
|
||||
components.backgroundServices.awaitStarted()
|
||||
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||
?: return@runCatching false
|
||||
|
||||
account.deviceConstellation()
|
||||
.setDeviceName(trimmed, components.profileApplicationContext)
|
||||
}.fold(
|
||||
onSuccess = { callback(Result.success(it)) },
|
||||
onFailure = { callback(Result.failure(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadDeviceDisplayNames(): Map<String, String> {
|
||||
components.backgroundServices.awaitStarted()
|
||||
val account = components.backgroundServices.accountManager.authenticatedAccount()
|
||||
?: return emptyMap()
|
||||
|
||||
val constellation = account.deviceConstellation()
|
||||
constellation.refreshDevices()
|
||||
val state = constellation.state() ?: return emptyMap()
|
||||
val devices = listOfNotNull(state.currentDevice) + state.otherDevices
|
||||
return devices.associate { it.id to it.displayName }
|
||||
}
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
package eu.weblibre.flutter_mozilla_components.components
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Build
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import mozilla.components.concept.sync.AccountObserver
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.Device
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.concept.sync.Profile
|
||||
import mozilla.components.concept.sync.TabData
|
||||
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
||||
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
||||
import mozilla.components.browser.storage.sync.RemoteTabsStorage
|
||||
import mozilla.components.concept.sync.DeviceConfig
|
||||
import mozilla.components.concept.sync.DeviceCapability
|
||||
import mozilla.components.concept.sync.DeviceType
|
||||
import mozilla.components.feature.accounts.push.SendTabFeature
|
||||
import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
|
||||
import mozilla.components.service.fxa.PeriodicSyncConfig
|
||||
import mozilla.components.service.fxa.ServerConfig
|
||||
import mozilla.components.service.fxa.SyncConfig
|
||||
import mozilla.components.service.fxa.SyncEngine
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import mozilla.components.service.fxa.manager.SCOPE_SESSION
|
||||
import mozilla.components.service.fxa.manager.SCOPE_SYNC
|
||||
import mozilla.components.service.fxa.manager.SyncEnginesStorage
|
||||
import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider
|
||||
import mozilla.components.service.fxa.sync.SyncReason
|
||||
import mozilla.components.service.fxa.sync.SyncStatusObserver
|
||||
import mozilla.components.service.fxa.sync.getLastSynced
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.GeckoSyncStateEvents
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.SyncAccountInfo
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.SyncEngineStatus
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.SyncEngineValue
|
||||
import eu.weblibre.flutter_mozilla_components.sync.SyncedTabsIntegration
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class BackgroundServices(
|
||||
private val context: Context,
|
||||
private val browserStore: Lazy<BrowserStore>,
|
||||
private val historyStorage: Lazy<PlacesHistoryStorage>,
|
||||
private val bookmarkStorage: Lazy<PlacesBookmarksStorage>,
|
||||
private val remoteTabsStorage: Lazy<RemoteTabsStorage>,
|
||||
private val fxaServerOverride: String?,
|
||||
private val syncTokenServerOverride: String?,
|
||||
private val syncStateEvents: GeckoSyncStateEvents?,
|
||||
) {
|
||||
companion object {
|
||||
private const val MIN_STARTUP_SYNC_INTERVAL_MS = 15 * 60 * 1000L
|
||||
private val MAX_ACTIVE_TIME_MS = TimeUnit.DAYS.toMillis(14L)
|
||||
}
|
||||
|
||||
data class IncomingTab(
|
||||
val title: String,
|
||||
val url: String,
|
||||
val fromDeviceId: String?,
|
||||
val fromDeviceName: String?,
|
||||
)
|
||||
|
||||
private val incomingTabsLock = Any()
|
||||
private val incomingTabsQueue = ArrayDeque<IncomingTab>()
|
||||
private val startedSignal = CompletableDeferred<Unit>()
|
||||
private val authStateScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val authStateLock = Any()
|
||||
private var lastAuthState: SyncAccountInfo? = null
|
||||
private val isDebuggable =
|
||||
(context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||
|
||||
private val supportedEngines = setOf(
|
||||
SyncEngine.History,
|
||||
SyncEngine.Bookmarks,
|
||||
SyncEngine.Tabs,
|
||||
)
|
||||
|
||||
private val syncConfig = SyncConfig(
|
||||
supportedEngines,
|
||||
periodicSyncConfig = PeriodicSyncConfig(periodMinutes = 240),
|
||||
)
|
||||
|
||||
val syncedTabsStorage by lazy {
|
||||
SyncedTabsStorage(
|
||||
accountManager,
|
||||
browserStore.value,
|
||||
remoteTabsStorage.value,
|
||||
MAX_ACTIVE_TIME_MS,
|
||||
)
|
||||
}
|
||||
|
||||
val serverConfig: ServerConfig = FxaServer.config(
|
||||
context = context,
|
||||
serverOverride = fxaServerOverride,
|
||||
tokenServerOverride = syncTokenServerOverride,
|
||||
)
|
||||
|
||||
private val deviceConfig = DeviceConfig(
|
||||
name = "WebLibre ${Build.MANUFACTURER} ${Build.MODEL}",
|
||||
type = DeviceType.MOBILE,
|
||||
capabilities = setOf(DeviceCapability.SEND_TAB, DeviceCapability.CLOSE_TABS),
|
||||
secureStateAtRest = true,
|
||||
)
|
||||
|
||||
init {
|
||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
|
||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage)
|
||||
GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage)
|
||||
}
|
||||
|
||||
private val sequenceCounter = AtomicLong(0)
|
||||
private val syncedTabsIntegrationLaunched = AtomicBoolean(false)
|
||||
|
||||
private fun dispatchAuthState(account: OAuthAccount?, needsReauth: Boolean = false) {
|
||||
val events = syncStateEvents ?: return
|
||||
authStateScope.launch {
|
||||
val profile = account?.getProfile()
|
||||
val engineStorage = SyncEnginesStorage(context)
|
||||
val engineStatus = engineStorage.getStatus()
|
||||
|
||||
val info = SyncAccountInfo(
|
||||
authenticated = account != null && !needsReauth,
|
||||
syncing = accountManager.isSyncActive(),
|
||||
needsReauth = needsReauth,
|
||||
email = profile?.email,
|
||||
displayName = profile?.displayName,
|
||||
lastSyncedAt = getLastSynced(context).takeIf { it > 0L },
|
||||
engines = listOf(
|
||||
SyncEngineStatus(
|
||||
engine = SyncEngineValue.HISTORY,
|
||||
enabled = engineStatus[SyncEngine.History] ?: true,
|
||||
),
|
||||
SyncEngineStatus(
|
||||
engine = SyncEngineValue.BOOKMARKS,
|
||||
enabled = engineStatus[SyncEngine.Bookmarks] ?: true,
|
||||
),
|
||||
SyncEngineStatus(
|
||||
engine = SyncEngineValue.TABS,
|
||||
enabled = engineStatus[SyncEngine.Tabs] ?: true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val shouldEmit = synchronized(authStateLock) {
|
||||
if (lastAuthState == info) {
|
||||
false
|
||||
} else {
|
||||
lastAuthState = info
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldEmit) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
runOnUiThread {
|
||||
events.onAuthStateChanged(sequenceCounter.incrementAndGet(), info) { _ -> }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val accountManager: FxaAccountManager by lazy {
|
||||
FxaAccountManager(
|
||||
context = context,
|
||||
serverConfig = serverConfig,
|
||||
deviceConfig = deviceConfig,
|
||||
syncConfig = syncConfig,
|
||||
applicationScopes = setOf(SCOPE_SYNC, SCOPE_SESSION),
|
||||
crashReporter = null,
|
||||
).also { accountManager ->
|
||||
SendTabFeature(accountManager) { device: Device?, tabs: List<TabData> ->
|
||||
synchronized(incomingTabsLock) {
|
||||
tabs.forEach { tab ->
|
||||
incomingTabsQueue.addLast(
|
||||
IncomingTab(
|
||||
title = tab.title,
|
||||
url = tab.url,
|
||||
fromDeviceId = device?.id,
|
||||
fromDeviceName = device?.displayName,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accountManager.register(object : AccountObserver {
|
||||
override fun onReady(authenticatedAccount: OAuthAccount?) {
|
||||
if (!startedSignal.isCompleted) {
|
||||
startedSignal.complete(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
|
||||
dispatchAuthState(account)
|
||||
}
|
||||
|
||||
override fun onAuthenticationProblems() {
|
||||
dispatchAuthState(accountManager.authenticatedAccount(), needsReauth = true)
|
||||
}
|
||||
|
||||
override fun onLoggedOut() {
|
||||
dispatchAuthState(null)
|
||||
}
|
||||
|
||||
override fun onProfileUpdated(profile: Profile) {
|
||||
dispatchAuthState(accountManager.authenticatedAccount())
|
||||
}
|
||||
|
||||
override fun onFlowError(error: mozilla.components.concept.sync.AuthFlowError) {
|
||||
dispatchAuthState(accountManager.authenticatedAccount(), needsReauth = true)
|
||||
}
|
||||
})
|
||||
|
||||
accountManager.registerForSyncEvents(object : SyncStatusObserver {
|
||||
override fun onStarted() {
|
||||
val events = syncStateEvents ?: return
|
||||
dispatchAuthState(accountManager.authenticatedAccount())
|
||||
runOnUiThread {
|
||||
events.onSyncStarted(sequenceCounter.incrementAndGet()) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIdle() {
|
||||
val events = syncStateEvents ?: return
|
||||
dispatchAuthState(accountManager.authenticatedAccount())
|
||||
runOnUiThread {
|
||||
events.onSyncCompleted(sequenceCounter.incrementAndGet()) { _ -> }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(error: Exception?) {
|
||||
val events = syncStateEvents ?: return
|
||||
dispatchAuthState(accountManager.authenticatedAccount())
|
||||
runOnUiThread {
|
||||
events.onSyncError(
|
||||
sequenceCounter.incrementAndGet(),
|
||||
error?.message,
|
||||
) { _ -> }
|
||||
}
|
||||
}
|
||||
}, owner = ProcessLifecycleOwner.get(), autoPause = false)
|
||||
|
||||
MainScope().launch {
|
||||
runCatching {
|
||||
accountManager.start()
|
||||
}.fold(
|
||||
onSuccess = {},
|
||||
onFailure = { error ->
|
||||
val isDuplicateInitialize = error.message
|
||||
?.contains("Initialize already sent", ignoreCase = true)
|
||||
?: false
|
||||
|
||||
if (isDuplicateInitialize) {
|
||||
return@fold
|
||||
}
|
||||
|
||||
if (!startedSignal.isCompleted) {
|
||||
startedSignal.completeExceptionally(error)
|
||||
}
|
||||
throw error
|
||||
},
|
||||
)
|
||||
if (accountManager.authenticatedAccount() != null && shouldSyncOnStartup()) {
|
||||
accountManager.syncNow(SyncReason.Startup)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitStarted() {
|
||||
accountManager
|
||||
launchSyncedTabsIntegrationIfNeeded()
|
||||
withTimeout(10_000) {
|
||||
startedSignal.await()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSyncedTabsIntegrationIfNeeded() {
|
||||
if (syncedTabsIntegrationLaunched.compareAndSet(false, true)) {
|
||||
SyncedTabsIntegration(accountManager, syncedTabsStorage).launch()
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldSyncOnStartup(): Boolean {
|
||||
val lastSynced = getLastSynced(context)
|
||||
if (lastSynced <= 0L) {
|
||||
return true
|
||||
}
|
||||
|
||||
return (System.currentTimeMillis() - lastSynced) >= MIN_STARTUP_SYNC_INTERVAL_MS
|
||||
}
|
||||
|
||||
fun drainIncomingTabs(): List<IncomingTab> {
|
||||
synchronized(incomingTabsLock) {
|
||||
if (incomingTabsQueue.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val values = incomingTabsQueue.toList()
|
||||
incomingTabsQueue.clear()
|
||||
return values
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -33,6 +33,7 @@ import mozilla.components.browser.state.engine.middleware.SessionPrioritizationM
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
||||
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
|
||||
import mozilla.components.browser.storage.sync.RemoteTabsStorage
|
||||
import mozilla.components.browser.thumbnails.ThumbnailsMiddleware
|
||||
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
||||
import mozilla.components.concept.engine.DefaultSettings
|
||||
@@ -62,8 +63,11 @@ 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.concept.base.crash.Breadcrumb
|
||||
import mozilla.components.concept.base.crash.CrashReporting
|
||||
import mozilla.components.support.base.worker.Frequency
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
import kotlinx.coroutines.Job
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private const val AMO_COLLECTION_MAX_CACHE_AGE = 24 * 60L
|
||||
@@ -74,6 +78,12 @@ class Core(
|
||||
private val flutterEvents: GeckoStateEvents,
|
||||
private val extensionEvents: BrowserExtensionEvents
|
||||
) {
|
||||
private val noOpCrashReporter = object : CrashReporting {
|
||||
override fun submitCaughtException(throwable: Throwable): Job = Job()
|
||||
|
||||
override fun recordCrashBreadcrumb(breadcrumb: Breadcrumb) = Unit
|
||||
}
|
||||
|
||||
val prefs by lazy {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
}
|
||||
@@ -251,12 +261,14 @@ class Core(
|
||||
*/
|
||||
val lazyHistoryStorage = lazy { PlacesHistoryStorage(context) }
|
||||
val lazyBookmarksStorage = lazy { PlacesBookmarksStorage(context) }
|
||||
val lazyRemoteTabsStorage = lazy { RemoteTabsStorage(context, noOpCrashReporter) }
|
||||
|
||||
/**
|
||||
* A convenience accessor to the [PlacesHistoryStorage].
|
||||
*/
|
||||
val historyStorage by lazy { lazyHistoryStorage.value }
|
||||
val bookmarksStorage by lazy { lazyBookmarksStorage.value }
|
||||
val remoteTabsStorage by lazy { lazyRemoteTabsStorage.value }
|
||||
|
||||
val permissionStorage by lazy { PermissionStorage(geckoSitePermissionsStorage) }
|
||||
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package eu.weblibre.flutter_mozilla_components.components
|
||||
|
||||
import android.content.Context
|
||||
import mozilla.appservices.fxaclient.FxaServer as AppServicesFxaServer
|
||||
import mozilla.components.service.fxa.ServerConfig
|
||||
|
||||
object FxaServer {
|
||||
private const val CLIENT_ID = "a2270f727f45f648"
|
||||
const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel"
|
||||
|
||||
fun config(
|
||||
context: Context,
|
||||
serverOverride: String?,
|
||||
tokenServerOverride: String?
|
||||
): ServerConfig {
|
||||
val effectiveServerOverride = serverOverride?.trim().orEmpty()
|
||||
val effectiveTokenOverride = tokenServerOverride?.trim().takeUnless { it.isNullOrEmpty() }
|
||||
|
||||
return if (effectiveServerOverride.isEmpty()) {
|
||||
ServerConfig(AppServicesFxaServer.Release, CLIENT_ID, REDIRECT_URL, effectiveTokenOverride)
|
||||
} else {
|
||||
ServerConfig(
|
||||
AppServicesFxaServer.Custom(effectiveServerOverride),
|
||||
CLIENT_ID,
|
||||
REDIRECT_URL,
|
||||
effectiveTokenOverride,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -5,13 +5,26 @@
|
||||
package eu.weblibre.flutter_mozilla_components.components
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.core.net.toUri
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.R
|
||||
import eu.weblibre.flutter_mozilla_components.activities.AuthIntentReceiverActivity
|
||||
import eu.weblibre.flutter_mozilla_components.ext.getPreferenceKey
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature
|
||||
import mozilla.components.feature.accounts.FxaCapability
|
||||
import mozilla.components.feature.accounts.FxaWebChannelFeature
|
||||
import mozilla.components.feature.app.links.AppLinksInterceptor
|
||||
import mozilla.components.feature.tabs.TabsUseCases
|
||||
import mozilla.components.service.fxa.ServerConfig
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Component group which encapsulates foreground-friendly services.
|
||||
@@ -20,9 +33,39 @@ class Services(
|
||||
private val context: Context,
|
||||
private val store: BrowserStore,
|
||||
private val tabsUseCases: TabsUseCases,
|
||||
accountManager: FxaAccountManager,
|
||||
private val engine: Engine,
|
||||
private val serverConfig: ServerConfig,
|
||||
) {
|
||||
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
val accountsAuthFeature by lazy {
|
||||
FirefoxAccountsAuthFeature(accountManager, FxaServer.REDIRECT_URL) { _, authUrl ->
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setInstantAppsEnabled(false)
|
||||
.build()
|
||||
.intent
|
||||
.setData(authUrl.toUri())
|
||||
.setClassName(context, AuthIntentReceiverActivity::class.java.name)
|
||||
.setPackage(context.packageName)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val fxaWebChannelFeature by lazy {
|
||||
FxaWebChannelFeature(
|
||||
customTabSessionId = null,
|
||||
runtime = engine,
|
||||
store = store,
|
||||
accountManager = accountManager,
|
||||
serverConfig = serverConfig,
|
||||
fxaCapabilities = setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC),
|
||||
)
|
||||
}
|
||||
|
||||
val appLinksInterceptor by lazy {
|
||||
AppLinksInterceptor(
|
||||
context = context,
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package eu.weblibre.flutter_mozilla_components.components
|
||||
|
||||
import mozilla.components.concept.sync.FxAEntryPoint
|
||||
|
||||
enum class WebLibreFxAEntryPoint(override val entryName: String) : FxAEntryPoint {
|
||||
Settings("settings"),
|
||||
}
|
||||
+14
-1
@@ -42,6 +42,19 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
||||
return null
|
||||
}
|
||||
|
||||
components.services.accountsAuthFeature.interceptor.onLoadRequest(
|
||||
engineSession,
|
||||
uri,
|
||||
lastUri,
|
||||
hasUserGesture,
|
||||
isSameDomain,
|
||||
isRedirect,
|
||||
isDirectNavigation,
|
||||
isSubframeRequest,
|
||||
)?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
return components.services.appLinksInterceptor.onLoadRequest(
|
||||
engineSession,
|
||||
uri,
|
||||
@@ -64,4 +77,4 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
|
||||
}
|
||||
|
||||
override fun interceptsAppInitiatedRequests() = true
|
||||
}
|
||||
}
|
||||
|
||||
+805
-141
File diff suppressed because it is too large
Load Diff
+35
@@ -0,0 +1,35 @@
|
||||
package eu.weblibre.flutter_mozilla_components.sync
|
||||
|
||||
import mozilla.components.concept.sync.AccountObserver
|
||||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
|
||||
import mozilla.components.service.fxa.manager.FxaAccountManager
|
||||
|
||||
/**
|
||||
* Starts and stops SyncedTabsStorage based on the authentication state.
|
||||
*/
|
||||
class SyncedTabsIntegration(
|
||||
private val accountManager: FxaAccountManager,
|
||||
private val syncedTabsStorage: SyncedTabsStorage,
|
||||
) {
|
||||
fun launch() {
|
||||
accountManager.register(SyncedTabsAccountObserver(syncedTabsStorage))
|
||||
|
||||
if (accountManager.authenticatedAccount() != null) {
|
||||
syncedTabsStorage.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class SyncedTabsAccountObserver(
|
||||
private val syncedTabsStorage: SyncedTabsStorage,
|
||||
) : AccountObserver {
|
||||
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
|
||||
syncedTabsStorage.start()
|
||||
}
|
||||
|
||||
override fun onLoggedOut() {
|
||||
syncedTabsStorage.stop()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user