jdk-updates: IDEA-261052

GitOrigin-RevId: 0e61362a53a83fbcc877b4de251511347f9484f5
This commit is contained in:
Eugene Petrenko
2021-03-24 18:01:21 +00:00
committed by intellij-monorepo-bot
parent 11fa6f32ef
commit c83f86faf7
8 changed files with 216 additions and 280 deletions
@@ -1,72 +0,0 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.projectRoots.impl.jdkDownloader
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.components.stateStore
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.LightPlatformTestCase
import org.junit.Assert
class JdkUpdateStoreTest : LightPlatformTestCase() {
private fun newSdk(sdkName: String, version: String): Sdk {
val oldSdk = ProjectJdkTable.getInstance().createSdk(sdkName, JavaSdk.getInstance())
oldSdk.sdkModificator.apply {
homePath = createTempDir("mock-old-home").toString()
versionString = version
}.commitChanges()
if (oldSdk is Disposable) {
Disposer.register(testRootDisposable, oldSdk)
}
return oldSdk
}
fun testState() {
val sdkA = newSdk("a", "1.2.3")
val sdkB = newSdk("b", "3.2.1")
Assert.assertTrue(service.isAllowed(sdkA, mockZipNew))
Assert.assertTrue(service.isAllowed(sdkB, mockZipNew))
service.blockVersion(sdkA, mockZipNew)
Assert.assertFalse(service.isAllowed(sdkA, mockZipNew))
Assert.assertTrue(service.isAllowed(sdkA, mockZipNew.copy(jdkVersion = "1234.53.5")))
Assert.assertTrue(service.isAllowed(sdkB, mockZipNew))
reloadState()
Assert.assertFalse(service.isAllowed(sdkA, mockZipNew))
Assert.assertTrue(service.isAllowed(sdkA, mockZipNew.copy(jdkVersion = "1234.53.5")))
Assert.assertTrue(service.isAllowed(sdkB, mockZipNew))
}
private val service get() = service<JdkUpdaterState>()
private val newState get() = JdkUpdaterStateData()
private fun resetState() {
service.loadState(newState)
}
override fun setUp() {
super.setUp()
resetState()
}
private fun reloadState() {
//just to check if the state can be saved
val store = ApplicationManager.getApplication().stateStore
val p = service
store.saveComponent(p)
//wipe the state
p.loadState(newState)
//load it (hopefully)
store.reloadState(service::class.java)
}
}
@@ -3,23 +3,26 @@ package com.intellij.openapi.projectRoots.impl.jdkDownloader
import com.intellij.application.subscribe
import com.intellij.notification.Notification
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.impl.NonBlockingReadActionImpl
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.components.service
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.impl.jdkDownloader.JdkUpdateNotification.InstallUpdateNotification
import com.intellij.openapi.projectRoots.impl.jdkDownloader.JdkUpdateNotification.RejectUpdateNotification
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.LightPlatformTestCase
import com.intellij.util.ui.UIUtil
import org.junit.Assert
import java.awt.event.KeyEvent
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import javax.swing.JPanel
class JdkUpdateTest : LightPlatformTestCase() {
private val myNotifications = Collections.synchronizedList(ArrayList<Notification>())
@@ -45,24 +48,22 @@ class JdkUpdateTest : LightPlatformTestCase() {
}
})
doEventsWhile(1)
listOurNotifications().forEach { it.expire() }
UIUtil.dispatchAllInvocationEvents()
listOurActions().forEach { it.reachTerminalState() }
}
private fun listOurActions(): List<JdkUpdateNotification> {
doEventsWhile(1)
return service<JdkUpdaterNotifications>().getActions().map { it.jdkUpdateNotification }.filter { it.isUpdateActionVisible }
}
private fun listOurNotifications() = myNotifications
.filter { !it.isExpired }
.filter { it.groupId == "JDK Update" || it.groupId == "JDK Update Error" }
private fun listOurExpiredNotifications() = myNotifications
.filter { it.isExpired }
.filter { it.groupId == "JDK Update" || it.groupId == "JDK Update Error" }
private fun expectSingleSuggestUpdateNotification() : Notification {
return listOurNotifications().single { it.groupId == "JDK Update" }
}
private fun newNotification(sdkName: String, oldVersion: JdkItem = mockZipOld, newVersion: JdkItem = mockZipNew): JdkUpdateNotification {
val oldSdk = ProjectJdkTable.getInstance().createSdk(sdkName, JavaSdk.getInstance())
private fun newNotification(sdkName: String, oldVersion: JdkItem = mockZipOld, newVersion: JdkItem = mockZipNew): JdkUpdateNotification? {
val oldSdk = ProjectJdkTable.getInstance().findJdk(sdkName) ?: ProjectJdkTable.getInstance().createSdk(sdkName, JavaSdk.getInstance())
oldSdk.sdkModificator.apply {
homePath = createTempDir("mock-old-home").toString()
versionString = oldVersion.versionString
@@ -72,31 +73,30 @@ class JdkUpdateTest : LightPlatformTestCase() {
Disposer.register(testRootDisposable, oldSdk)
}
return JdkUpdateNotification(oldSdk, oldVersion, newVersion) {}
val notification = service<JdkUpdaterNotifications>().showNotification(oldSdk, oldVersion, newVersion)
doEventsWhile(5)
return notification
}
fun `test the same popup is not shown twice`() {
val notification = newNotification("old-sdk")
notification.showNotificationIfAbsent()
notification.showNotificationIfAbsent()
notification.showNotificationIfAbsent()
newNotification("old-sdk")
val actions = listOurActions()
val notifications = listOurNotifications()
val expired = listOurExpiredNotifications()
Assert.assertEquals("$notifications", 1, notifications.size)
Assert.assertEquals("$expired", 0, expired.size)
Assert.assertEquals("$notifications", 0, notifications.size)
Assert.assertEquals("$actions", 1, actions.size)
}
fun `test jdk update`() {
val update = newNotification("old-sdk2")
update.showNotificationIfAbsent()
val update = newNotification("old-sdk2")!!
Assert.assertEquals(setOf(update), listOurActions().toSet())
val n = expectSingleSuggestUpdateNotification()
n.fireAction<InstallUpdateNotification>()
UIUtil.dispatchAllInvocationEvents()
update.fireAction()
doEventsWhile(5)
val ourActions = listOurActions()
val ourNotifications = listOurNotifications()
Assert.assertTrue("$ourActions", ourActions.isEmpty())
Assert.assertTrue("$ourNotifications", ourNotifications.isEmpty())
Assert.assertEquals(update.newItem.versionString, update.jdk.versionString)
Assert.assertTrue(update.isTerminated())
@@ -108,89 +108,59 @@ class JdkUpdateTest : LightPlatformTestCase() {
}
fun `test jdk update failed`() {
val update = newNotification("old-sdk2", newVersion = mockZipNewBroken)
update.showNotificationIfAbsent()
val update = newNotification("old-sdk2", newVersion = mockZipNewBroken)!!
Assert.assertEquals(setOf(update), listOurActions().toSet())
val n = expectSingleSuggestUpdateNotification()
n.fireAction<InstallUpdateNotification>()
UIUtil.dispatchAllInvocationEvents()
update.fireAction()
doEventsWhile(2)
val ourActions = listOurActions()
val ourNotifications = listOurNotifications()
Assert.assertEquals(setOf(update), listOurActions().toSet())
Assert.assertEquals("$ourNotifications", 1, ourNotifications.size)
Assert.assertEquals("$ourNotifications", 1, ourNotifications.filter { it.type == NotificationType.ERROR }.size)
Assert.assertEquals(update.oldItem.versionString, update.jdk.versionString)
Assert.assertTrue(!update.isTerminated())
update.showNotificationIfAbsent()
Assert.assertEquals("$ourNotifications", ourNotifications, listOurNotifications())
}
fun `test reject is not lost`() {
val update = newNotification("old-sdk2")
update.showNotificationIfAbsent()
val n = expectSingleSuggestUpdateNotification()
n.fireAction<RejectUpdateNotification>()
UIUtil.dispatchAllInvocationEvents()
val ourNotifications = listOurNotifications()
Assert.assertTrue("$ourNotifications", ourNotifications.isEmpty())
Assert.assertEquals(update.oldItem.versionString, update.jdk.versionString)
Assert.assertTrue(update.isTerminated())
Assert.assertFalse(service<JdkUpdaterState>().isAllowed(update.jdk, update.newItem))
}
private inline fun <reified T: NotificationAction> Notification.fireAction() {
val action = actions.filterIsInstance<T>().single()
Notification.fire(this, action)
}
fun `test merge notifications correctly`() {
val old1 = newNotification("old-1")
val old1 = newNotification("old-1")!!
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem)
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem)
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem)
val notifications = listOurNotifications()
val expired = listOurExpiredNotifications()
Assert.assertEquals("$notifications", 1, notifications.size)
Assert.assertEquals("$expired", 0, expired.size)
val actions = listOurActions()
Assert.assertEquals("$actions", 1, actions.size)
}
fun `test merge notifications correctly 2`() {
val old1 = newNotification("old-1")
val old2 = newNotification("old-2")
val old1 = newNotification("old-1")!!
val old2 = newNotification("old-2")!!
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem)
service<JdkUpdaterNotifications>().showNotification(old2.jdk, old2.oldItem, old2.newItem)
service<JdkUpdaterNotifications>().showNotification(old2.jdk, old2.oldItem, old2.newItem)
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem)
val notifications = listOurNotifications()
val expired = listOurExpiredNotifications()
Assert.assertEquals("$notifications", 2, notifications.size)
Assert.assertEquals("$expired", 0, expired.size)
val actions = listOurActions()
Assert.assertEquals("$actions", 2, actions.size)
}
fun `test replace notifications correctly 2`() {
val old1 = newNotification("old-1")
val old1 = newNotification("old-1")!!
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem)
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem.copy(jdkVersion = "17.0.777"))
val notifications = listOurNotifications()
val expired = listOurExpiredNotifications()
Assert.assertEquals("$notifications", 1, notifications.size)
Assert.assertTrue("$notifications", notifications.single().content.contains("17.0.777"))
Assert.assertEquals("$expired", 1, expired.size)
Assert.assertFalse("$notifications", expired.single().content.contains("17.0.777"))
val actions = listOurActions()
Assert.assertEquals("$actions", 1, actions.size)
Assert.assertTrue("$actions", actions.single().newItem.jdkVersion == "17.0.777")
}
fun `test replace notifications correctly 3`() {
val old1 = newNotification("old-1")
val old1 = newNotification("old-1")!!
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem)
Assert.assertEquals(1, listOurNotifications().size)
Assert.assertEquals(0, listOurExpiredNotifications().size)
doEventsWhile(5)
Assert.assertEquals(1, listOurActions().size)
runWriteAction {
old1.jdk.sdkModificator.also { it.versionString = "new JDK version" }.commitChanges()
@@ -198,16 +168,14 @@ class JdkUpdateTest : LightPlatformTestCase() {
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem)
//notification must expire and update
Assert.assertEquals(1, listOurNotifications().size)
Assert.assertEquals(1, listOurExpiredNotifications().size)
Assert.assertEquals(1, listOurActions().size)
}
fun `test replace notifications correctly 4`() {
val old1 = newNotification("old-1")
val old1 = newNotification("old-1")!!
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem)
Assert.assertEquals(1, listOurNotifications().size)
Assert.assertEquals(0, listOurExpiredNotifications().size)
Assert.assertEquals(1, listOurActions().size)
runWriteAction {
old1.jdk.sdkModificator.also { it.homePath = it.homePath + "-123" }.commitChanges()
@@ -215,8 +183,7 @@ class JdkUpdateTest : LightPlatformTestCase() {
service<JdkUpdaterNotifications>().showNotification(old1.jdk, old1.oldItem, old1.newItem)
//notification must expire and update
Assert.assertEquals(1, listOurNotifications().size)
Assert.assertEquals(1, listOurExpiredNotifications().size)
Assert.assertEquals(1, listOurActions().size)
}
}
@@ -270,3 +237,29 @@ private fun jdkItemForTest(url: String,
sharedIndexAliases = listOf(),
saveToFile = {}
)
private fun doEventsWhile(iterations: Int = Int.MAX_VALUE / 2,
condition: () -> Boolean = { true }) {
repeat(iterations) {
if (!condition()) return
ApplicationManager.getApplication().invokeAndWait {
NonBlockingReadActionImpl.waitForAsyncTaskCompletion()
UIUtil.dispatchAllInvocationEvents()
}
if (!condition()) return
Thread.sleep(30)
}
}
private fun runAction(theAction: AnAction) {
ApplicationManager.getApplication().invokeAndWait {
val event = KeyEvent(JPanel(), 1, 0, 0, 0, ' ')
ActionManager.getInstance().tryToExecute(theAction, event, null, null, true)
}
}
private fun JdkUpdateNotification.fireAction() = runAction(updateAction)
@@ -8,18 +8,16 @@ import com.intellij.notification.NotificationType
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.Task
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.ProjectBundle
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.SdkType
import com.intellij.openapi.util.NlsContexts.NotificationContent
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.util.io.systemIndependentPath
import java.util.concurrent.locks.ReentrantLock
@@ -50,7 +48,6 @@ class JdkUpdateNotification(val jdk: Sdk,
private val jdkVersion = jdk.versionString
private val jdkHome = jdk.homePath
private val openProjectsSnapshot = ProjectManager.getInstance().openProjects.map { it.locationHash }.toSortedSet()
private var myIsTerminated = false
private var myIsUpdateRunning = false
@@ -58,7 +55,9 @@ class JdkUpdateNotification(val jdk: Sdk,
/**
* Can be either suggestion or error notification
*/
private var myPendingNotification : Notification? = null
private var myRetryNotification : Notification? = null
val persistentId = "${jdk.name}-${oldItem.fullPresentationText}-${newItem.fullPresentationText}-${jdk.homePath}"
private fun Notification.bindNextNotificationAndShow() {
bindNextNotification(this)
@@ -67,17 +66,17 @@ class JdkUpdateNotification(val jdk: Sdk,
private fun bindNextNotification(notification: Notification) {
lock.withLock {
myPendingNotification?.expire()
myRetryNotification?.expire()
notification.whenExpired {
lock.withLock {
if (myPendingNotification === notification) {
myPendingNotification = null
if (myRetryNotification === notification) {
myRetryNotification = null
}
}
}
myPendingNotification = notification
myRetryNotification = notification
}
}
@@ -88,8 +87,8 @@ class JdkUpdateNotification(val jdk: Sdk,
//the pending notification is the same as before
if (other != null && isSameNotification(other)) return false
myPendingNotification?.expire()
myPendingNotification = null
myRetryNotification?.expire()
myRetryNotification = null
reachTerminalState()
return true
@@ -99,15 +98,13 @@ class JdkUpdateNotification(val jdk: Sdk,
if (this.jdkVersion != other.jdkVersion) return false
if (this.jdkHome != other.jdkHome) return false
if (this.newItem != other.newItem) return false
//a new open project may also have a update notification, that is not shown there
if (!this.openProjectsSnapshot.containsAll(other.openProjectsSnapshot)) return false
return true
}
/**
* The state-machine reached it's end
*/
private fun reachTerminalState(): Unit = lock.withLock {
fun reachTerminalState(): Unit = lock.withLock {
if (myIsTerminated) return
myIsTerminated = true
whenComplete(this)
@@ -115,44 +112,32 @@ class JdkUpdateNotification(val jdk: Sdk,
fun isTerminated() = lock.withLock { myIsTerminated }
private fun updateJdkAction(@NotificationContent message: String) = InstallUpdateNotification(message)
inner class InstallUpdateNotification(@NotificationContent message: String) : NotificationAction(message) {
inner class InstallUpdateNotification : NotificationAction(ProjectBundle.message("notification.link.jdk.update.retry")) {
override fun actionPerformed(e: AnActionEvent, notification: Notification) {
lock.withLock {
if (myIsUpdateRunning) return
myIsUpdateRunning = true
}
updateJdk(e.project)
performUpdateAction(e)
notification.expire()
}
}
private fun rejectJdkAction() = RejectUpdateNotification()
val updateAction = JdkUpdateSuggestionAction()
inner class RejectUpdateNotification : NotificationAction(ProjectBundle.message("notification.link.jdk.update.skip")) {
override fun actionPerformed(e: AnActionEvent, notification: Notification) {
service<JdkUpdaterState>().blockVersion(jdk, newItem)
notification.expire()
reachTerminalState()
val isUpdateActionVisible get() = !myIsUpdateRunning && !myIsTerminated
inner class JdkUpdateSuggestionAction : DumbAwareAction() {
val jdkUpdateNotification = this@JdkUpdateNotification
init {
templatePresentation.text = ProjectBundle.message("action.title.jdk.update.found", jdk.name, newItem.fullPresentationText, oldItem.versionPresentationText)
templatePresentation.description = ProjectBundle.message("action.description.jdk.update.found", jdk.name, newItem.fullPresentationText, oldItem.versionPresentationText)
}
}
fun showNotificationIfAbsent() : Unit = lock.withLock {
if (myPendingNotification != null || myIsUpdateRunning || myIsTerminated) return
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = isUpdateActionVisible
}
val title = ProjectBundle.message("notification.title.jdk.update.found")
val message = ProjectBundle.message("notification.text.jdk.update.found",
jdk.name,
newItem.fullPresentationText,
oldItem.versionPresentationText)
NotificationGroupManager.getInstance().getNotificationGroup("JDK Update")
.createNotification(title, message, NotificationType.INFORMATION)
.setImportant(true)
.addAction(updateJdkAction(ProjectBundle.message("notification.link.jdk.update.apply")))
.addAction(rejectJdkAction())
.bindNextNotificationAndShow()
override fun actionPerformed(e: AnActionEvent) {
performUpdateAction(e)
}
}
private fun showUpdateErrorNotification(feedItem: JdkItem) : Unit = lock.withLock {
@@ -160,12 +145,18 @@ class JdkUpdateNotification(val jdk: Sdk,
.createNotification(type = NotificationType.ERROR)
.setTitle(ProjectBundle.message("progress.title.updating.jdk.0.to.1", jdk.name, feedItem.fullPresentationText))
.setContent(ProjectBundle.message("progress.title.updating.jdk.failed", feedItem.fullPresentationText))
.addAction(updateJdkAction(ProjectBundle.message("notification.link.jdk.update.retry")))
.addAction(rejectJdkAction())
.addAction(InstallUpdateNotification())
.bindNextNotificationAndShow()
}
private fun updateJdk(project: Project?) {
private fun performUpdateAction(e: AnActionEvent) {
myRetryNotification?.expire()
lock.withLock {
if (myIsUpdateRunning) return
myIsUpdateRunning = true
}
val title = ProjectBundle.message("progress.title.updating.jdk.0.to.1", jdk.name, newItem.fullPresentationText)
ProgressManager.getInstance().run(
object : Task.Backgroundable(null /*progress should be global*/, title, true, ALWAYS_BACKGROUND) {
@@ -174,7 +165,7 @@ class JdkUpdateNotification(val jdk: Sdk,
val installer = JdkInstaller.getInstance()
val request = installer.prepareJdkInstallation(newItem, installer.defaultInstallDir(newItem))
installer.installJdk(request, indicator, project)
installer.installJdk(request, indicator, e.project)
//make sure VFS sees the files and sets up the JDK correctly
indicator.text = ProjectBundle.message("progress.text.updating.jdk.setting.up")
@@ -1,34 +0,0 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.projectRoots.impl.jdkDownloader
import com.intellij.openapi.components.*
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.xmlb.annotations.OptionTag
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class JdkUpdaterStateData : BaseState() {
@get:OptionTag
val dndVersions by stringSet()
}
@State(name = "jdk-update-state", storages = [Storage(StoragePathMacros.CACHE_FILE)], allowLoadInTests = true)
@Service
class JdkUpdaterState : SimplePersistentStateComponent<JdkUpdaterStateData>(JdkUpdaterStateData()) {
private val lock = ReentrantLock()
override fun loadState(state: JdkUpdaterStateData) = lock.withLock {
super.loadState(state)
}
private fun key(forJdk: Sdk, feedItem: JdkItem) = "for(${forJdk.name})-${feedItem.fullPresentationText}"
fun isAllowed(forJdk: Sdk, feedItem: JdkItem) = lock.withLock {
key(forJdk, feedItem) !in state.dndVersions
}
fun blockVersion(forJdk: Sdk, feedItem: JdkItem) = lock.withLock {
state.dndVersions += key(forJdk, feedItem)
state.intIncrementModificationCount()
}
}
@@ -29,8 +29,6 @@ import com.intellij.util.concurrency.AppExecutorUtil
import com.intellij.util.text.VersionComparatorUtil
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
/**
* This extension point is used to collect
@@ -180,8 +178,6 @@ internal class JdkUpdatesCollector(
it.suggestedSdkName == actualItem.suggestedSdkName && it.arch == actualItem.arch && it.os == actualItem.os
} ?: continue
if (!service<JdkUpdaterState>().isAllowed(jdk, feedItem)) continue
//internal versions are not considered here (JBRs?)
if (VersionComparatorUtil.compare(feedItem.jdkVersion, actualItem.jdkVersion) <= 0) continue
@@ -195,35 +191,3 @@ internal class JdkUpdatesCollector(
}
}
}
@Service //Application service
class JdkUpdaterNotifications : Disposable {
private val lock = ReentrantLock()
private val pendingNotifications = HashMap<Sdk, JdkUpdateNotification>()
override fun dispose() : Unit = lock.withLock {
pendingNotifications.clear()
}
fun showNotification(jdk: Sdk, actualItem: JdkItem, newItem: JdkItem) : Unit = lock.withLock {
val newNotification = JdkUpdateNotification(
jdk = jdk,
oldItem = actualItem,
newItem = newItem,
whenComplete = {
lock.withLock {
pendingNotifications.remove(jdk, it)
}
}
)
val currentNotification = pendingNotifications[jdk]
if (currentNotification != null && !currentNotification.tryReplaceWithNewerNotification(newNotification)) return
pendingNotifications[jdk] = newNotification
newNotification
}.showNotificationIfAbsent()
fun hideNotification(jdk: Sdk) = lock.withLock {
pendingNotifications[jdk]?.tryReplaceWithNewerNotification()
}
}
@@ -0,0 +1,96 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.projectRoots.impl.jdkDownloader
import com.intellij.ide.actions.SettingsEntryPointAction
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.components.*
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.util.Alarm
import com.intellij.util.ui.update.MergingUpdateQueue
import com.intellij.util.ui.update.Update
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
class JdkSettingsActionRegistryState: BaseState() {
val knownActions by list<String>()
}
@Service(Service.Level.APP)
@State(name = "jdk-update-state", storages = [Storage(StoragePathMacros.CACHE_FILE)], allowLoadInTests = true)
class JdkUpdaterNotifications : SimplePersistentStateComponent<JdkSettingsActionRegistryState>(JdkSettingsActionRegistryState()), Disposable {
private val lock = ReentrantLock()
private val pendingNotifications = HashMap<Sdk, JdkUpdateNotification>()
private var pendingActionsCopy = listOf<JdkUpdateNotification.JdkUpdateSuggestionAction>()
private val alarm = MergingUpdateQueue("jdk-update-actions", 500, true, null, this, null, Alarm.ThreadToUse.POOLED_THREAD).usePassThroughInUnitTestMode()
override fun dispose() = Unit
private fun scheduleUpdate() {
alarm.queue(object: Update(this) {
override fun run() = lock.withLock {
//we would not like it to overflow
if (state.knownActions.size > 300) {
val tail = state.knownActions.toList().takeLast(30).toHashSet()
state.knownActions.clear()
state.knownActions.addAll(tail)
state.intIncrementModificationCount()
}
val ids = pendingNotifications.values.map { it.persistentId }.toSortedSet()
val iconState = if (!state.knownActions.containsAll(ids)) {
state.knownActions.addAll(ids)
state.intIncrementModificationCount()
SettingsEntryPointAction.IconState.ApplicationUpdate
} else {
SettingsEntryPointAction.IconState.Current
}
invokeLater {
pendingActionsCopy = pendingNotifications.values.sortedBy { it.persistentId }.map { it.updateAction }
SettingsEntryPointAction.updateState(iconState)
}
}
})
}
fun hideNotification(jdk: Sdk) {
lock.withLock {
pendingNotifications[jdk]?.tryReplaceWithNewerNotification()
}
scheduleUpdate()
}
fun showNotification(jdk: Sdk, actualItem: JdkItem, newItem: JdkItem): JdkUpdateNotification? {
val newNotification = lock.withLock {
val newNotification = JdkUpdateNotification(
jdk = jdk,
oldItem = actualItem,
newItem = newItem,
whenComplete = {
lock.withLock {
pendingNotifications.remove(jdk, it)
}
scheduleUpdate()
}
)
val currentNotification = pendingNotifications[jdk]
if (currentNotification != null && !currentNotification.tryReplaceWithNewerNotification(newNotification)) return null
pendingNotifications[jdk] = newNotification
newNotification
}
scheduleUpdate()
return newNotification
}
fun getActions() : List<JdkUpdateNotification.JdkUpdateSuggestionAction> = pendingActionsCopy
}
class JdkSettingsActionRegistryActionProvider : SettingsEntryPointAction.ActionProvider {
override fun getUpdateActions(context: DataContext) = service<JdkUpdaterNotifications>().getActions()
}
@@ -269,10 +269,8 @@ progress.title.updating.jdk.0.to.1=Updating JDK "{0}" to {1}
progress.title.updating.jdk.failed=Failed to download and install JDK {0}
progress.text.updating.jdk.setting.up=Configuring JDK
progress.title.checking.for.jdk.updates=Checking for JDK updates
notification.title.jdk.update.found=New JDK version is available
notification.text.jdk.update.found={1}<br/>Current version is {2} (JDK "{0}")
notification.link.jdk.update.apply=Download
notification.link.jdk.update.skip=Skip this update
action.title.jdk.update.found=Update JDK "{0}" to {1}
action.description.jdk.update.found=JDK update to {1} is available for JDK \'{0}\'. Current version is {2}
notification.link.jdk.update.retry=Retry
dialog.text.resolving.sdks.item={0} "{1}"
unknown.sdk.with.no.name=<unknown>
@@ -886,7 +886,6 @@
configurationSchemaKey="java"
/>
<notificationGroup id="JDK Update" displayType="BALLOON"/>
<notificationGroup id="JDK Update Error" displayType="STICKY_BALLOON" />
<notificationGroup id="Runtime Chooser" displayType="BALLOON" bundle="messages.LangBundle" key="notification.title.choose.ide.runtime" />
@@ -894,6 +893,7 @@
<registryKey key="runtime.chooser.pretend.major" description="Makes Runtime Chooser show JBRs from previous releases" defaultValue="" />
<backgroundPostStartupActivity implementation="com.intellij.openapi.projectRoots.impl.jdkDownloader.JdkUpdaterStartup" />
<settingsEntryPointActionProvider implementation="com.intellij.openapi.projectRoots.impl.jdkDownloader.JdkSettingsActionRegistryActionProvider"/>
<registryKey key="jdk.auto.setup" defaultValue="true" description="Attempt to use local or downloadable SDK to configure project"/>
<registryKey key="jdk.updater" defaultValue="true" description="Check for JDK updates and suggest actions in notifications"/>