IJPL-165434 Provide ability to sync with additional providers in Android Studio

(cherry picked from commit 656ed2562377be73a7b49b965bfb42c187ce69ca)
Signed-off-by: Sergey Pak <sergey.pak@jetbrains.com>

GitOrigin-RevId: 6dba33081f0acb3ca5ba1fc89bb4ed08b624c280
This commit is contained in:
Sergey Pak
2025-01-23 01:22:16 +00:00
committed by intellij-monorepo-bot
parent de367a30ba
commit 5b7c2d071b
49 changed files with 1831 additions and 620 deletions
+1
View File
@@ -919,6 +919,7 @@
<module fileurl="file://$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.iml" filepath="$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.tests.iml" filepath="$PROJECT_DIR$/plugins/settings-repository/intellij.settingsRepository.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/settings-sync/intellij.settingsSync.iml" filepath="$PROJECT_DIR$/plugins/settings-sync/intellij.settingsSync.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/settings-sync/fileSystem/intellij.settingsSync.fileSystem.iml" filepath="$PROJECT_DIR$/plugins/settings-sync/fileSystem/intellij.settingsSync.fileSystem.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/settings-sync/git/intellij.settingsSync.git.iml" filepath="$PROJECT_DIR$/plugins/settings-sync/git/intellij.settingsSync.git.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/settings-sync/jba/intellij.settingsSync.jba.iml" filepath="$PROJECT_DIR$/plugins/settings-sync/jba/intellij.settingsSync.jba.iml" />
<module fileurl="file://$PROJECT_DIR$/plugins/sh/intellij.sh.iml" filepath="$PROJECT_DIR$/plugins/sh/intellij.sh.iml" />
+1
View File
@@ -150,6 +150,7 @@
<orderEntry type="module" module-name="intellij.settingsSync" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.settingsSync.git" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.settingsSync.jba" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.settingsSync.fileSystem" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.java.featuresTrainer" scope="RUNTIME" />
<orderEntry type="module" module-name="intellij.idea.community.build.tasks" scope="TEST" />
<orderEntry type="module" module-name="intellij.junit.v5.rt.tests" scope="TEST" />
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/testResources" type="java-test-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.settingsSync" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.ide.core" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
</component>
</module>
@@ -0,0 +1,10 @@
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<idea-plugin package="intellij.settingsSync.fileSystem">
<extensions defaultExtensionNs="com.intellij">
<settingsSync.communicatorProvider implementation="intellij.settingsSync.fileSystem.FSCommunicatorProvider"/>
</extensions>
<actions resource-bundle="messages.BackupNSyncFSBundle">
<action class="intellij.settingsSync.fileSystem.EnableBackupNSyncRemotely" id="settingsSync.enableRemotely"
icon="AllIcons.General.Settings"/>
</actions>
</idea-plugin>
@@ -0,0 +1 @@
action.settingsSync.enableRemotely.text = Enable Backup Sync (from FS)
@@ -0,0 +1,52 @@
package intellij.settingsSync.fileSystem
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.fileChooser.FileChooserFactory
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.settingsSync.SettingsSyncLocalSettings
import com.intellij.settingsSync.SettingsSyncSettings
import com.intellij.settingsSync.UpdateResult.*
import com.intellij.settingsSync.config.SettingsSyncEnabler
import com.intellij.util.containers.toMutableSmartList
class EnableBackupNSyncRemotely : DumbAwareAction() {
override fun getActionUpdateThread() = ActionUpdateThread.BGT
override fun actionPerformed(e: AnActionEvent) {
ApplicationManager.getApplication().invokeLater {
val folderDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
.withTitle("Select root folder")
val chooser = FileChooserFactory.getInstance().createPathChooser(folderDescriptor, null, null)
chooser.choose(null) {
val path = it.single().path
val availableAccounts = (PropertiesComponent.getInstance().getList("FSAuthServiceAccounts") ?: emptyList()).toMutableSmartList()
availableAccounts.add(path)
SettingsSyncLocalSettings.getInstance().userId = path
SettingsSyncLocalSettings.getInstance().providerCode = "fs"
PropertiesComponent.getInstance().setList("FSAuthServiceAccounts", availableAccounts)
}
SettingsSyncSettings.getInstance().syncEnabled = true
val enabler = SettingsSyncEnabler()
val serverState = enabler.getServerState()
when (serverState) {
is NoFileOnServer, FileDeletedFromServer -> {
enabler.pushSettingsToServer()
}
is Success -> {
enabler.getSettingsFromServer(null)
}
is Error -> {
logger<EnableBackupNSyncRemotely>().error(serverState.message)
}
}
}
}
}
@@ -0,0 +1,80 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package intellij.settingsSync.fileSystem
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.EDT
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.fileChooser.FileChooserFactory
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.settingsSync.communicator.SettingsSyncUserData
import com.intellij.util.containers.mapSmart
import com.intellij.util.containers.toMutableSmartList
import kotlinx.coroutines.*
import java.awt.Component
import java.nio.file.Path
import javax.swing.Icon
import kotlin.io.path.absolutePathString
import kotlin.io.path.name
import kotlin.coroutines.resume
internal class FSAuthService : SettingsSyncAuthService {
override val providerCode: String
get() = "fs"
override val providerName: String
get() = "File System"
override val icon: Icon?
get() = com.intellij.icons.AllIcons.Actions.ModuleDirectory
override suspend fun login(parentComponent: Component?): SettingsSyncUserData? {
val modalTaskOwner = if (parentComponent != null)
ModalTaskOwner.component(parentComponent)
else
ModalTaskOwner.guess()
return withModalProgress(modalTaskOwner, "Getting data", TaskCancellation.cancellable(), ) {
val folderDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
.withTitle("Select root folder")
withContext(Dispatchers.EDT) {
val chooser = FileChooserFactory.getInstance().createPathChooser(folderDescriptor, null, null)
suspendCancellableCoroutine<SettingsSyncUserData?> { cont ->
cont.invokeOnCancellation {
cont.resume(null)
}
chooser.choose(null) {
val path = it.single().path
val userData = userDataFromPath(path)
val availableAccounts = (PropertiesComponent.getInstance().getList("FSAuthServiceAccounts") ?: emptyList()).toMutableSmartList()
if (!availableAccounts.contains(path)) {
availableAccounts.add(path)
PropertiesComponent.getInstance().setList("FSAuthServiceAccounts", availableAccounts)
}
cont.resume(userData)
}
}
}
}
}
override fun getUserData(userId: String): SettingsSyncUserData? {
return getAvailableUserAccounts().find { it.id == userId }
}
override fun getAvailableUserAccounts(): List<SettingsSyncUserData> {
return PropertiesComponent.getInstance().getList("FSAuthServiceAccounts")?.mapSmart { userDataFromPath(it) } ?: emptyList()
}
private fun userDataFromPath(pathStr: String) : SettingsSyncUserData {
val path = Path.of(pathStr)
return SettingsSyncUserData(
path.absolutePathString(),
providerCode,
path.name,
"noname@email.com"
)
}
}
@@ -0,0 +1,59 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package intellij.settingsSync.fileSystem
import com.intellij.openapi.diagnostic.logger
import com.intellij.settingsSync.*
import java.io.InputStream
import java.nio.file.Path
import kotlin.io.path.getLastModifiedTime
internal class FSCommunicator(override val userId: String) : AbstractServerCommunicator() {
private val basePath: Path = Path.of(userId)
companion object {
private val LOG = logger<FSCommunicator>()
}
override fun requestSuccessful() {
LOG.info("requestSuccessful")
}
override fun handleRemoteError(e: Throwable): String {
LOG.warn("remote error occurred", e)
return e.message ?: "Remote error occurred: ${e.javaClass.name}"
}
override fun readFileInternal(filePath: String): Pair<InputStream?, String?> {
val path = basePath.resolve(filePath)
val file = path.toFile()
if (file.exists()) {
return Pair(file.inputStream(), path.getLastModifiedTime().toString())
}
return Pair(null, null)
}
override fun writeFileInternal(filePath: String, versionId: String?, content: InputStream): String? {
val path = basePath.resolve(filePath)
if (path.toFile().exists() && path.getLastModifiedTime().toString() != versionId) {
throw InvalidVersionIdException("Expected versionId is $versionId, but actual is ${path.getLastModifiedTime()}")
}
if (!path.parent.toFile().exists()) {
path.parent.toFile().mkdirs()
}
path.toFile().outputStream().use { content.copyTo(it) }
return path.getLastModifiedTime().toString()
}
override fun getLatestVersion(filePath: String): String? {
val path = basePath.resolve(filePath)
if (!path.toFile().exists())
return null
return path.getLastModifiedTime().toString()
}
override fun deleteFileInternal(filePath: String) {
val path = basePath.resolve(filePath)
path.toFile().delete()
}
}
@@ -0,0 +1,21 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package intellij.settingsSync.fileSystem
import com.intellij.settingsSync.SettingsSyncRemoteCommunicator
import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.settingsSync.communicator.SettingsSyncCommunicatorProvider
class FSCommunicatorProvider : SettingsSyncCommunicatorProvider {
private val authServiceLazy = lazy<FSAuthService> { FSAuthService() }
override val providerCode: String
get() = "fs"
override val authService: SettingsSyncAuthService
get() = authServiceLazy.value
override fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator? {
return FSCommunicator(userId)
}
}
@@ -56,5 +56,6 @@
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" scope="TEST" />
<orderEntry type="module" module-name="intellij.performanceTesting" />
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
<orderEntry type="module" module-name="intellij.platform.ide.observable" />
</component>
</module>
@@ -20,5 +20,6 @@
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
<orderEntry type="module" module-name="intellij.platform.util.progress" />
</component>
</module>
@@ -1,4 +1,5 @@
action.settingsSync.troubleShoot.text=Backup and Sync Troubleshooting
login.manual.helper.text=In case of troubles please log in via Help->Register Plugins
troubleshooting.loading.info.progress.dialog.title=Loading information about Backup and Sync\u2026
troubleshooting.dialog.title=Backup and Sync Troubleshooting
@@ -56,8 +56,8 @@ internal open class CloudConfigServerCommunicator(serverUrl: String? = null,
}
@Throws(IOException::class)
override fun readFileInternal(snapshotFilePath: String): Pair<InputStream?, String?> {
return clientVersionContext.doWithVersion(snapshotFilePath, null) { filePath ->
override fun readFileInternal(filePath: String): Pair<InputStream?, String?> {
return clientVersionContext.doWithVersion(filePath, null) { filePath ->
try {
val stream = client.read(filePath)
@@ -122,6 +122,9 @@ internal open class CloudConfigServerCommunicator(serverUrl: String? = null,
client.delete(filePath)
}
override val userId: String
get() = "jba"
override fun writeFileInternal(filePath: String, versionId: String?, content: InputStream) : String? {
return clientVersionContext.doWithVersion(filePath, versionId) { filePath ->
@@ -12,12 +12,13 @@ class JbaCommunicatorProvider : SettingsSyncCommunicatorProvider, Disposable {
override val providerCode: String
get() = "jba"
override val authService: SettingsSyncAuthService
get() {
return authServiceLazy.value
}
override fun createCommunicator(): SettingsSyncRemoteCommunicator? = lazy<CloudConfigServerCommunicator> {
override fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator = lazy<CloudConfigServerCommunicator> {
CloudConfigServerCommunicator(null, authServiceLazy.value)
}.value
@@ -0,0 +1,16 @@
package com.intellij.settingsSync.jba
import com.intellij.DynamicBundle
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.PropertyKey
private const val PATH_TO_BUNDLE = "messages.SettingsSyncJbaBundle"
@ApiStatus.Internal
object SettingsSyncJbaBundle {
private val bundle = DynamicBundle(SettingsSyncJbaBundle::class.java, PATH_TO_BUNDLE)
fun message(@PropertyKey(resourceBundle = PATH_TO_BUNDLE) key: String, vararg params: Any) : @Nls String {
return bundle.getMessage(key, *params)
}
}
@@ -79,7 +79,7 @@ class SettingsSyncPromotion : SettingsDialogListener {
SettingsSyncEvents.Companion.getInstance().addListener(object : SettingsSyncEventListener {
override fun loginStateChanged() {
if (RemoteCommunicatorHolder.getAuthService().isLoggedIn()) {
if (RemoteCommunicatorHolder.getCurrentUserData() != null) {
SettingsSyncEventsStatistics.PROMOTION_IN_SETTINGS.log(SettingsSyncEventsStatistics.PromotionInSettingsEvent.LOGGED_IN)
}
}
@@ -64,7 +64,12 @@ internal class SettingsSyncTroubleshootingAction : DumbAwareAction() {
}
override fun actionPerformed(e: AnActionEvent) {
val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator()
val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run {
Messages.showErrorDialog(e.project,
"No remote communicator available",
SettingsSyncBundle.message("troubleshooting.dialog.title"))
return
}
if (remoteCommunicator !is CloudConfigServerCommunicator) {
Messages.showErrorDialog(e.project,
SettingsSyncBundle.message("troubleshooting.dialog.error.wrong.configuration", remoteCommunicator::class),
@@ -147,7 +152,7 @@ internal class SettingsSyncTroubleshootingAction : DumbAwareAction() {
val remoteCommunicator: CloudConfigServerCommunicator,
val rootNode: TreeNode.Branch) : DialogWrapper(project, true) {
val userData = RemoteCommunicatorHolder.getAuthService().getUserData()
val userData = RemoteCommunicatorHolder.getCurrentUserData()
init {
title = SettingsSyncBundle.message("troubleshooting.dialog.title")
@@ -273,7 +278,7 @@ internal class SettingsSyncTroubleshootingAction : DumbAwareAction() {
if (showHistoryButton) {
actionButton(object : DumbAwareAction(AllIcons.Vcs.History) {
override fun actionPerformed(e: AnActionEvent) {
showHistoryDialog(project, remoteCommunicator, version.filePath, userData.name!!)
showHistoryDialog(project, remoteCommunicator, version.filePath, userData?.name ?: "Unknown")
}
})
}
@@ -22,7 +22,15 @@ object DummyJBAccountInfoService : JBAccountInfoService {
}
override fun startLoginSession(loginMode: JBAccountInfoService.LoginMode, authProviderId: String?, clientMetadata: Map<String, String>): JBAccountInfoService.LoginSession {
TODO("Not yet implemented")
return object : JBAccountInfoService.LoginSession {
override fun close() {
}
override fun onCompleted(): CompletableFuture<JBAccountInfoService.LoginResult> {
return CompletableFuture.completedFuture(JBAccountInfoService.LoginResult.LoginSuccessful(dummyUserData))
}
}
}
override fun getAvailableLicenses(productCode: String): CompletableFuture<JBAccountInfoService.LicenseListResult> {
@@ -1,32 +1,38 @@
package com.intellij.settingsSync.jba.auth
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.ex.ApplicationManagerEx
import com.intellij.openapi.diagnostic.logger
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.settingsSync.SettingsSyncEvents
import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.settingsSync.communicator.SettingsSyncUserData
import com.intellij.settingsSync.jba.SettingsSyncJbaBundle
import com.intellij.settingsSync.jba.SettingsSyncPromotion
import com.intellij.ui.JBAccountInfoService
import java.util.function.Consumer
import kotlinx.coroutines.*
import java.awt.Component
import java.util.concurrent.CancellationException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
internal class JBAAuthService : SettingsSyncAuthService {
companion object {
private val LOG = logger<JBAAuthService>()
private const val JBA_USER_ID = "jba"
}
@Volatile
private var invalidatedIdToken: String? = null
override fun isLoggedIn(): Boolean {
return isTokenValid(getAccountInfoService()?.idToken)
}
private fun isTokenValid(token: String?): Boolean {
return token != null && token != invalidatedIdToken
}
override fun getUserData() = fromJBAData(
override fun getUserData(userId: String) = fromJBAData(
if (ApplicationManagerEx.isInIntegrationTest()) {
DummyJBAccountInfoService.userData
} else {
@@ -34,12 +40,23 @@ internal class JBAAuthService : SettingsSyncAuthService {
}
)
private fun fromJBAData(jbaData: JBAccountInfoService.JBAData?) : SettingsSyncUserData {
override fun getAvailableUserAccounts(): List<SettingsSyncUserData> {
val userData = getUserData("")
if (userData != null) {
return listOf(userData)
} else {
return emptyList()
}
}
private fun fromJBAData(jbaData: JBAccountInfoService.JBAData?) : SettingsSyncUserData? {
if (jbaData == null) {
return SettingsSyncUserData.EMPTY
return null
} else {
return SettingsSyncUserData(
jbaData.loginName,
JBA_USER_ID,
providerCode,
jbaData.email,
jbaData.email,
)
}
@@ -53,9 +70,17 @@ internal class JBAAuthService : SettingsSyncAuthService {
}
override val providerCode: String
get() = "jba"
override val providerName: String
get() = "JetBrains"
override fun login() {
if (!isLoggedIn()) {
override val icon = AllIcons.Ultimate.IdeaUltimatePromo
override suspend fun login(parentComponent: Component?): SettingsSyncUserData? {
val modalTaskOwner = if (parentComponent != null)
ModalTaskOwner.component(parentComponent)
else
ModalTaskOwner.guess()
return withModalProgress(modalTaskOwner, SettingsSyncJbaBundle.message("login.manual.helper.text"), TaskCancellation.cancellable()) {
val accountInfoService = getAccountInfoService()
val loginMetadata = hashMapOf(
"from.settings.sync" to "true"
@@ -63,25 +88,55 @@ internal class JBAAuthService : SettingsSyncAuthService {
if (SettingsSyncPromotion.promotionShownThisSession) {
loginMetadata["from.settings.sync.promotion"] = "true"
}
if (accountInfoService != null) {
if (accountInfoService == null) {
LOG.error("JBA auth service is not available!")
return@withModalProgress null
}
if (isTokenValid(accountInfoService.idToken)) {
return@withModalProgress fromJBAData(accountInfoService.userData)
}
suspendCancellableCoroutine<SettingsSyncUserData?> { cont ->
try {
val loginSession: JBAccountInfoService.LoginSession? = accountInfoService.startLoginSession(
JBAccountInfoService.LoginMode.AUTO, null, loginMetadata)
loginSession!!.onCompleted().thenAccept(Consumer<JBAccountInfoService.LoginResult> {
SettingsSyncEvents.getInstance().fireLoginStateChanged()
})
cont.invokeOnCancellation {
cont.resume(null)
}
accountInfoService
.startLoginSession(JBAccountInfoService.LoginMode.AUTO, null, loginMetadata)
.onCompleted()
.exceptionally { exc ->
if (exc is CancellationException) {
LOG.warn("Login cancelled")
}
else {
LOG.warn("Login failed", exc)
}
cont.resume(null)
null
}.thenApply { loginResult ->
val result: SettingsSyncUserData? = when (loginResult) {
is JBAccountInfoService.LoginResult.LoginSuccessful -> {
fromJBAData(loginResult.jbaUser)
}
is JBAccountInfoService.LoginResult.LoginFailed -> {
LOG.warn("Login failed: ${loginResult.errorMessage}")
null
}
else -> {
LOG.warn("Unknown login result: $loginResult")
null
}
}
cont.resume(result)
}
}
catch (e: Throwable) {
LOG.error(e)
SettingsSyncEvents.getInstance().fireLoginStateChanged()
cont.resumeWithException(e)
}
}
}
}
override fun isLoginAvailable(): Boolean = getAccountInfoService() != null
fun invalidateJBA(idToken: String) {
if (invalidatedIdToken == idToken) return
@@ -2,6 +2,7 @@ package com.intellij.settingsSync
import com.intellij.idea.TestFor
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.settingsSync.jba.CloudConfigServerCommunicator
import com.intellij.settingsSync.jba.CloudConfigVersionContext
@@ -72,11 +73,14 @@ internal class SettingsSyncAuthTest : BasePlatformTestCase() {
communicator.checkServerState()
verify(authServiceSpy, times(1)).invalidateJBA("OLD-ID-TOKEN")
assertFalse(authServiceSpy.isLoggedIn())
TODO("assertFalse(authServiceSpy.isLoggedIn())")
// User updates JBA details
`when`(accountInfoService.idToken).thenReturn("NEW-ID-TOKEN")
authServiceSpy.login()
runBlockingCancellable {
authServiceSpy.login(null)
}
// Check that userId was updated in communicator
communicator.checkServerState()
@@ -115,7 +119,7 @@ internal class SettingsSyncAuthTest : BasePlatformTestCase() {
}
communicator.checkServerState()
assertFalse(authServiceSpy.isLoggedIn())
TODO("assertFalse(authServiceSpy.isLoggedIn())")
assertFalse(SettingsSyncSettings.getInstance().syncEnabled)
}
}
@@ -4,13 +4,14 @@ import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.settingsSync.communicator.SettingsSyncUserData
import com.intellij.settingsSync.jba.auth.DummyJBAccountInfoService
import com.intellij.ui.JBAccountInfoService
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import java.awt.Component
import javax.swing.Icon
internal class SettingsSyncTestAuthService : SettingsSyncAuthService {
override fun isLoggedIn(): Boolean {
return true
}
override fun getUserData(): SettingsSyncUserData {
override fun getUserData(userId: String): SettingsSyncUserData {
val id = System.getenv("SETTINGS_SYNC_TEST_ID")
val loginName = "testLogin"
val email = "testEmail@example.com"
@@ -18,6 +19,10 @@ internal class SettingsSyncTestAuthService : SettingsSyncAuthService {
return SettingsSyncUserData(loginName, email)
}
override fun getAvailableUserAccounts(): List<SettingsSyncUserData> {
TODO("Not yet implemented")
}
fun getAccountInfoService(): JBAccountInfoService {
return DummyJBAccountInfoService
}
@@ -27,11 +32,12 @@ internal class SettingsSyncTestAuthService : SettingsSyncAuthService {
override val providerCode: String
get() = TODO("Not yet implemented")
override val providerName: String
get() = TODO("Not yet implemented")
override val icon: Icon?
get() = TODO("Not yet implemented")
override fun login() {
}
override fun isLoginAvailable(): Boolean {
return false
override suspend fun login(parentComponent: Component?) : SettingsSyncUserData? {
return null
}
}
@@ -12,6 +12,7 @@
<content>
<module name="intellij.settingsSync.git"/>
<module name="intellij.settingsSync.jba"/>
<!--<module name="intellij.settingsSync.fileSystem"/>-->
</content>
<depends>com.intellij.modules.platform</depends>
@@ -0,0 +1,4 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="2" fill="#818594"/>
</svg>

After

Width:  |  Height:  |  Size: 275 B

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="2" fill="#CED0D6"/>
</svg>

After

Width:  |  Height:  |  Size: 275 B

@@ -41,16 +41,28 @@ config.button.login=Log in with JetBrains Account\u2026
# config.button.logout=Log Out...
config.button.enable=Enable Backup and Sync\u2026
config.button.disable=Disable Backup and Sync\u2026
enable.dialog.select.what.to.sync=Select what settings to sync:
enable.dialog.select.what.to.sync=Settings to sync
enable.dialog.enable.sync.action=Enable Backup and Sync
enable.dialog.source.option.title=Choose Settings Source
enable.dialog.source.option.text=There already are existing settings on this account. Please choose which settings you would like to use as the basis for sync going forward.
enable.dialog.sync.local.settings=Push Settings to Account
enable.dialog.sync.local.settings.option=Use the local settings and upload them to your account storage
enable.dialog.sync.local.settings.text=Settings saved on your account will be overwritten by the local ones
enable.dialog.get.settings.from.account=Get Settings from Account
enable.sync.check.server.data.progress=Checking Server Data\u2026
enable.dialog.get.settings.from.account.option=Use the settings from your account storage
enable.dialog.get.settings.from.account.text=Your local settings will be overwritten by the settings on your account
enable.dialog.change=Change
enable.sync.check.server.data.progress=Checking server data\u2026
enable.sync.get.from.server.progress=Getting Settings from Server\u2026
enable.sync.push.to.server.progress=Pushing Settings to Server\u2026
disable.dialog.title=Disable Backup and Sync?
disable.dialog.text=The settings will not be synced anymore
disable.dialog.remove.data.box=Remove data from JB account and disable for all IDEs
enable.sync.add.account=Add Account
enable.sync.choose.data.provider.title=Choose Provider
enable.sync.choose.data.provider.text=Please select a provider that will store and process your data
disable.dialog.title=Confirm disabling Settings Sync
# {0} - account type, i.e. JetBrains or Google
disable.dialog.text=This will disable Settings Sync for this installation,\nleaving the data stored on the {0} account intact.\nThis change will take effect immediately.
# {0} - account type, i.e. JetBrains or Google
disable.dialog.remove.data.box=Remove data from {0} account and disable for all IDEs
disable.dialog.disable.button=Disable
disable.remove.data.title=Removing Server Data\u2026
disable.remove.data.failure=Unable to remove data from server
@@ -66,6 +78,7 @@ subcategory.config.link=Configure
settings.category.ui.editor.font=Editor font
settings.sync.info.message=Sync UI, Code and System settings, Keymaps, Plugins, and Tools.
settings.sync.select.provider.message=To enable Backup and Sync, select a provider that will store and process your data
sync.restart.notification.title=Restart required after syncing settings
# {0} - count of plugins, {1} - coma separated list of plugins (max two plugins)
@@ -83,12 +96,14 @@ sync.notification.restart.message.list.entry.plugin.disable=Disable {0,choice,1#
sync.restart.notification.submessage.plugins={0} plugin(s): {1}\u2026
# {0} - IDE name, i.e. Android Studio, MPS, etc.
sync.restart.notification.action=Restart {0}
sync.status.enabled=Backup and Sync enabled
sync.status.enabled=Backup and Sync enabled for
sync.status.disabled=Backup and Sync disabled
sync.status.failed=Sync failed.
sync.status.login.message=Login to enable Backup and Sync
# {0} is last sync time (human-readable), {1} is a user name, for example: "Last synced 5 minutes ago for John.Doe"
sync.status.last.sync.message=Last synced {0} for {1}.
# {0} is last sync time (human-readable), after (in a different component) goes a user-name, for example: "Last synced 5 minutes ago for John.Doe"
sync.status.last.sync.message=Synced {0} for
sync.status.disabled.message=Disabled for
sync.login.message=Logging in\u2026
# The full (original) message is "..., install JetBrains Marketplace Licensing Support"
# but the plugin name is rendered in a separate component and doesn't need to be translated
sync.status.login.not.available=To enable Backup and Sync, install
@@ -15,13 +15,75 @@ abstract class AbstractServerCommunicator() : SettingsSyncRemoteCommunicator {
private val LOG = logger<AbstractServerCommunicator>()
}
private var myTemporary = false
override fun setTemporary(isTemporary: Boolean) {
myTemporary = isTemporary
}
/**
* called when a request is successful, as an opposite to handleRemoteError
*/
protected open fun requestSuccessful() {}
/**
* Handles errors that occur during remote operations by mapping the given `Throwable` to a meaningful error message.
*
* @param e The exception or error that occurred during a remote operation.
* @return A string describing the error in a human-readable format, suitable for logging or display.
*/
protected abstract fun handleRemoteError(e: Throwable): String
/**
* Reads the content of a file from the given file path.
*
* @param filePath The path of the file to be read.
* @return A pair containing:
* - An InputStream of the file content if the operation is successful, otherwise null.
* - A server version identifier associated with the file, or null if unavailable.
* @throws IOException If an I/O error occurs during file reading.
*/
@Throws(IOException::class)
protected abstract fun readFileInternal(filePath: String): Pair<InputStream?, String?>
/**
* Writes the content to a specified file, potentially associated with a particular version ID.
*
* @param filePath The path to the file where the content will be written.
* @param versionId An optional version identifier for the file. If provided, the version ID must match
* the version on server; otherwise, an InvalidVersionIdException may occur.
* @param content The content to be written to the file, provided as an InputStream.
* @return The version ID of the file after the content is written, or null if the operation does not result
* in an identifiable version.
* @throws IOException If an I/O error occurs during the writing process.
* @throws InvalidVersionIdException If the provided(expected) version ID doesn't match the actual remote one.
*/
@Throws(IOException::class, InvalidVersionIdException::class)
protected abstract fun writeFileInternal(filePath: String, versionId: String?, content: InputStream): String?
/**
* Fetches the latest version identifier for the file at the specified file path.
*
* This version is compared against SettingsSyncLocalSettings.getKnownAndAppliedServerId
* to check whether settings version on server is different from the local one.
*
* @param filePath The path to the file whose latest version is to be retrieved.
* @return The latest version identifier of the file, or null if no version information is available.
* @throws IOException If an I/O error occurs while attempting to retrieve the version information.
*/
@Throws(IOException::class)
protected abstract fun getLatestVersion(filePath: String) : String?
@Throws(IOException::class)
protected abstract fun deleteFileInternal(filePath: String)
@VisibleForTesting
@Throws(IOException::class, SecurityException::class)
protected fun currentSnapshotFilePath(): Pair<String, Boolean>? {
try {
val crossIdeSyncEnabled = isFileExists(CROSS_IDE_SYNC_MARKER_FILE)
if (crossIdeSyncEnabled != SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled) {
if (!myTemporary && crossIdeSyncEnabled != SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled) {
LOG.info("Cross-IDE sync status on server is: ${enabledOrDisabled(crossIdeSyncEnabled)}. Updating local settings with it.")
SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled = crossIdeSyncEnabled
}
@@ -63,7 +125,7 @@ abstract class AbstractServerCommunicator() : SettingsSyncRemoteCommunicator {
if (force) {
// get the latest server version: pushing with it will overwrite the file in any case
versionToPush = getLatestVersion(snapshotFilePath)
writeFileInternal(snapshotFilePath, null, inputStream)
writeFileInternal(snapshotFilePath, versionToPush, inputStream)
}
else {
if (knownServerVersion != null) {
@@ -172,22 +234,6 @@ abstract class AbstractServerCommunicator() : SettingsSyncRemoteCommunicator {
}
}
protected abstract fun requestSuccessful()
protected abstract fun handleRemoteError(e: Throwable): String
@Throws(IOException::class)
protected abstract fun readFileInternal(snapshotFilePath: String): Pair<InputStream?, String?>
@Throws(IOException::class, InvalidVersionIdException::class)
protected abstract fun writeFileInternal(filePath: String, versionId: String?, content: InputStream): String?
@Throws(IOException::class)
protected abstract fun getLatestVersion(filePath: String) : String?
@Throws(IOException::class)
protected abstract fun deleteFileInternal(filePath: String)
override fun createFile(filePath: String, content: String) {
writeFileInternal(filePath, null, content.byteInputStream())
}
@@ -34,7 +34,7 @@ class SettingsSyncBridge(
private val pendingEvents = ContainerUtil.createConcurrentList<SyncSettingsEvent.StandardEvent>()
private val remoteCommunicator: SettingsSyncRemoteCommunicator
get() = RemoteCommunicatorHolder.getRemoteCommunicator()
get() = RemoteCommunicatorHolder.getRemoteCommunicator() ?: DummyCommunicator
@Volatile
private var queueJob: Job? = null
@@ -73,6 +73,7 @@ class SettingsSyncBridge(
}
}
while (!locked)
LOG.info("Lock obtained for exclusive event")
processExclusiveEvent(event)
}
catch (th: Throwable) {
@@ -83,6 +84,7 @@ class SettingsSyncBridge(
if (!eventsLock.compareAndSet(true, false)) {
LOG.error("eventsLock already unlocked by someone else!!!")
}
LOG.info("Lock released for exclusive event")
}
pendingExclusiveEvents.remove(event)
}
@@ -563,11 +565,54 @@ class SettingsSyncBridge(
}
}
@TestOnly
internal fun stop() {
stopSyncingAndRollback(null, null)
}
private object DummyCommunicator : SettingsSyncRemoteCommunicator {
override val userId: String
get() = ""
override fun setTemporary(isTemporary: Boolean) {
// do nothing
}
override fun checkServerState(): ServerState {
val errorMsg = "Cannot check server state - no communicator provided"
LOG.info(errorMsg)
return ServerState.Error(errorMsg)
}
override fun receiveUpdates(): UpdateResult {
val errorMsg = "Cannot received updates - no communicator provided"
LOG.info(errorMsg)
return UpdateResult.Error(errorMsg)
}
override fun push(snapshot: SettingsSnapshot, force: Boolean, expectedServerVersionId: String?): SettingsSyncPushResult {
val errorMsg = "Cannot push - no communicator provided"
LOG.info(errorMsg)
return SettingsSyncPushResult.Error(errorMsg)
}
override fun createFile(filePath: String, content: String) {
LOG.info("Cannot create file '$filePath' - no communicator provided")
}
override fun deleteFile(filePath: String) {
LOG.info("Cannot delete file '$filePath' - no communicator provided")
}
override fun isFileExists(filePath: String): Boolean {
LOG.info("Cannot check if file '$filePath' exists - no communicator provided")
return false;
}
}
companion object {
private val LOG = logger<SettingsSyncBridge>()
}
@@ -9,6 +9,8 @@ interface SettingsSyncLocalState {
val applicationId: UUID
var knownAndAppliedServerId: String?
var isCrossIdeSyncEnabled: Boolean
var userId: String?
var providerCode: String?
}
@State(name = "SettingsSyncLocalSettings", storages = [Storage("settingsSyncLocal.xml", roamingType = RoamingType.DISABLED)])
@@ -23,12 +25,16 @@ class SettingsSyncLocalSettings : SimplePersistentStateComponent<SettingsSyncLoc
var applicationId: String? by string(UUID.randomUUID().toString())
var knownAndAppliedServerId: String? by string(null)
var crossIdeSyncEnabled by property(false)
var userId by string(null)
var providerCode by string(null)
@TestOnly
internal fun reset() {
applicationId = UUID.randomUUID().toString()
knownAndAppliedServerId = null
crossIdeSyncEnabled = false
userId = null
providerCode = null
}
}
@@ -36,6 +42,8 @@ class SettingsSyncLocalSettings : SimplePersistentStateComponent<SettingsSyncLoc
applicationId = newState.applicationId
knownAndAppliedServerId = newState.knownAndAppliedServerId
isCrossIdeSyncEnabled = newState.isCrossIdeSyncEnabled
userId = newState.userId
providerCode = newState.providerCode
}
override var applicationId: UUID
@@ -55,6 +63,18 @@ class SettingsSyncLocalSettings : SimplePersistentStateComponent<SettingsSyncLoc
set(value) {
state.crossIdeSyncEnabled = value
}
override var userId: String?
get() = state.userId
set(value) {
state.userId = value
}
override var providerCode: String?
get() = state.providerCode
set(value) {
state.providerCode = value
}
}
// Temporary non-persistent form state akin to `SettingsSyncSettings`'s `SettingsSyncStateHolder`
@@ -83,4 +103,17 @@ class SettingsSyncLocalStateHolder(
set(value) {
state.crossIdeSyncEnabled = value
}
override var userId: String?
get() = state.userId
set(value) {
state.userId = value
}
override var providerCode: String?
get() = state.providerCode
set(value) {
state.providerCode = value
}
}
@@ -65,7 +65,10 @@ class SettingsSyncMain(coroutineScope: CoroutineScope) : Disposable {
ideMediator: SettingsSyncIdeMediator,
): SettingsSyncControls {
val settingsLog = GitSettingsLog(settingsSyncStorage, appConfigPath, parentDisposable,
RemoteCommunicatorHolder.getAuthService()::getUserData,
{
val userId = RemoteCommunicatorHolder.getRemoteCommunicator()?.userId ?: return@GitSettingsLog null
RemoteCommunicatorHolder.getAuthService()?.getUserData(userId)
},
initialSnapshotProvider = { currentSnapshot ->
ideMediator.getInitialSnapshot(appConfigPath, currentSnapshot)
})
@@ -18,6 +18,19 @@ const val SETTINGS_SYNC_SNAPSHOT_ZIP = "$SETTINGS_SYNC_SNAPSHOT.zip"
@ApiStatus.Internal
interface SettingsSyncRemoteCommunicator {
/**
* userId used within the communicator
*/
val userId: String
/**
* Indicates whether the communicator is temporary (i.e. used to check a remote state before enabling)
* and shouldn't update configuration values
*/
fun setTemporary(isTemporary: Boolean)
/**
* checks the current state of the user's data in the cloud.
*/
@@ -60,6 +60,12 @@ class SettingsSyncSettings : SettingsSyncState, SerializablePersistentStateCompo
override val disabledSubcategories: Map<SettingsCategory, List<String>>
get() = state.disabledSubcategories
fun updateCategories(disabledCategories: List<SettingsCategory>, disabledSubcategories: Map<SettingsCategory, List<String>>) {
updateState {
it.withDisabledCategories(disabledCategories).withDisabledSubcategories(disabledSubcategories)
}
}
fun applyFromState(state: SettingsSyncState) {
updateState {
State(state.disabledCategories, state.disabledSubcategories, state.migrationFromOldStorageChecked, state.syncEnabled)
@@ -78,7 +84,7 @@ class SettingsSyncSettings : SettingsSyncState, SerializablePersistentStateCompo
return State(disabledCategories, disabledSubcategories, checked, syncEnabled)
}
private fun withDisabledCategories(newCategories: List<SettingsCategory>): State {
internal fun withDisabledCategories(newCategories: List<SettingsCategory>): State {
return State(newCategories, disabledSubcategories, migrationFromOldStorageChecked, syncEnabled)
}
@@ -95,7 +101,7 @@ class SettingsSyncSettings : SettingsSyncState, SerializablePersistentStateCompo
}
private fun withDisabledSubcategories(newSubcategoriesMap: Map<SettingsCategory, List<String>>): State {
internal fun withDisabledSubcategories(newSubcategoriesMap: Map<SettingsCategory, List<String>>): State {
return State(disabledCategories, newSubcategoriesMap, migrationFromOldStorageChecked, syncEnabled)
}
@@ -15,7 +15,11 @@ class SettingsSyncUpdateChecker() {
@RequiresBackgroundThread
fun scheduleUpdateFromServer() : UpdateResult {
val updateResult = RemoteCommunicatorHolder.getRemoteCommunicator().receiveUpdates()
val updateResult = RemoteCommunicatorHolder.getRemoteCommunicator()?.receiveUpdates() ?: run {
val errorMsg = "Cannot get update from server - no communicator provider"
LOG.info(errorMsg)
return UpdateResult.Error(errorMsg)
}
when(updateResult) {
is UpdateResult.Success -> {
val snapshot = updateResult.settingsSnapshot
@@ -13,6 +13,7 @@ import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.progress.blockingContext
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.wm.IdeFrame
import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder
import com.intellij.settingsSync.migration.migrateIfNeeded
import com.intellij.settingsSync.statistics.SettingsSyncEventsStatistics
import com.intellij.util.concurrency.AppExecutorUtil
@@ -50,6 +51,9 @@ private class SettingsSynchronizerApplicationInitializedListener : ApplicationAc
}
}
}
setProviderCodeAndUserId()
serviceAsync<SettingsSyncEvents>().addListener(settingsSyncEventListener)
if (isSettingsSyncEnabledInSettings()) {
@@ -75,17 +79,28 @@ private class SettingsSynchronizerApplicationInitializedListener : ApplicationAc
}
}
}
}
private suspend fun initializeSyncing(initMode: SettingsSyncBridge.InitMode, settingsSyncEventListener: SettingsSyncEventListener) {
LOG.info("Initializing settings sync. Mode: $initMode")
val settingsSyncMain = serviceAsync<SettingsSyncMain>()
blockingContext {
settingsSyncMain.controls.bridge.initialize(initMode)
val settingsSyncEvents = SettingsSyncEvents.getInstance()
settingsSyncEvents.addListener(settingsSyncEventListener)
settingsSyncEvents.fireSettingsChanged(SyncSettingsEvent.SyncRequest)
LocalHostNameProvider.initialize()
private suspend fun initializeSyncing(initMode: SettingsSyncBridge.InitMode, settingsSyncEventListener: SettingsSyncEventListener) {
LOG.info("Initializing settings sync. Mode: $initMode")
val settingsSyncMain = serviceAsync<SettingsSyncMain>()
blockingContext {
settingsSyncMain.controls.bridge.initialize(initMode)
val settingsSyncEvents = SettingsSyncEvents.getInstance()
settingsSyncEvents.addListener(settingsSyncEventListener)
settingsSyncEvents.fireSettingsChanged(SyncSettingsEvent.SyncRequest)
LocalHostNameProvider.initialize()
}
}
private fun setProviderCodeAndUserId() {
if (!SettingsSyncSettings.getInstance().syncEnabled)
return
if (SettingsSyncLocalSettings.getInstance().providerCode.isNullOrBlank()) {
SettingsSyncLocalSettings.getInstance().providerCode = RemoteCommunicatorHolder.DEFAULT_PROVIDER_CODE
}
if (SettingsSyncLocalSettings.getInstance().userId.isNullOrBlank()) {
SettingsSyncLocalSettings.getInstance().userId = RemoteCommunicatorHolder.DEFAULT_USER_ID
}
}
}
@@ -1,6 +1,9 @@
package com.intellij.settingsSync.auth
import com.intellij.settingsSync.communicator.SettingsSyncUserData
import kotlinx.coroutines.Deferred
import java.awt.Component
import javax.swing.Icon
interface SettingsSyncAuthService {
/**
@@ -10,22 +13,25 @@ interface SettingsSyncAuthService {
val providerCode: String
/**
* Starts the login procedure
* Free-form name of the provider
*/
fun login()
val providerName: String
/**
* Whether the user has logged in
* provider icon
*/
fun isLoggedIn(): Boolean
val icon: Icon?
/**
* Starts the login procedure (if necessary) and returns the Deferred of the logged-in user
*/
suspend fun login(parentComponent: Component?) : SettingsSyncUserData?
/**
* Data of the current user. If there's no user, return null
* This data is used for in the local git repo as well as UI (if necessary)
*/
fun getUserData(): SettingsSyncUserData
fun getUserData(userId: String): SettingsSyncUserData?
/**
* Indicates if it's currently possible to log in (i.e. all necessary plugins are present and enabled, etc), given the current state of IDE.
*/
fun isLoginAvailable(): Boolean
fun getAvailableUserAccounts(): List<SettingsSyncUserData>
}
@@ -1,95 +1,90 @@
package com.intellij.settingsSync.communicator
import com.intellij.openapi.diagnostic.logger
import com.intellij.settingsSync.ServerState
import com.intellij.settingsSync.SettingsSnapshot
import com.intellij.settingsSync.SettingsSyncPushResult
import com.intellij.settingsSync.SettingsSyncRemoteCommunicator
import com.intellij.settingsSync.UpdateResult
import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.util.concurrency.SynchronizedClearableLazy
import com.intellij.settingsSync.*
import com.intellij.util.resettableLazy
import org.jetbrains.annotations.ApiStatus
object RemoteCommunicatorHolder {
@ApiStatus.Internal
object RemoteCommunicatorHolder : SettingsSyncEventListener {
private val logger = logger<RemoteCommunicatorHolder>()
const val DEFAULT_PROVIDER_CODE = "jba"
const val DEFAULT_USER_ID = "jba"
private val communicatorInternalLazy = SynchronizedClearableLazy<SettingsSyncRemoteCommunicator> {
getCurrentCommunicator().also {
logger<RemoteCommunicatorHolder>().warn("Initializing remote communicator: $it")
// pair userId:remoteCommunicator
private val communicatorLazy = resettableLazy {
createRemoteCommunicator()
}
fun getRemoteCommunicator(): SettingsSyncRemoteCommunicator? = communicatorLazy.value ?: createRemoteCommunicator()
fun getAuthService() = getCurrentProvider()?.authService
fun isAvailable() = communicatorLazy.value != null
fun invalidateCommunicator() = communicatorLazy.reset().also {
logger.warn("Invalidating remote communicator")
}
fun getCurrentUserData() : SettingsSyncUserData? {
val userId = SettingsSyncLocalSettings.getInstance().userId ?: run {
logger.warn("No current userId. Returning null user data")
return null
}
val provider: SettingsSyncCommunicatorProvider = getCurrentProvider() ?: run {
logger.warn("No active provider. Returning null user data")
return null
}
return provider.authService.getUserData(userId)
}
fun getRemoteCommunicator(): SettingsSyncRemoteCommunicator = communicatorInternalLazy.value
fun getAuthService() = getProvider().authService
fun isAvailable() = communicatorInternalLazy.isInitialized()
fun invalidateCommunicator() = communicatorInternalLazy.drop().also {
logger<RemoteCommunicatorHolder>().warn("Invalidating remote communicator: $it")
override fun loginStateChanged() {
}
fun createRemoteCommunicator(provider: SettingsSyncCommunicatorProvider, userId: String): SettingsSyncRemoteCommunicator? {
return provider.createCommunicator(userId)
}
private fun getProvider() : SettingsSyncCommunicatorProvider {
private fun createRemoteCommunicator(): SettingsSyncRemoteCommunicator? {
val provider: SettingsSyncCommunicatorProvider = getCurrentProvider() ?: run {
logger.warn("Attempting to create remote communicator without active provider")
return null
}
val userId = SettingsSyncLocalSettings.getInstance().userId ?: run {
logger.warn("Empty current userId. Communicator will not be created.")
return null
}
val currentUserData = provider.authService.getUserData(userId) ?: run {
logger.warn("Empty current user data. Communicator will not be created.")
return null
}
val communicator: SettingsSyncRemoteCommunicator = provider.createCommunicator(currentUserData.id) ?: run {
logger.warn("Provider '${provider.providerCode}' returned empty communicator")
return null
}
return communicator
}
fun getAvailableProviders(): List<SettingsSyncCommunicatorProvider> {
val extensionList = SettingsSyncCommunicatorProvider.PROVIDER_EP.extensionList
return extensionList.firstOrNull() ?: DummyProvider
return extensionList
}
private fun getCurrentCommunicator() : SettingsSyncRemoteCommunicator {
return getProvider().createCommunicator() ?: DummyCommunicator
fun getDefaultProvider(): SettingsSyncCommunicatorProvider {
return getProvider(DEFAULT_PROVIDER_CODE)!!
}
internal object DummyProvider: SettingsSyncCommunicatorProvider {
override val providerCode: String
get() = "dummy"
override val authService: SettingsSyncAuthService
get() = DummyAuthProvider
override fun createCommunicator(): SettingsSyncRemoteCommunicator? {
return DummyCommunicator
}
fun getProvider(providerCode: String): SettingsSyncCommunicatorProvider? {
return getAvailableProviders().find { it.providerCode == providerCode }
}
internal object DummyAuthProvider: SettingsSyncAuthService {
override val providerCode: String
get() = "dummy"
override fun login() {
TODO("Not yet implemented")
}
override fun isLoggedIn(): Boolean {
return false
}
override fun getUserData(): SettingsSyncUserData {
return SettingsSyncUserData.EMPTY
}
override fun isLoginAvailable(): Boolean {
return true
}
fun getAvailableUserAccounts(): List<SettingsSyncUserData> {
return getAvailableProviders().flatMap { it.authService.getAvailableUserAccounts() }
}
internal object DummyCommunicator: SettingsSyncRemoteCommunicator {
override fun checkServerState(): ServerState {
TODO("Not yet implemented")
}
override fun receiveUpdates(): UpdateResult {
TODO("Not yet implemented")
}
override fun push(snapshot: SettingsSnapshot, force: Boolean, expectedServerVersionId: String?): SettingsSyncPushResult {
TODO("Not yet implemented")
}
override fun createFile(filePath: String, content: String) {
TODO("Not yet implemented")
}
override fun deleteFile(filePath: String) {
TODO("Not yet implemented")
}
override fun isFileExists(filePath: String): Boolean {
TODO("Not yet implemented")
}
private fun getCurrentProvider(): SettingsSyncCommunicatorProvider? {
val providerCode = SettingsSyncLocalSettings.getInstance().providerCode ?: return null
return getProvider(providerCode)
}
}
@@ -19,7 +19,7 @@ interface SettingsSyncCommunicatorProvider {
/**
* Creates a communicator (using the login data from authService)
*/
fun createCommunicator(): SettingsSyncRemoteCommunicator?
fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator?
companion object {
@JvmField
@@ -28,10 +28,9 @@ interface SettingsSyncCommunicatorProvider {
}
data class SettingsSyncUserData(
val name: String?,
val email: String?,
) {
companion object {
val EMPTY = SettingsSyncUserData(null, null)
}
}
val id: String,
val providerCode: String,
val name: String? = null,
val email: String? = null,
val printableName: String? = null
)
@@ -1,451 +1,576 @@
package com.intellij.settingsSync.config
import com.intellij.icons.AllIcons
import com.intellij.ide.DataManager
import com.intellij.ide.plugins.InstalledPluginsState
import com.intellij.ide.plugins.PluginManagerConfigurable
import com.intellij.ide.plugins.PluginStateManager
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.ex.ApplicationEx
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.impl.stores.stateStore
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.application.*
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
import com.intellij.openapi.observable.properties.AtomicProperty
import com.intellij.openapi.observable.util.and
import com.intellij.openapi.observable.util.not
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.ConfigurableProvider
import com.intellij.openapi.options.ex.Settings
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.*
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.settingsSync.*
import com.intellij.settingsSync.SettingsSyncBundle.message
import com.intellij.settingsSync.UpdateResult.*
import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder
//import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.settingsSync.communicator.SettingsSyncCommunicatorProvider
import com.intellij.settingsSync.communicator.SettingsSyncUserData
import com.intellij.settingsSync.config.SettingsSyncEnabler.State
import com.intellij.settingsSync.statistics.SettingsSyncEventsStatistics
import com.intellij.ui.components.ActionLink
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.layout.ComponentPredicate
import com.intellij.ui.layout.and
import com.intellij.ui.layout.not
import com.intellij.ui.components.DropDownLink
import com.intellij.ui.dsl.builder.*
import com.intellij.ui.dsl.listCellRenderer.groupedTextListCellRenderer
import com.intellij.util.text.DateFormatUtil
import org.jetbrains.annotations.Nls
import com.intellij.util.ui.JBUI
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.awt.event.ItemEvent
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import javax.swing.JButton
import javax.swing.JCheckBox
import javax.swing.JLabel
import javax.swing.*
internal class SettingsSyncConfigurable : BoundConfigurable(message("title.settings.sync")),
SettingsSyncEnabler.Listener,
SettingsSyncStatusTracker.Listener {
internal class SettingsSyncConfigurable(private val coroutineScope: CoroutineScope) : BoundConfigurable(message("title.settings.sync")),
SettingsSyncEnabler.Listener,
SettingsSyncStatusTracker.Listener {
companion object {
private val LOG = logger<SettingsSyncConfigurable>()
}
private lateinit var configPanel: DialogPanel
private lateinit var enableButton: Cell<JButton>
private lateinit var enableButton: JButton
private lateinit var statusLabel: JLabel
@Volatile
private var marketplacePluginInstalled = false
private lateinit var userDropDownLink: DropDownLink<UserProviderHolder?>
private lateinit var syncTypeLabel: JEditorPane
private val syncEnabler = SettingsSyncEnabler()
private val MARKETPLACE_PLUGIN_ID = PluginId.getId("com.intellij.marketplace")
private val enabledStatus = AtomicBooleanProperty(false)
private val enableSyncOption = AtomicProperty<InitSyncType>(InitSyncType.GET_FROM_SERVER)
private val disableSyncOption = AtomicProperty<Int>(DisableSyncType.DISABLE)
private val remoteSettingsExist = AtomicBooleanProperty(false)
private val wasUsedBefore = AtomicBooleanProperty(SettingsSyncLocalSettings.getInstance().userId != null)
private val userAccountsList = arrayListOf<UserProviderHolder>()
private val syncPanelHolder = SettingsSyncPanelHolder()
private val hasMultipleProviders = AtomicBooleanProperty(RemoteCommunicatorHolder.getAvailableProviders().size > 1)
init {
syncEnabler.addListener(this)
SettingsSyncStatusTracker.getInstance().addListener(this)
}
inner class LoggedInPredicate : ComponentPredicate() {
override fun addListener(listener: (Boolean) -> Unit) =
SettingsSyncEvents.getInstance().addListener(
object : SettingsSyncEventListener {
override fun loginStateChanged() {
listener(invoke())
}
},
disposable!!)
override fun invoke() = RemoteCommunicatorHolder.getAuthService().isLoggedIn()
}
inner class EnabledPredicate : ComponentPredicate() {
override fun addListener(listener: (Boolean) -> Unit) {
SettingsSyncEvents.getInstance().addListener(object : SettingsSyncEventListener {
override fun enabledStateChanged(syncEnabled: Boolean) {
listener(invoke())
configPanel.reset()
}
}, disposable!!)
}
override fun invoke() = SettingsSyncSettings.getInstance().syncEnabled
}
inner class SyncEnablerRunning : ComponentPredicate() {
private var isRunning = false
override fun addListener(listener: (Boolean) -> Unit) {
syncEnabler.addListener(object : SettingsSyncEnabler.Listener {
override fun serverRequestStarted() {
updateRunning(listener, true)
}
override fun serverRequestFinished() {
updateRunning(listener, false)
}
})
}
private fun updateRunning(listener: (Boolean) -> Unit, isRunning: Boolean) {
this.isRunning = isRunning
listener(invoke())
}
override fun invoke(): Boolean = isRunning
}
inner class AuthServiceRestartPredicate : ComponentPredicate() {
init {
marketplacePluginInstalled = InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID)
}
override fun addListener(listener: (Boolean) -> Unit) {
PluginStateManager.addStateListener { descriptor ->
if (descriptor.pluginId == MARKETPLACE_PLUGIN_ID) {
// InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID) is still false at that time,
// so we just cache the value
marketplacePluginInstalled = true
listener(marketplacePluginInstalled)
}
}
}
override fun invoke(): Boolean {
return marketplacePluginInstalled
}
}
override fun createPanel(): DialogPanel {
val syncConfigPanel = SettingsSyncPanelFactory.createCombinedSyncSettingsPanel(
message("configurable.what.to.sync.label"),
SettingsSyncSettings.getInstance(),
SettingsSyncLocalSettings.getInstance(),
)
val authService = RemoteCommunicatorHolder.getAuthService()
val authAvailable = authService.isLoginAvailable()
val syncConfigPanel = syncPanelHolder.createCombinedSyncSettingsPanel(message("configurable.what.to.sync.label"),
SettingsSyncSettings.getInstance(),
SettingsSyncLocalSettings.getInstance())
configPanel = panel {
val isSyncEnabled = LoggedInPredicate().and(EnabledPredicate())
if (settingsRepositoryIsEnabled()) {
row {
label(message("settings.warning.sync.cannot.be.enabled.label")).applyToComponent {
icon = AllIcons.General.Warning
enabledStatus.set(SettingsSyncSettings.getInstance().syncEnabled)
var userProviderHolder: UserProviderHolder? = null
if (SettingsSyncLocalSettings.getInstance().userId != null && SettingsSyncLocalSettings.getInstance().providerCode != null) {
val authService = RemoteCommunicatorHolder.getProvider(SettingsSyncLocalSettings.getInstance().providerCode!!)?.authService
if (authService != null) {
authService.getAvailableUserAccounts().find {
it.id == SettingsSyncLocalSettings.getInstance().userId
}?.apply {
userProviderHolder = toUserProviderHolder(authService.providerName)
}
bottomGap(BottomGap.MEDIUM)
}
}
// authService is not available without restart
if (authAvailable) {
row {
val statusCell = label("")
statusCell
.visibleIf(LoggedInPredicate())
.enabled(!settingsRepositoryIsEnabled())
statusLabel = statusCell.component
updateStatusInfo()
label(message("sync.status.login.message"))
.visibleIf(LoggedInPredicate().not())
.enabled(!settingsRepositoryIsEnabled())
}
row {
button(message("config.button.login")) {
authService.login()
}.visibleIf(LoggedInPredicate().not())
.enabled(!settingsRepositoryIsEnabled())
enableButton = button(message("config.button.enable")) {
syncEnabler.checkServerState()
}.visibleIf(LoggedInPredicate().and(EnabledPredicate().not()))
.enabledIf(SyncEnablerRunning().not())
.enabled(!settingsRepositoryIsEnabled())
button(message("config.button.disable")) {
LoggedInPredicate().and(EnabledPredicate())
disableSync()
}.visibleIf(isSyncEnabled)
bottomGap(BottomGap.MEDIUM)
}
updateUserAccountsList()
enabledStatus.afterChange {
syncStatusChanged()
}
else {
val authServiceRestartPredicate = AuthServiceRestartPredicate()
row {
label(message("sync.status.login.not.available")).gap(RightGap.SMALL)
@Suppress("DialogTitleCapitalization", "HardCodedStringLiteral")
link("JetBrains Marketplace Licensing Support") {
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(it.source as ActionLink))
val pluginManager = settings?.find("preferences.pluginManager")
if (pluginManager is PluginManagerConfigurable) {
settings.select(pluginManager).doWhenDone {
pluginManager.openMarketplaceTab("/organization:JetBrains Marketplace Licensing")
row {
label(message("settings.sync.info.message"))
}.visibleIf(enabledStatus.not())
row {
label(message("settings.sync.select.provider.message"))
}.visibleIf(enabledStatus.not())
row {
val availableProviders = RemoteCommunicatorHolder.getAvailableProviders()
availableProviders.forEach { provider ->
button(provider.authService.providerName) {
login(provider, syncConfigPanel)
}.applyToComponent {
icon = provider.authService.icon
}
}
}.visibleIf(wasUsedBefore.not().and(hasMultipleProviders))
row {
val defaultProvider = RemoteCommunicatorHolder.getDefaultProvider()
button(message("config.button.login")) {
login(defaultProvider, syncConfigPanel)
}
}.visibleIf(wasUsedBefore.not().and(hasMultipleProviders.not()))
row {
val label = label("").applyToComponent {
iconTextGap = 6
}.gap(RightGap.SMALL)
statusLabel = label.component
cell(object: DropDownLink<UserProviderHolder?>(userProviderHolder, userAccountsList) {
override fun createRenderer(): ListCellRenderer<in UserProviderHolder?> {
return groupedTextListCellRenderer({
if (it == UserProviderHolder.addAccount) {
message("enable.sync.add.account")
} else {
it.toString()
}
}, {
it?.separatorString
})
}
}).onChangedContext { component, context ->
val event = context.event
if (event is ItemEvent && event.item == UserProviderHolder.addAccount) {
val syncTypeDialog = AddAccountDialog(configPanel)
if (syncTypeDialog.showAndGet()) {
val providerCode = syncTypeDialog.providerCode
val provider = RemoteCommunicatorHolder.getProvider(providerCode) ?: return@onChangedContext
component.selectedItem = null
component.text = ""
login(provider, syncConfigPanel)
}
} else {
component.text = component.selectedItem.toString()
}
}.apply {
userDropDownLink = this.component
}
}.visibleIf(wasUsedBefore)
row {
val enableButtonCell = button(message("config.button.enable")) {
if (!enabledStatus.get()) {
runWithModalProgressBlocking(ModalTaskOwner.component(configPanel), message("enable.sync.check.server.data.progress")) {
val (userId, userData, providerCode, providerName) = userDropDownLink.selectedItem ?: run {
LOG.warn("No selected user")
return@runWithModalProgressBlocking
}
val provider = RemoteCommunicatorHolder.getProvider(providerCode) ?: run {
LOG.warn("Provider '$providerName' ($providerCode) is not available")
return@runWithModalProgressBlocking
}
val remoteCommunicator = RemoteCommunicatorHolder.createRemoteCommunicator(provider, userId) ?: run {
LOG.warn("Cannot create remote communicator of type '$providerName' ($providerCode)")
return@runWithModalProgressBlocking
}
if (checkServerState(syncPanelHolder, remoteCommunicator)) {
enabledStatus.set(true)
syncStatusChanged()
}
}
} else {
val syncDisableOption = showDisableSyncDialog()
if (syncDisableOption != DisableSyncType.DONT_DISABLE) {
enabledStatus.set(false)
disableSyncOption.set(syncDisableOption)
syncStatusChanged()
}
}
}.visibleIf(authServiceRestartPredicate.not())
row {
label(message("sync.status.restart.required", ApplicationNamesInfo.getInstance().fullProductName))
}.visibleIf(authServiceRestartPredicate)
row {
button(message("sync.status.restart.ide.button")) {
val app = ApplicationManager.getApplication() as ApplicationEx
app.restart(true)
}
}.visibleIf(authServiceRestartPredicate)
}
row {
comment(message("settings.sync.info.message"), 80)
.visibleIf(isSyncEnabled.not())
}
row {
cell(syncConfigPanel)
.visibleIf(LoggedInPredicate().and(EnabledPredicate()))
.onApply {
syncConfigPanel.apply()
SettingsSyncEvents.getInstance().fireCategoriesChanged()
SettingsSyncEvents.getInstance().fireSettingsChanged(
SyncSettingsEvent.CrossIdeSyncStateChanged(SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled))
}
.onReset(syncConfigPanel::reset)
.onIsModified(syncConfigPanel::isModified)
}
}
SettingsSyncEvents.getInstance().addListener(
object : SettingsSyncEventListener {
override fun loginStateChanged() {
if (RemoteCommunicatorHolder.getAuthService().isLoggedIn() && !SettingsSyncSettings.getInstance().syncEnabled) {
syncEnabler.checkServerState()
}
reset()
}
},
disposable!!
)
enableButton = enableButtonCell.component
}.visibleIf(wasUsedBefore)
// settings to sync
group(message("enable.dialog.select.what.to.sync")) {
row {
icon(AllIcons.General.BalloonWarning).applyToComponent {
isOpaque = true
background = JBUI.CurrentTheme.Banner.WARNING_BACKGROUND
border = JBUI.Borders.compound(
JBUI.Borders.customLine(JBUI.CurrentTheme.Banner.WARNING_BORDER_COLOR, 1, 1, 1, 0),
JBUI.Borders.empty(8)
)
verticalAlignment = SwingConstants.TOP
}.align(AlignY.FILL)
text("",
action = {
val syncTypeDialog = ChangeSyncTypeDialog(configPanel, enableSyncOption.get())
if (syncTypeDialog.showAndGet()) {
enableSyncOption.set(syncTypeDialog.option)
}
}).applyToComponent {
isOpaque = true
background = JBUI.CurrentTheme.Banner.WARNING_BACKGROUND
border = JBUI.Borders.compound(
JBUI.Borders.customLine(JBUI.CurrentTheme.Banner.WARNING_BORDER_COLOR, 1, 0, 1, 1),
JBUI.Borders.empty(8)
)
}.align(AlignX.FILL).resizableColumn().also {
syncTypeLabel = it.component
enableSyncOption.afterChange {
updateSyncOptionText()
}
}
cell()
}.layout(RowLayout.PARENT_GRID).topGap(TopGap.SMALL)
.visibleIf(remoteSettingsExist)
row {
cell(syncConfigPanel)
.onReset(syncConfigPanel::reset)
.onIsModified{
enabledStatus.get() != SettingsSyncSettings.getInstance().syncEnabled || syncConfigPanel.isModified()
}
.onApply {
with(SettingsSyncLocalSettings.getInstance()) {
userId = userDropDownLink.selectedItem?.userId
providerCode = userDropDownLink.selectedItem?.providerCode
}
if (enabledStatus.get()) {
syncConfigPanel.apply()
}
if (SettingsSyncSettings.getInstance().syncEnabled != enabledStatus.get()) {
if (enabledStatus.get()) {
SettingsSyncSettings.getInstance().syncEnabled = enabledStatus.get()
if (enableSyncOption.get() == InitSyncType.GET_FROM_SERVER) {
syncEnabler.getSettingsFromServer()
}
else {
syncEnabler.pushSettingsToServer()
}
} else {
when (disableSyncOption.get()) {
DisableSyncType.DISABLE_AND_REMOVE_DATA -> {
disableAndRemoveData()
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(
SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_AND_REMOVED_DATA_FROM_SERVER)
}
DisableSyncType.DISABLE -> {
SettingsSyncSettings.getInstance().syncEnabled = false
syncStatusChanged()
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY)
}
else -> {
SettingsSyncSettings.getInstance().syncEnabled = false
syncStatusChanged()
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY)
}
}
SettingsSyncSettings.getInstance().syncEnabled = enabledStatus.get()
}
}
}
}.topGap(TopGap.SMALL)
}.visibleIf(enabledStatus)
// apply necessary changes
}
syncStatusChanged()
return configPanel
}
private fun settingsRepositoryIsEnabled(): Boolean {
return !SettingsSyncSettings.getInstance().syncEnabled &&
(ApplicationManager.getApplication().stateStore.storageManager).streamProvider.let { it.enabled && it.isExclusive }
}
override fun serverStateCheckFinished(state: UpdateResult) {
when (state) {
NoFileOnServer, FileDeletedFromServer -> showEnableSyncDialog(null, null)
is Success -> showEnableSyncDialog(
state.settingsSnapshot.getState(),
SettingsSyncLocalStateHolder(state.isCrossIdeSyncEnabled),
)
is Error -> {
if (state != SettingsSyncEnabler.State.CANCELLED) {
showError(message("notification.title.update.error"), state.message)
}
}
}
}
override fun updateFromServerFinished(result: UpdateResult) {
when (result) {
is Success -> {
reset()
SettingsSyncSettings.getInstance().syncEnabled = true
}
NoFileOnServer, FileDeletedFromServer -> {
showError(message("notification.title.update.error"), message("notification.title.update.no.such.file"))
}
is Error -> {
showError(message("notification.title.update.error"), result.message)
}
}
updateStatusInfo()
}
private fun showEnableSyncDialog(remoteSettings: SettingsSyncState?, remoteSyncScopeSettings: SettingsSyncLocalStateHolder?) {
val dialog = EnableSettingsSyncDialog(configPanel, remoteSettings, remoteSyncScopeSettings)
dialog.show()
val dialogResult = dialog.getResult()
if (dialogResult != null) {
when (dialogResult) {
EnableSettingsSyncDialog.Result.GET_FROM_SERVER -> {
syncEnabler.getSettingsFromServer(dialog.syncSettings)
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.GET_FROM_SERVER)
}
EnableSettingsSyncDialog.Result.PUSH_LOCAL -> {
SettingsSyncSettings.getInstance().syncEnabled = true
syncEnabler.pushSettingsToServer()
if (remoteSettings != null) {
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL)
}
else {
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL_WAS_ONLY_WAY)
}
}
}
}
else {
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.CANCELED)
}
reset()
configPanel.reset()
}
companion object DisableResult {
const val RESULT_CANCEL = 0
const val RESULT_REMOVE_DATA_AND_DISABLE = 1
const val RESULT_DISABLE = 2
}
private fun disableSync() {
private fun showDisableSyncDialog(): Int {
@Suppress("DialogTitleCapitalization")
val result = Messages.showCheckboxMessageDialog( // TODO<rv>: Use AlertMessage instead
message("disable.dialog.text"),
val providerName = userDropDownLink.selectedItem?.providerName ?: ""
return Messages.showCheckboxMessageDialog( // TODO<rv>: Use AlertMessage instead
message("disable.dialog.text", providerName),
message("disable.dialog.title"),
arrayOf(Messages.getCancelButton(), message("disable.dialog.disable.button")),
message("disable.dialog.remove.data.box"),
message("disable.dialog.remove.data.box", providerName),
false,
1,
1,
Messages.getInformationIcon()
) { index: Int, checkbox: JCheckBox ->
if (index == 1) {
if (checkbox.isSelected) RESULT_REMOVE_DATA_AND_DISABLE else RESULT_DISABLE
if (checkbox.isSelected) DisableSyncType.DISABLE_AND_REMOVE_DATA else DisableSyncType.DISABLE
}
else {
RESULT_CANCEL
}
}
when (result) {
RESULT_DISABLE -> {
SettingsSyncSettings.getInstance().syncEnabled = false
updateStatusInfo()
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY)
}
RESULT_REMOVE_DATA_AND_DISABLE -> {
disableAndRemoveData()
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(
SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_AND_REMOVED_DATA_FROM_SERVER)
}
RESULT_CANCEL -> {
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.CANCEL)
0
}
}
}
private fun disableAndRemoveData() {
val modality = ModalityState.current();
runWithModalProgressBlocking(ModalTaskOwner.component(configPanel), message("disable.remove.data.title"), TaskCancellation.cancellable()) {
val cdl = CountDownLatch(1)
SettingsSyncEvents.getInstance().fireSettingsChanged(SyncSettingsEvent.DeleteServerData { result ->
cdl.countDown()
object : Task.Modal(null, message("disable.remove.data.title"), false) {
override fun run(indicator: ProgressIndicator) {
val cdl = CountDownLatch(1)
SettingsSyncEvents.getInstance().fireSettingsChanged(SyncSettingsEvent.DeleteServerData { result ->
cdl.countDown()
when (result) {
is DeleteServerDataResult.Error -> {
runInEdt {
showError(message("disable.remove.data.failure"), result.error)
}
}
DeleteServerDataResult.Success -> {
runInEdt(modality) {
updateStatusInfo()
}
}
when (result) {
is DeleteServerDataResult.Error -> {
val messageBuilder = StringBuilder()
messageBuilder.append(message("sync.status.failed"))
statusLabel.icon = AllIcons.General.Error
messageBuilder.append(' ').append("${message("disable.remove.data.failure")}: ${result.error}")
@Suppress("HardCodedStringLiteral")
statusLabel.text = messageBuilder.toString()
}
})
cdl.await(1, TimeUnit.MINUTES)
}
}.queue()
}
private fun showError(message: @Nls String, details: @Nls String) {
val messageBuilder = StringBuilder()
messageBuilder.append(message("sync.status.failed"))
statusLabel.icon = AllIcons.General.Error
messageBuilder.append(' ').append("$message: $details")
@Suppress("HardCodedStringLiteral")
statusLabel.text = messageBuilder.toString()
}
private fun updateStatusInfo() {
if (::statusLabel.isInitialized) {
val messageBuilder = StringBuilder()
statusLabel.icon = null
if (SettingsSyncSettings.getInstance().syncEnabled) {
val statusTracker = SettingsSyncStatusTracker.getInstance()
if (statusTracker.isSyncSuccessful()) {
messageBuilder
.append(message("sync.status.enabled"))
if (statusTracker.isSynced()) {
messageBuilder
.append(". ")
.append(message("sync.status.last.sync.message", getReadableSyncTime(), getUserName()))
DeleteServerDataResult.Success -> {
syncStatusChanged()
}
}
else {
messageBuilder.append(message("sync.status.failed"))
statusLabel.icon = AllIcons.General.Error
messageBuilder.append(' ').append(statusTracker.getErrorMessage())
}
}
else {
messageBuilder.append(message("sync.status.disabled"))
}
@Suppress("HardCodedStringLiteral") // The above strings are localized
statusLabel.text = messageBuilder.toString()
})
cdl.await(1, TimeUnit.MINUTES)
}
}
private fun getReadableSyncTime(): String {
return DateFormatUtil.formatPrettyDateTime(SettingsSyncStatusTracker.getInstance().getLastSyncTime()).lowercase()
private fun updateSyncOptionText() {
val message = if (enableSyncOption.get() == InitSyncType.GET_FROM_SERVER) {
message("enable.dialog.get.settings.from.account.text")
} else if (enableSyncOption.get() == InitSyncType.PUSH_LOCAL) {
message("enable.dialog.sync.local.settings.text")
} else {
""
}
syncTypeLabel.text ="<div>$message</div> <div style='margin-top: 5px'><a>${message("enable.dialog.change")}</a></div>"
}
private fun getUserName(): String {
return RemoteCommunicatorHolder.getAuthService().getUserData().name ?: "?"
private fun updateUserAccountsList() {
userAccountsList.clear()
val providersMap = RemoteCommunicatorHolder.getAvailableProviders().map { it.providerCode to it }.toMap()
providersMap.forEach { providerId, communicator ->
val authService = communicator.authService
val providerName = authService.providerName
authService.getAvailableUserAccounts().forEachIndexed { idx, account ->
val separatorString = if (idx == 0)
providerName
else
null
userAccountsList.add(account.toUserProviderHolder(providerName, separatorString))
}
}
if (hasMultipleProviders.get()) {
userAccountsList.add(UserProviderHolder.addAccount)
}
}
private fun login(
provider: SettingsSyncCommunicatorProvider,
syncConfigPanel: DialogPanel,
) {
coroutineScope.launch(ModalityState.current().asContextElement()) {
try {
val userData = provider.authService.login(syncConfigPanel)
if (userData != null) {
withContext(Dispatchers.EDT) {
updateUserAccountsList()
val remoteCommunicator = RemoteCommunicatorHolder.createRemoteCommunicator(provider, userData.id) ?: return@withContext
if (checkServerState(syncPanelHolder, remoteCommunicator)) {
SettingsSyncEvents.getInstance().fireLoginStateChanged()
userDropDownLink.selectedItem = UserProviderHolder(userData.id, userData, provider.authService.providerCode,
provider.authService.providerName, null)
userDropDownLink.text
enabledStatus.set(true)
wasUsedBefore.set(true)
syncStatusChanged()
syncConfigPanel.reset()
}
}
}
else {
LOG.info("Received empty user data from login")
}
}
catch (ex: CancellationException) {
LOG.info("Login procedure was cancelled")
if (LOG.isDebugEnabled) {
LOG.info("Login procedure was cancelled", ex)
}
}
catch (ex: Throwable) {
LOG.warn("Error during login", ex)
}
syncConfigPanel.requestFocusInWindow()
}
}
private fun SettingsSyncUserData.toUserProviderHolder(providerName: String, separatorString: String? = null) =
UserProviderHolder(id, this, providerCode, providerName, separatorString)
override fun syncStatusChanged() {
updateStatusInfo()
if (::statusLabel.isInitialized) {
if (enabledStatus.get()) {
val messageBuilder = StringBuilder()
if (SettingsSyncSettings.getInstance().syncEnabled) {
val statusTracker = SettingsSyncStatusTracker.getInstance()
if (statusTracker.isSyncSuccessful()) {
statusLabel.icon = icons.SettingsSyncIcons.StatusEnabled
if (statusTracker.isSynced()) {
messageBuilder.append(message("sync.status.last.sync.message", getReadableSyncTime()))
} else {
messageBuilder.append(message("sync.status.enabled"))
}
}
else {
messageBuilder.append(message("sync.status.failed"))
statusLabel.icon = AllIcons.General.Error
messageBuilder.append(' ').append(statusTracker.getErrorMessage())
}
}
else {
statusLabel.icon = icons.SettingsSyncIcons.StatusNotRun
messageBuilder.append(message("sync.status.enabled"))
}
@Suppress("HardCodedStringLiteral") // The above strings are localized
statusLabel.text = messageBuilder.toString()
enableButton.text = message("config.button.disable")
}
else {
statusLabel.icon = icons.SettingsSyncIcons.StatusDisabled
statusLabel.text = message("sync.status.disabled.message")
enableButton.text = message("config.button.enable")
}
}
}
override fun disposeUIResources() {
super.disposeUIResources()
SettingsSyncStatusTracker.getInstance().removeListener(this)
private fun getReadableSyncTime(): String =
DateFormatUtil.formatPrettyDateTime(SettingsSyncStatusTracker.getInstance().getLastSyncTime())
private fun checkServerState(syncPanelHolder: SettingsSyncPanelHolder, communicator: SettingsSyncRemoteCommunicator) : Boolean {
communicator.setTemporary(true)
val updateResult = try {
communicator.receiveUpdates()
}
catch (ex: Exception) {
LOG.warn(ex.message)
State.CANCELLED
}
when (updateResult) {
NoFileOnServer, FileDeletedFromServer -> {
syncPanelHolder.setSyncSettings(null)
syncPanelHolder.setSyncScopeSettings(null)
enableSyncOption.set(InitSyncType.PUSH_LOCAL)
remoteSettingsExist.set(false)
return true
}
is Success -> {
syncPanelHolder.setSyncSettings(updateResult.settingsSnapshot.getState())
syncPanelHolder.setSyncScopeSettings(SettingsSyncLocalStateHolder(updateResult.isCrossIdeSyncEnabled))
enableSyncOption.set(InitSyncType.GET_FROM_SERVER)
remoteSettingsExist.set(true)
return true
}
is Error -> {
if (updateResult != SettingsSyncEnabler.State.CANCELLED) {
//showError(message("notification.title.update.error"), state.message)
return false
}
}
}
return false
}
override fun getHelpTopic(): String = "cloud-config.plugin-dialog"
private data class UserProviderHolder(
val userId: String,
val userData: SettingsSyncUserData,
val providerCode: String,
val providerName: String,
val separatorString: String?, // separator value, set only for the first account in the list
) {
companion object{
val addAccount = UserProviderHolder(
"<ADDACCOUNT>", SettingsSyncUserData("<ADDACCOUNT>", ""), "",
"", "")
}
override fun toString(): String {
return userData.printableName ?: userData.email ?: userData.name ?: userData.id
}
}
private enum class InitSyncType {
PUSH_LOCAL,
GET_FROM_SERVER
}
private sealed class DisableSyncType{
companion object{
const val DISABLE = 1
const val DISABLE_AND_REMOVE_DATA = 2
const val DONT_DISABLE = 0
}
}
private class ChangeSyncTypeDialog(parent: JComponent, var option: InitSyncType) : DialogWrapper(parent, false) {
init {
title = message("title.settings.sync")
init()
}
override fun createCenterPanel(): JComponent {
return panel {
row {
icon(AllIcons.General.QuestionDialog).align(AlignY.TOP)
panel {
row {
text(message("enable.dialog.source.option.title")).bold()
}
row {
text(message("enable.dialog.source.option.text"), 50)
}
buttonsGroup ("", false) {
row {
radioButton(message("enable.dialog.get.settings.from.account.option"), InitSyncType.GET_FROM_SERVER)
}
row {
radioButton(message("enable.dialog.sync.local.settings.option"), InitSyncType.PUSH_LOCAL)
}
}.bind(::option)
}
}
}
}
override fun createActions(): Array<Action> =
arrayOf(cancelAction, okAction)
}
private class AddAccountDialog(parent: JComponent) : DialogWrapper(parent, false) {
var providerCode: String = ""
init {
title = message("title.settings.sync")
init()
}
override fun createCenterPanel(): JComponent {
return panel {
row {
icon(AllIcons.General.QuestionDialog).align(AlignY.TOP)
panel {
row {
text(message("enable.sync.choose.data.provider.title")).bold()
}
buttonsGroup (message("enable.sync.choose.data.provider.text"), false) {
val availableProviders = RemoteCommunicatorHolder.getAvailableProviders()
row {
for (provider in availableProviders) {
radioButton(provider.authService.providerName, provider.providerCode)
}
}
}.bind(::providerCode)
}
}
}
}
}
}
class SettingsSyncConfigurableProvider : ConfigurableProvider() {
override fun createConfigurable(): Configurable = SettingsSyncConfigurable()
override fun canCreateConfigurable() = isSettingsSyncEnabledByKey()
class SettingsSyncConfigurableProvider(private val coroutineScope: CoroutineScope) : ConfigurableProvider() {
override fun createConfigurable(): Configurable = SettingsSyncConfigurable(coroutineScope)
}
@@ -1,30 +1,30 @@
package com.intellij.settingsSync.config
import com.intellij.configurationStore.saveSettings
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.util.NlsContexts
import com.intellij.platform.util.progress.withProgressText
import com.intellij.settingsSync.*
import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder
import com.intellij.util.EventDispatcher
import kotlinx.coroutines.*
import java.util.*
internal class SettingsSyncEnabler {
class SettingsSyncEnabler {
companion object {
private val logger = logger<SettingsSyncEnabler>()
}
private val eventDispatcher = EventDispatcher.create(Listener::class.java)
object State {
val CANCELLED = UpdateResult.Error("Cancelled")
}
fun checkServerState() {
fun checkServerStateAsync() {
eventDispatcher.multicaster.serverStateCheckStarted()
val communicator = RemoteCommunicatorHolder.getRemoteCommunicator()
val communicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run {
logger.info("communicator doesn't exist, skipping check")
return
}
object : Task.Modal(null, SettingsSyncBundle.message("enable.sync.check.server.data.progress"), true) {
private lateinit var updateResult: UpdateResult
@@ -42,6 +42,14 @@ internal class SettingsSyncEnabler {
}.queue()
}
fun getServerState() : UpdateResult {
val communicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run {
logger.info("communicator doesn't exist, skipping check")
return State.CANCELLED
}
return communicator.receiveUpdates()
}
fun getSettingsFromServer(syncSettings: SettingsSyncState? = null) {
eventDispatcher.multicaster.updateFromServerStarted()
@@ -50,7 +58,12 @@ internal class SettingsSyncEnabler {
private lateinit var updateResult: UpdateResult
override fun run(indicator: ProgressIndicator) {
val result = RemoteCommunicatorHolder.getRemoteCommunicator().receiveUpdates()
val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator() ?: run {
logger.info("communicator doesn't exist, cannot get settings from server")
updateResult = UpdateResult.Error("No remote communicator")
return
}
val result = remoteCommunicator.receiveUpdates()
updateResult = result
if (result is UpdateResult.Success) {
val cloudEvent = SyncSettingsEvent.CloudChange(result.settingsSnapshot, result.serverVersionId, syncSettings)
@@ -0,0 +1,448 @@
package com.intellij.settingsSync.config
import com.intellij.icons.AllIcons
import com.intellij.ide.DataManager
import com.intellij.ide.plugins.InstalledPluginsState
import com.intellij.ide.plugins.PluginManagerConfigurable
import com.intellij.ide.plugins.PluginStateManager
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.ex.ApplicationEx
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.impl.stores.stateStore
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.ConfigurableProvider
import com.intellij.openapi.options.ex.Settings
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.Task
import com.intellij.openapi.progress.runBlockingCancellable
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.Messages
import com.intellij.settingsSync.*
import com.intellij.settingsSync.SettingsSyncBundle.message
import com.intellij.settingsSync.UpdateResult.*
import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder
//import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.settingsSync.statistics.SettingsSyncEventsStatistics
import com.intellij.ui.components.ActionLink
import com.intellij.ui.dsl.builder.BottomGap
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.layout.ComponentPredicate
import com.intellij.ui.layout.and
import com.intellij.ui.layout.not
import com.intellij.util.text.DateFormatUtil
import org.jetbrains.annotations.Nls
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import javax.swing.JButton
import javax.swing.JCheckBox
import javax.swing.JLabel
internal class SettingsSyncOldConfigurable : BoundConfigurable(message("title.settings.sync")),
SettingsSyncEnabler.Listener,
SettingsSyncStatusTracker.Listener {
private lateinit var configPanel: DialogPanel
private lateinit var enableButton: Cell<JButton>
private lateinit var statusLabel: JLabel
@Volatile
private var marketplacePluginInstalled = false
private val syncEnabler = SettingsSyncEnabler()
private val MARKETPLACE_PLUGIN_ID = PluginId.getId("com.intellij.marketplace")
init {
syncEnabler.addListener(this)
SettingsSyncStatusTracker.getInstance().addListener(this)
}
inner class LoggedInPredicate : ComponentPredicate() {
override fun addListener(listener: (Boolean) -> Unit) =
SettingsSyncEvents.getInstance().addListener(
object : SettingsSyncEventListener {
override fun loginStateChanged() {
listener(invoke())
}
},
disposable!!)
override fun invoke() = RemoteCommunicatorHolder.getCurrentUserData() != null
}
inner class EnabledPredicate : ComponentPredicate() {
override fun addListener(listener: (Boolean) -> Unit) {
SettingsSyncEvents.getInstance().addListener(object : SettingsSyncEventListener {
override fun enabledStateChanged(syncEnabled: Boolean) {
listener(invoke())
configPanel.reset()
}
}, disposable!!)
}
override fun invoke() = SettingsSyncSettings.getInstance().syncEnabled
}
inner class SyncEnablerRunning : ComponentPredicate() {
private var isRunning = false
override fun addListener(listener: (Boolean) -> Unit) {
syncEnabler.addListener(object : SettingsSyncEnabler.Listener {
override fun serverRequestStarted() {
updateRunning(listener, true)
}
override fun serverRequestFinished() {
updateRunning(listener, false)
}
})
}
private fun updateRunning(listener: (Boolean) -> Unit, isRunning: Boolean) {
this.isRunning = isRunning
listener(invoke())
}
override fun invoke(): Boolean = isRunning
}
inner class AuthServiceRestartPredicate : ComponentPredicate() {
init {
marketplacePluginInstalled = InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID)
}
override fun addListener(listener: (Boolean) -> Unit) {
PluginStateManager.addStateListener { descriptor ->
if (descriptor.pluginId == MARKETPLACE_PLUGIN_ID) {
// InstalledPluginsState.getInstance().wasInstalled(MARKETPLACE_PLUGIN_ID) is still false at that time,
// so we just cache the value
marketplacePluginInstalled = true
listener(marketplacePluginInstalled)
}
}
}
override fun invoke(): Boolean {
return marketplacePluginInstalled
}
}
override fun createPanel(): DialogPanel {
val syncConfigPanel = SettingsSyncPanelFactory.createCombinedSyncSettingsPanel(
message("configurable.what.to.sync.label"),
SettingsSyncSettings.getInstance(),
SettingsSyncLocalSettings.getInstance(),
)
val authService = RemoteCommunicatorHolder.getAuthService()
val authAvailable = true
configPanel = panel {
val isSyncEnabled = LoggedInPredicate().and(EnabledPredicate())
if (settingsRepositoryIsEnabled()) {
row {
label(message("settings.warning.sync.cannot.be.enabled.label")).applyToComponent {
icon = AllIcons.General.Warning
}
bottomGap(BottomGap.MEDIUM)
}
}
// authService is not available without restart
if (authAvailable) {
row {
val statusCell = label("")
statusCell
.visibleIf(LoggedInPredicate())
.enabled(!settingsRepositoryIsEnabled())
statusLabel = statusCell.component
updateStatusInfo()
label(message("sync.status.login.message"))
.visibleIf(LoggedInPredicate().not())
.enabled(!settingsRepositoryIsEnabled())
}
row {
button(message("config.button.login")) {
runBlockingCancellable {
authService?.login(configPanel)
}
}.visibleIf(LoggedInPredicate().not())
.enabled(!settingsRepositoryIsEnabled())
enableButton = button(message("config.button.enable")) {
syncEnabler.checkServerStateAsync()
}.visibleIf(LoggedInPredicate().and(EnabledPredicate().not()))
.enabledIf(SyncEnablerRunning().not())
.enabled(!settingsRepositoryIsEnabled())
button(message("config.button.disable")) {
LoggedInPredicate().and(EnabledPredicate())
disableSync()
}.visibleIf(isSyncEnabled)
bottomGap(BottomGap.MEDIUM)
}
}
else {
val authServiceRestartPredicate = AuthServiceRestartPredicate()
row {
label(message("sync.status.login.not.available")).gap(RightGap.SMALL)
@Suppress("DialogTitleCapitalization", "HardCodedStringLiteral")
link("JetBrains Marketplace Licensing Support") {
val settings = Settings.KEY.getData(DataManager.getInstance().getDataContext(it.source as ActionLink))
val pluginManager = settings?.find("preferences.pluginManager")
if (pluginManager is PluginManagerConfigurable) {
settings.select(pluginManager).doWhenDone {
pluginManager.openMarketplaceTab("/organization:JetBrains Marketplace Licensing")
}
}
}
}.visibleIf(authServiceRestartPredicate.not())
row {
label(message("sync.status.restart.required", ApplicationNamesInfo.getInstance().fullProductName))
}.visibleIf(authServiceRestartPredicate)
row {
button(message("sync.status.restart.ide.button")) {
val app = ApplicationManager.getApplication() as ApplicationEx
app.restart(true)
}
}.visibleIf(authServiceRestartPredicate)
}
row {
comment(message("settings.sync.info.message"), 80)
.visibleIf(isSyncEnabled.not())
}
row {
cell(syncConfigPanel)
.visibleIf(LoggedInPredicate().and(EnabledPredicate()))
.onApply {
syncConfigPanel.apply()
SettingsSyncEvents.getInstance().fireCategoriesChanged()
SettingsSyncEvents.getInstance().fireSettingsChanged(
SyncSettingsEvent.CrossIdeSyncStateChanged(SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled))
}
.onReset(syncConfigPanel::reset)
.onIsModified(syncConfigPanel::isModified)
}
}
SettingsSyncEvents.getInstance().addListener(
object : SettingsSyncEventListener {
override fun loginStateChanged() {
if (RemoteCommunicatorHolder.getCurrentUserData() != null
&& !SettingsSyncSettings.getInstance().syncEnabled) {
syncEnabler.checkServerStateAsync()
}
reset()
}
},
disposable!!
)
return configPanel
}
private fun settingsRepositoryIsEnabled(): Boolean {
return !SettingsSyncSettings.getInstance().syncEnabled &&
(ApplicationManager.getApplication().stateStore.storageManager).streamProvider.let { it.enabled && it.isExclusive }
}
override fun serverStateCheckFinished(state: UpdateResult) {
when (state) {
NoFileOnServer, FileDeletedFromServer -> showEnableSyncDialog(null, null)
is Success -> showEnableSyncDialog(
state.settingsSnapshot.getState(),
SettingsSyncLocalStateHolder(state.isCrossIdeSyncEnabled),
)
is Error -> {
if (state != SettingsSyncEnabler.State.CANCELLED) {
showError(message("notification.title.update.error"), state.message)
}
}
}
}
override fun updateFromServerFinished(result: UpdateResult) {
when (result) {
is Success -> {
reset()
SettingsSyncSettings.getInstance().syncEnabled = true
}
NoFileOnServer, FileDeletedFromServer -> {
showError(message("notification.title.update.error"), message("notification.title.update.no.such.file"))
}
is Error -> {
showError(message("notification.title.update.error"), result.message)
}
}
updateStatusInfo()
}
private fun showEnableSyncDialog(remoteSettings: SettingsSyncState?, remoteSyncScopeSettings: SettingsSyncLocalStateHolder?) {
val dialog = EnableSettingsSyncDialog(configPanel, remoteSettings, remoteSyncScopeSettings)
dialog.show()
val dialogResult = dialog.getResult()
if (dialogResult != null) {
when (dialogResult) {
EnableSettingsSyncDialog.Result.GET_FROM_SERVER -> {
syncEnabler.getSettingsFromServer(dialog.syncSettings)
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.GET_FROM_SERVER)
}
EnableSettingsSyncDialog.Result.PUSH_LOCAL -> {
SettingsSyncSettings.getInstance().syncEnabled = true
syncEnabler.pushSettingsToServer()
if (remoteSettings != null) {
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL)
}
else {
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.PUSH_LOCAL_WAS_ONLY_WAY)
}
}
}
}
else {
SettingsSyncEventsStatistics.ENABLED_MANUALLY.log(SettingsSyncEventsStatistics.EnabledMethod.CANCELED)
}
reset()
configPanel.reset()
}
companion object DisableResult {
const val RESULT_CANCEL = 0
const val RESULT_REMOVE_DATA_AND_DISABLE = 1
const val RESULT_DISABLE = 2
}
private fun disableSync() {
@Suppress("DialogTitleCapitalization")
val result = Messages.showCheckboxMessageDialog( // TODO<rv>: Use AlertMessage instead
message("disable.dialog.text"),
message("disable.dialog.title"),
arrayOf(Messages.getCancelButton(), message("disable.dialog.disable.button")),
message("disable.dialog.remove.data.box"),
false,
1,
1,
Messages.getInformationIcon()
) { index: Int, checkbox: JCheckBox ->
if (index == 1) {
if (checkbox.isSelected) RESULT_REMOVE_DATA_AND_DISABLE else RESULT_DISABLE
}
else {
RESULT_CANCEL
}
}
when (result) {
RESULT_DISABLE -> {
SettingsSyncSettings.getInstance().syncEnabled = false
updateStatusInfo()
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_ONLY)
}
RESULT_REMOVE_DATA_AND_DISABLE -> {
disableAndRemoveData()
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(
SettingsSyncEventsStatistics.ManualDisableMethod.DISABLED_AND_REMOVED_DATA_FROM_SERVER)
}
RESULT_CANCEL -> {
SettingsSyncEventsStatistics.DISABLED_MANUALLY.log(SettingsSyncEventsStatistics.ManualDisableMethod.CANCEL)
}
}
}
private fun disableAndRemoveData() {
val modality = ModalityState.current();
object : Task.Modal(null, message("disable.remove.data.title"), false) {
override fun run(indicator: ProgressIndicator) {
val cdl = CountDownLatch(1)
SettingsSyncEvents.getInstance().fireSettingsChanged(SyncSettingsEvent.DeleteServerData { result ->
cdl.countDown()
when (result) {
is DeleteServerDataResult.Error -> {
runInEdt {
showError(message("disable.remove.data.failure"), result.error)
}
}
DeleteServerDataResult.Success -> {
runInEdt(modality) {
updateStatusInfo()
}
}
}
})
cdl.await(1, TimeUnit.MINUTES)
}
}.queue()
}
private fun showError(message: @Nls String, details: @Nls String) {
val messageBuilder = StringBuilder()
messageBuilder.append(message("sync.status.failed"))
statusLabel.icon = AllIcons.General.Error
messageBuilder.append(' ').append("$message: $details")
@Suppress("HardCodedStringLiteral")
statusLabel.text = messageBuilder.toString()
}
private fun updateStatusInfo() {
if (::statusLabel.isInitialized) {
val messageBuilder = StringBuilder()
statusLabel.icon = null
if (SettingsSyncSettings.getInstance().syncEnabled) {
val statusTracker = SettingsSyncStatusTracker.getInstance()
if (statusTracker.isSyncSuccessful()) {
messageBuilder
.append(message("sync.status.enabled"))
if (statusTracker.isSynced()) {
messageBuilder
.append(". ")
.append(message("sync.status.last.sync.message", getReadableSyncTime()))
}
}
else {
messageBuilder.append(message("sync.status.failed"))
statusLabel.icon = AllIcons.General.Error
messageBuilder.append(' ').append(statusTracker.getErrorMessage())
}
}
else {
messageBuilder.append(message("sync.status.disabled"))
}
@Suppress("HardCodedStringLiteral") // The above strings are localized
statusLabel.text = messageBuilder.toString()
}
}
private fun getReadableSyncTime(): String {
return DateFormatUtil.formatPrettyDateTime(SettingsSyncStatusTracker.getInstance().getLastSyncTime()).lowercase()
}
private fun getUserName(): String {
return RemoteCommunicatorHolder.getCurrentUserData()?.name ?: "?"
}
override fun syncStatusChanged() {
updateStatusInfo()
}
override fun disposeUIResources() {
super.disposeUIResources()
SettingsSyncStatusTracker.getInstance().removeListener(this)
}
override fun getHelpTopic(): String = "cloud-config.plugin-dialog"
}
@@ -1,6 +1,8 @@
package com.intellij.settingsSync.config
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.components.SettingsCategory
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.settingsSync.*
@@ -30,17 +32,41 @@ internal object SettingsSyncPanelFactory {
syncSettings: SettingsSyncState,
syncScopeSettings: SettingsSyncLocalState,
): DialogPanel {
val categoriesPanel = createSyncCategoriesPanel(syncLabel, syncSettings)
val syncScopePanel = createSyncScopePanel(syncScopeSettings)
return SettingsSyncPanelHolder().createCombinedSyncSettingsPanel(syncLabel, syncSettings, syncScopeSettings)
}
}
internal class SettingsSyncPanelHolder() {
private lateinit var panel : DialogPanel
private var isCrossIdeSyncEnabled = false
return panel {
fun setSyncSettings(syncSettings: SettingsSyncState?) {
val notNullState = syncSettings ?: SettingsSyncStateHolder()
SyncCategoryHolder.updateState(notNullState)
}
fun setSyncScopeSettings(settings: SettingsSyncLocalState?) {
isCrossIdeSyncEnabled = settings?.isCrossIdeSyncEnabled ?: false
}
fun createCombinedSyncSettingsPanel(
syncLabel: @Nls String,
syncSettings: SettingsSyncState?,
syncScopeSettings: SettingsSyncLocalState?,
): DialogPanel {
setSyncSettings(syncSettings)
setSyncScopeSettings(syncScopeSettings)
val categoriesPanel = createSyncCategoriesPanel(syncLabel)
val syncScopePanel = createSyncScopePanel()
panel = panel {
row {
cell(categoriesPanel)
.onApply(categoriesPanel::apply)
.onReset(categoriesPanel::reset)
.onIsModified(categoriesPanel::isModified)
.onIsModified {
categoriesPanel.isModified()
}
}
row {
cell(syncScopePanel)
.onApply(syncScopePanel::apply)
@@ -48,14 +74,23 @@ internal object SettingsSyncPanelFactory {
.onIsModified(syncScopePanel::isModified)
}
onApply {
SettingsSyncLocalSettings.getInstance().applyFromState(syncScopeSettings)
SettingsSyncSettings.getInstance().applyFromState(syncSettings)
// do nothing, handled by descendants
}
onIsModified {
categoriesPanel.isModified() || syncScopePanel.isModified()
}
}
return panel
}
private fun createSyncScopePanel(state: SettingsSyncLocalState): DialogPanel {
private fun createSyncScopePanel(): DialogPanel {
return panel {
onApply {
SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled = isCrossIdeSyncEnabled
SettingsSyncEvents.getInstance().fireSettingsChanged(
SyncSettingsEvent.CrossIdeSyncStateChanged(SettingsSyncLocalSettings.getInstance().isCrossIdeSyncEnabled))
}
row {
topGap(TopGap.MEDIUM)
label(message("settings.cross.product.sync"))
@@ -68,27 +103,42 @@ internal object SettingsSyncPanelFactory {
row {
radioButton(message("settings.cross.product.sync.choice.all.products"), true)
}
}.bind(state::isCrossIdeSyncEnabled)
}.bind(::isCrossIdeSyncEnabled)
}
}
private fun createSyncCategoriesPanel(syncLabel: @Nls String, state: SettingsSyncState): DialogPanel {
private fun createSyncCategoriesPanel(syncLabel: @Nls String): DialogPanel {
return panel {
onApply {
SettingsSyncSettings.getInstance().updateCategories(
SyncCategoryHolder.disabledCategories,
SyncCategoryHolder.disabledSubcategories
)
SettingsSyncEvents.getInstance().fireCategoriesChanged()
}
row {
label(syncLabel)
}
val categoryHolders = SyncCategoryHolder.createAllForState(state)
for (holder in categoryHolders) {
for (holder in SyncCategoryHolder.allHolders) {
indent {
row {
if (holder.secondaryGroup == null) {
checkBox(
val checkBox = checkBox(
holder.name
)
checkBox
.bindSelected(holder::isSynchronized)
.onReset { holder.reset() }
.onApply { holder.apply() }
.onIsModified { holder.isModified() }
.onReset {
holder.reset()
checkBox.component.isSelected = holder.isSynchronized
}
.onApply {
holder.apply()
}
.onIsModified {
holder.isModified()
}
.enabled(isModifiable(holder))
comment(holder.description)
}
else {
@@ -109,8 +159,12 @@ internal object SettingsSyncPanelFactory {
topCheckBox.state = getGroupState(holder)
holder.isSynchronized = topCheckBox.state != State.NOT_SELECTED
}
cell(subcategoryLink)
val subcategoryLinkCell = cell(subcategoryLink)
subcategoryLinkCell
.visible(holder.secondaryGroup!!.getDescriptors().size > 1 || !holder.secondaryGroup!!.isComplete())
.onReset {
subcategoryLinkCell.visible(holder.secondaryGroup!!.getDescriptors().size > 1 || !holder.secondaryGroup!!.isComplete())
}
topCheckBox.addActionListener {
holder.isSynchronized = topCheckBox.state != State.NOT_SELECTED
holder.secondaryGroup!!.getDescriptors().forEach {
@@ -119,7 +173,6 @@ internal object SettingsSyncPanelFactory {
subcategoryLink.isEnabled = holder.secondaryGroup!!.isComplete() || holder.isSynchronized
}
}
}
}
}
@@ -21,7 +21,7 @@ private enum class SyncStatus {ON, OFF, FAILED}
private fun getStatus() : SyncStatus {
if (SettingsSyncSettings.getInstance().syncEnabled &&
RemoteCommunicatorHolder.getAuthService().isLoggedIn()) {
RemoteCommunicatorHolder.getCurrentUserData() != null) {
return if (SettingsSyncStatusTracker.getInstance().isSyncSuccessful()) SyncStatus.ON
else SyncStatus.FAILED
}
@@ -7,11 +7,11 @@ import com.intellij.settingsSync.SettingsSyncBundle.message
import org.jetbrains.annotations.Nls
import java.util.*
internal class SyncCategoryHolder(
val descriptor: Category,
private val state: SettingsSyncState
) {
var isSynchronized: Boolean = state.isCategoryEnabled(descriptor.category)
internal class SyncCategoryHolder(val descriptor: Category) {
private var state: SettingsSyncState? = null
var isSynchronized: Boolean = state?.isCategoryEnabled(descriptor.category) ?: false
val name: @Nls String
get() = descriptor.name
@@ -24,10 +24,10 @@ internal class SyncCategoryHolder(
fun reset() {
with(descriptor) {
isSynchronized = state.isCategoryEnabled(category)
isSynchronized = state?.isCategoryEnabled(category) ?: false
if (secondaryGroup != null) {
secondaryGroup.getDescriptors().forEach {
it.isSelected = isSynchronized && state.isSubcategoryEnabled(category, it.id)
it.isSelected = isSynchronized && state?.isSubcategoryEnabled(category, it.id) ?: false
}
}
}
@@ -38,38 +38,68 @@ internal class SyncCategoryHolder(
if (secondaryGroup != null) {
secondaryGroup.getDescriptors().forEach {
// !isSynchronized not store disabled states individually
state.setSubcategoryEnabled(category, it.id, !isSynchronized || it.isSelected)
state?.setSubcategoryEnabled(category, it.id, !isSynchronized || it.isSelected)
}
}
state.setCategoryEnabled(category, isSynchronized)
state?.setCategoryEnabled(category, isSynchronized)
}
}
fun isModified(): Boolean {
with(descriptor) {
if (isSynchronized != state.isCategoryEnabled(category)) return true
if (isSynchronized != state?.isCategoryEnabled(category)) return true
if (secondaryGroup != null && isSynchronized) {
secondaryGroup.getDescriptors().forEach {
if (it.isSelected != state.isSubcategoryEnabled(category, it.id)) return true
if (it.isSelected != state?.isSubcategoryEnabled(category, it.id)) return true
}
}
return false
}
}
override fun toString(): String {
return "SyncCategoryHolder(name='$name', isSynchronized=$isSynchronized, isModified=${isModified()})"
}
companion object {
fun createAllForState(state: SettingsSyncState): List<SyncCategoryHolder> {
val retval = arrayListOf<SyncCategoryHolder>()
val allHolders: List<SyncCategoryHolder> = arrayListOf<SyncCategoryHolder>().apply {
for (descriptor in Category.DESCRIPTORS) {
retval.add(SyncCategoryHolder(descriptor, state))
add(SyncCategoryHolder(descriptor))
}
return retval
}
fun updateState(state: SettingsSyncState) {
allHolders.forEach {
it.state = state
}
}
val disabledCategories: List<SettingsCategory>
get() = arrayListOf<SettingsCategory>().apply {
for (holder in allHolders) {
if (!holder.isSynchronized) {
add(holder.descriptor.category)
}
}
}
val disabledSubcategories: Map<SettingsCategory, List<String>>
get() = hashMapOf<SettingsCategory, MutableList<String>>().apply {
for (holder in allHolders) {
val descriptors = holder.secondaryGroup?.getDescriptors() ?: continue
for (descriptor in descriptors) {
if (!descriptor.isSelected) {
computeIfAbsent(holder.descriptor.category) { arrayListOf() }.add(descriptor.id)
}
}
}
}
}
internal class Category(
val category: SettingsCategory,
val secondaryGroup: SyncSubcategoryGroup? = null
val secondaryGroup: SyncSubcategoryGroup? = null,
) {
val name: @Nls String
@@ -6,7 +6,6 @@ import com.intellij.openapi.ui.playback.commands.PlaybackCommandCoroutineAdapter
import com.intellij.settingsSync.*
import com.intellij.settingsSync.communicator.RemoteCommunicatorHolder
import com.intellij.settingsSync.config.SettingsSyncEnabler
import com.jetbrains.performancePlugin.commands.Waiter
import kotlinx.coroutines.*
import org.jetbrains.annotations.NonNls
import java.util.concurrent.TimeUnit
@@ -64,7 +63,7 @@ class EnableSettingsSyncCommand(text: @NonNls String, line: Int) : PlaybackComma
serverRespondedOnCheck.complete(true)
}
})
settingsSyncEnabler.checkServerState()
settingsSyncEnabler.checkServerStateAsync()
serverRespondedOnCheck.await()
var startTime = System.currentTimeMillis()
@@ -81,11 +80,11 @@ class EnableSettingsSyncCommand(text: @NonNls String, line: Int) : PlaybackComma
}
//there is no event that cross-ide sync was enabled, so we need to check that file appears and wait a bit :(
startTime = System.currentTimeMillis()
val remoteCommunicator = RemoteCommunicatorHolder.getRemoteCommunicator()
while (remoteCommunicator.isFileExists(CROSS_IDE_SYNC_MARKER_FILE) != isCrossIdeSync) {
while (RemoteCommunicatorHolder.getRemoteCommunicator() != null &&
RemoteCommunicatorHolder.getRemoteCommunicator()?.isFileExists(CROSS_IDE_SYNC_MARKER_FILE) != isCrossIdeSync) {
delay(500)
if (System.currentTimeMillis() - startTime > TimeUnit.SECONDS.toMillis(5)) {
val fileExists = remoteCommunicator.isFileExists(CROSS_IDE_SYNC_MARKER_FILE)
val fileExists = RemoteCommunicatorHolder.getRemoteCommunicator()?.isFileExists(CROSS_IDE_SYNC_MARKER_FILE)
throw Exception("Cross-IDE sync marker file was not updated in 5 seconds. File exists=$fileExists, expected=$isCrossIdeSync")
}
}
@@ -10,6 +10,9 @@ import javax.swing.*;
* DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead
*/
public final class SettingsSyncIcons {
private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) {
return IconManager.getInstance().loadRasterizedIcon(path, SettingsSyncIcons.class.getClassLoader(), cacheKey, flags);
}
private static @NotNull Icon load(@NotNull String expUIPath, @NotNull String path, int cacheKey, int flags) {
return IconManager.getInstance().loadRasterizedIcon(path, expUIPath, SettingsSyncIcons.class.getClassLoader(), cacheKey, flags);
}
@@ -18,4 +21,5 @@ public final class SettingsSyncIcons {
/** 16x16 */ public static final @NotNull Icon RemoteChanges = load("icons/expui/remoteChanges.svg", "icons/remoteChanges.svg", 287889654, 0);
/** 16x16 */ public static final @NotNull Icon StatusDisabled = load("icons/expui/statusDisabled.svg", "icons/statusDisabled.svg", -1854758088, 2);
/** 16x16 */ public static final @NotNull Icon StatusEnabled = load("icons/expui/statusEnabled.svg", "icons/statusEnabled.svg", 1900690067, 0);
/** 16x16 */ public static final @NotNull Icon StatusNotRun = load("icons/statusNotRun.svg", 1430629267, 2);
}
@@ -471,14 +471,14 @@ internal class GitSettingsLogTest {
fun `use empty email if JBA doesn't provide one`() {
val jbaName = "JBA Name 2"
userData = SettingsSyncUserData(jbaName, null)
userData = SettingsSyncUserData(jbaName, "dummy", jbaName, null)
checkUsernameEmail(jbaName, "")
}
@Test
@TestFor(issues = ["EA-844607"])
fun `use empty name if JBA doesn't provide one`() {
userData = SettingsSyncUserData(null, null)
userData = SettingsSyncUserData("empty", "dummy", null, null)
checkUsernameEmail("", "")
}
@@ -5,7 +5,10 @@ import com.intellij.openapi.util.io.FileUtil
import com.intellij.settingsSync.auth.SettingsSyncAuthService
import com.intellij.settingsSync.communicator.SettingsSyncCommunicatorProvider
import com.intellij.settingsSync.communicator.SettingsSyncUserData
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import org.junit.Assert
import java.awt.Component
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
@@ -14,9 +17,10 @@ import java.time.Instant
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicInteger
import javax.swing.Icon
import kotlin.isInitialized
internal class MockRemoteCommunicator : AbstractServerCommunicator() {
internal class MockRemoteCommunicator(override val userId: String) : AbstractServerCommunicator() {
private val filesAndVersions = mutableMapOf<String, Version>()
private val versionIdStorage = mutableMapOf<String, String>()
private val LOG = logger<MockRemoteCommunicator>()
@@ -43,11 +47,11 @@ internal class MockRemoteCommunicator : AbstractServerCommunicator() {
return e.message ?: "unknown error"
}
override fun readFileInternal(snapshotFilePath: String): Pair<InputStream?, String?> {
override fun readFileInternal(filePath: String): Pair<InputStream?, String?> {
checkConnected()
val version = filesAndVersions[snapshotFilePath] ?: throw IOException("file $snapshotFilePath is not found")
versionIdStorage.put(snapshotFilePath, version.versionId)
LOG.warn("Put version '${version.versionId}' for file $snapshotFilePath (after read)")
val version = filesAndVersions[filePath] ?: throw IOException("file $filePath is not found")
versionIdStorage.put(filePath, version.versionId)
LOG.warn("Put version '${version.versionId}' for file $filePath (after read)")
return Pair(ByteArrayInputStream(version.content), version.versionId)
}
@@ -150,7 +154,7 @@ internal class MockCommunicatorProvider (
override val providerCode: String
get() = "MOCK"
override fun createCommunicator(): SettingsSyncRemoteCommunicator? = remoteCommunicator
override fun createCommunicator(userId: String): SettingsSyncRemoteCommunicator? = remoteCommunicator
}
internal class MockAuthService (
@@ -158,21 +162,21 @@ internal class MockAuthService (
): SettingsSyncAuthService {
override val providerCode: String
get() = "MOCK"
override val providerName: String
get() = TODO("Not yet implemented")
override val icon: Icon?
get() = TODO("Not yet implemented")
override fun login() {
// do nothing
override suspend fun login(parentComponent: Component?) : SettingsSyncUserData? {
return null
}
override fun isLoggedIn(): Boolean {
return true
}
override fun getUserData(): SettingsSyncUserData {
override fun getUserData(userId: String): SettingsSyncUserData {
return userData
}
override fun isLoginAvailable(): Boolean {
return true
override fun getAvailableUserAccounts(): List<SettingsSyncUserData> {
TODO("Not yet implemented")
}
}
@@ -73,7 +73,7 @@ internal class SettingsSyncFlowTest : SettingsSyncTestBase() {
// emulate first session with initialization
val fileName = "options/laf.xml"
val file = configDir.resolve(fileName).write("LaF Initial")
val log = GitSettingsLog(settingsSyncStorage, configDir, disposable, { SettingsSyncUserData.EMPTY },
val log = GitSettingsLog(settingsSyncStorage, configDir, disposable, { SettingsSyncUserData("empty", "dummy") },
initialSnapshotProvider = { MockSettingsSyncIdeMediator.getAllFilesFromSettingsAsSnapshot(configDir) })
log.initialize()
log.logExistingSettings()
@@ -167,7 +167,7 @@ internal abstract class SettingsSyncRealIdeTestBase : SettingsSyncTestBase() {
tempDir.resolve("storage").toPath(),
tempDir.resolve("config").toPath(),
parentDisposable,
{ SettingsSyncUserData.EMPTY },
{ SettingsSyncUserData("empty", "dummy") },
initialSnapshotProvider = {
SettingsSnapshot(
SettingsSnapshot.MetaInfo(Instant.now(), null, true),
@@ -55,7 +55,7 @@ internal abstract class SettingsSyncTestBase {
TODO("Implement with real server via TestRemoteCommunicator()")
}
else {
MockRemoteCommunicator().apply {this.isConnected = true }
MockRemoteCommunicator("mockUser").apply {this.isConnected = true }
}
val providerEP = SettingsSyncCommunicatorProvider.PROVIDER_EP.point
if (providerEP.extensions.size > 0) {