From c83f86faf7d9decff607f9ebbeb9b5886a2c32e6 Mon Sep 17 00:00:00 2001 From: Eugene Petrenko Date: Wed, 24 Mar 2021 14:53:59 +0100 Subject: [PATCH] jdk-updates: IDEA-261052 GitOrigin-RevId: 0e61362a53a83fbcc877b4de251511347f9484f5 --- .../impl/jdkDownloader/JdkUpdateStoreTest.kt | 72 -------- .../impl/jdkDownloader/JdkUpdateTest.kt | 163 +++++++++--------- .../jdkDownloader/JdkUpdateNotification.kt | 87 +++++----- .../impl/jdkDownloader/JdkUpdateState.kt | 34 ---- .../impl/jdkDownloader/JdkUpdater.kt | 36 ---- .../impl/jdkDownloader/JdkUpdaterActions.kt | 96 +++++++++++ .../messages/ProjectBundle.properties | 6 +- .../src/META-INF/PlatformExtensions.xml | 2 +- 8 files changed, 216 insertions(+), 280 deletions(-) delete mode 100644 java/idea-ui/testSrc/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateStoreTest.kt delete mode 100644 platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateState.kt create mode 100644 platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdaterActions.kt diff --git a/java/idea-ui/testSrc/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateStoreTest.kt b/java/idea-ui/testSrc/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateStoreTest.kt deleted file mode 100644 index 1be7a92b1beb..000000000000 --- a/java/idea-ui/testSrc/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateStoreTest.kt +++ /dev/null @@ -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() - 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) - } -} diff --git a/java/idea-ui/testSrc/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateTest.kt b/java/idea-ui/testSrc/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateTest.kt index 15584cfc2144..978832754c36 100644 --- a/java/idea-ui/testSrc/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateTest.kt +++ b/java/idea-ui/testSrc/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateTest.kt @@ -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()) @@ -45,24 +48,22 @@ class JdkUpdateTest : LightPlatformTestCase() { } }) + doEventsWhile(1) listOurNotifications().forEach { it.expire() } - UIUtil.dispatchAllInvocationEvents() + listOurActions().forEach { it.reachTerminalState() } + } + + private fun listOurActions(): List { + doEventsWhile(1) + return service().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().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() - 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() - 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() - 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().isAllowed(update.jdk, update.newItem)) - } - - private inline fun Notification.fireAction() { - val action = actions.filterIsInstance().single() - Notification.fire(this, action) - } - - fun `test merge notifications correctly`() { - val old1 = newNotification("old-1") + val old1 = newNotification("old-1")!! service().showNotification(old1.jdk, old1.oldItem, old1.newItem) service().showNotification(old1.jdk, old1.oldItem, old1.newItem) service().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().showNotification(old1.jdk, old1.oldItem, old1.newItem) service().showNotification(old2.jdk, old2.oldItem, old2.newItem) service().showNotification(old2.jdk, old2.oldItem, old2.newItem) service().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().showNotification(old1.jdk, old1.oldItem, old1.newItem) service().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().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().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().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().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) diff --git a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateNotification.kt b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateNotification.kt index 008cfaaf8d87..9b9cfb109195 100644 --- a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateNotification.kt +++ b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateNotification.kt @@ -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().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") diff --git a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateState.kt b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateState.kt deleted file mode 100644 index 04870c4da715..000000000000 --- a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdateState.kt +++ /dev/null @@ -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()) { - 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() - } -} diff --git a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdater.kt b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdater.kt index c6726e662dca..58946110fc4f 100644 --- a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdater.kt +++ b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdater.kt @@ -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().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() - - 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() - } -} diff --git a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdaterActions.kt b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdaterActions.kt new file mode 100644 index 000000000000..6697aa443b5f --- /dev/null +++ b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkUpdaterActions.kt @@ -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() +} + +@Service(Service.Level.APP) +@State(name = "jdk-update-state", storages = [Storage(StoragePathMacros.CACHE_FILE)], allowLoadInTests = true) +class JdkUpdaterNotifications : SimplePersistentStateComponent(JdkSettingsActionRegistryState()), Disposable { + private val lock = ReentrantLock() + private val pendingNotifications = HashMap() + private var pendingActionsCopy = listOf() + + 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 = pendingActionsCopy +} + +class JdkSettingsActionRegistryActionProvider : SettingsEntryPointAction.ActionProvider { + override fun getUpdateActions(context: DataContext) = service().getActions() +} diff --git a/platform/platform-api/resources/messages/ProjectBundle.properties b/platform/platform-api/resources/messages/ProjectBundle.properties index 0595b5021682..b46dc8ac15a8 100644 --- a/platform/platform-api/resources/messages/ProjectBundle.properties +++ b/platform/platform-api/resources/messages/ProjectBundle.properties @@ -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}
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= diff --git a/platform/platform-resources/src/META-INF/PlatformExtensions.xml b/platform/platform-resources/src/META-INF/PlatformExtensions.xml index d844eeec7445..e5ffbd54c790 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensions.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensions.xml @@ -886,7 +886,6 @@ configurationSchemaKey="java" /> - @@ -894,6 +893,7 @@ +