11package org.digma.intellij.plugin.digmathon
22
3+ import com.fasterxml.jackson.core.type.TypeReference
34import com.intellij.collaboration.async.disposingScope
45import com.intellij.openapi.Disposable
56import com.intellij.openapi.application.ApplicationManager
@@ -12,17 +13,18 @@ import kotlinx.coroutines.delay
1213import kotlinx.coroutines.isActive
1314import kotlinx.coroutines.launch
1415import org.digma.intellij.plugin.common.allowSlowOperation
16+ import org.digma.intellij.plugin.common.createObjectMapperWithJavaTimeModule
1517import org.digma.intellij.plugin.common.findActiveProject
1618import org.digma.intellij.plugin.errorreporting.ErrorReporter
1719import org.digma.intellij.plugin.log.Log
1820import org.digma.intellij.plugin.notifications.NotificationUtil
1921import org.digma.intellij.plugin.persistence.PersistenceService
2022import org.digma.intellij.plugin.posthog.ActivityMonitor
23+ import java.time.Instant
2124import java.time.LocalDate
2225import java.time.LocalDateTime
2326import java.time.ZoneId
2427import java.time.ZonedDateTime
25- import java.util.concurrent.atomic.AtomicBoolean
2628import java.util.concurrent.atomic.AtomicReference
2729import java.util.function.Supplier
2830import kotlin.time.Duration.Companion.minutes
@@ -31,25 +33,24 @@ import kotlin.time.Duration.Companion.minutes
3133@Service(Service .Level .APP )
3234class DigmathonService : Disposable {
3335
34-
3536 private val logger = Logger .getInstance(this ::class .java)
3637
3738 private val digmathonInfo = AtomicReference (
3839 DigmathonInfo (
39- LocalDate .of(2024 , 4 , 5 ).atStartOfDay().atZone(ZoneId .systemDefault()),
40- LocalDate .of(2024 , 4 , 17 ).atStartOfDay().atZone(ZoneId .systemDefault())
40+ LocalDate .of(2024 , 5 , 1 ).atStartOfDay().atZone(ZoneId .systemDefault()),
41+ LocalDate .of(2024 , 5 , 17 ).atStartOfDay().atZone(ZoneId .systemDefault())
4142 )
4243 )
4344
44- private val isDigmathonActive = AtomicBoolean (digmathonInfo.get().isActive())
45-
46- val viewedInsights = PersistenceService .getInstance().getDigmathonInsightsViewed()
47- ?.split(" ," )?.toMutableSet() ? : mutableSetOf ()
45+ val viewedInsights: MutableMap <String , Instant > = readInsightsViewedFromPersistence()
4846
4947 var isUserFinishedDigmathon = PersistenceService .getInstance().isFinishDigmathonGameForUser()
5048
5149
5250 companion object {
51+
52+ private val objectMapper = createObjectMapperWithJavaTimeModule()
53+
5354 @JvmStatic
5455 fun getInstance (): DigmathonService {
5556 return service<DigmathonService >()
@@ -64,27 +65,7 @@ class DigmathonService : Disposable {
6465
6566 init {
6667
67- // for development,simulate a 5 minutes event that starts 2 minutes after IDE start and lasts for 5 minutes
68- // val simulateStart = System.getProperty("org.digma.digmathon.simulate.startAfterMinutes")
69- // val simulatePeriod = System.getProperty("org.digma.digmathon.simulate.periodMinutes")
70- //
71- // if (simulateStart != null) {
72- // val startAfter = simulateStart.toLong()
73- // val endAfter = (simulatePeriod.toLongOrNull() ?: 10) + startAfter
74- //
75- // digmathonInfo.set(
76- // DigmathonInfo(
77- // LocalDateTime.now().plusMinutes(startAfter).atZone(ZoneId.systemDefault()),
78- // LocalDateTime.now().plusMinutes(endAfter).atZone(ZoneId.systemDefault())
79- // )
80- // )
81- // DigmathonProductKey().clear()
82- // isDigmathonActive.set(digmathonInfo.get().isActive())
83- // isUserFinishedDigmathon = false
84- // }
85-
86-
87- if (isDigmathonActive.get() && digmathonInfo.get().isEnded()) {
68+ if (isDigmathonStartedForUser() && digmathonInfo.get().isEnded()) {
8869 end()
8970 } else {
9071
@@ -97,11 +78,11 @@ class DigmathonService : Disposable {
9778 while (isActive && digmathonInfo.get().isActive()) {
9879 try {
9980
100- if (! isDigmathonActive.get () && digmathonInfo.get().isActive()) {
81+ if (! isDigmathonStartedForUser () && digmathonInfo.get().isActive()) {
10182 start()
10283 }
10384
104- if (isDigmathonActive.get () && digmathonInfo.get().isEnded()) {
85+ if (isDigmathonStartedForUser () && digmathonInfo.get().isEnded()) {
10586 end()
10687 cancel(" digmathon ended" )
10788 }
@@ -116,23 +97,46 @@ class DigmathonService : Disposable {
11697 }
11798 }
11899
119- if (digmathonInfo.get().isEnded()) {
100+ // the job may be canceled by the system, do a last check
101+ if (isDigmathonStartedForUser() && digmathonInfo.get().isEnded()) {
120102 end()
121103 }
122104 }
123105 }
124106 }
125107
126108
109+ // this is needed so that we can call start and end only once
110+ private fun isDigmathonStartedForUser (): Boolean {
111+ return PersistenceService .getInstance().isDigmathonStartedForUser()
112+ }
113+
114+ private fun markStartedDigmathonForUser () {
115+ PersistenceService .getInstance().setDigmathonStartedForUser(true )
116+ }
117+
118+ private fun markEndedDigmathonForUser () {
119+ PersistenceService .getInstance().setDigmathonStartedForUser(false )
120+ }
121+
122+
127123 private fun start () {
128- isDigmathonActive.set(true )
124+ // reset persistence properties every time a new digmathon starts
125+ PersistenceService .getInstance().setFinishDigmathonGameForUser(false )
126+ PersistenceService .getInstance().setDigmathonInsightsViewed(null )
127+ PersistenceService .getInstance().setDigmathonInsightsViewedLastUpdated(null )
128+ markStartedDigmathonForUser()
129129 fireStateChangedEvent()
130130 reportEvent(" start" )
131131 }
132132
133133
134134 private fun end () {
135- isDigmathonActive.set(false )
135+ // reset persistence properties for next time
136+ PersistenceService .getInstance().setFinishDigmathonGameForUser(false )
137+ PersistenceService .getInstance().setDigmathonInsightsViewed(null )
138+ PersistenceService .getInstance().setDigmathonInsightsViewedLastUpdated(null )
139+ markEndedDigmathonForUser()
136140 DigmathonProductKey ().clear()
137141 fireStateChangedEvent()
138142 fireProductKeyStateChanged()
@@ -201,43 +205,63 @@ class DigmathonService : Disposable {
201205 }
202206
203207
204- data class DigmathonInfo (val startTime : ZonedDateTime , val endTime : ZonedDateTime ) {
205-
206- fun isActive (): Boolean {
207- val now = LocalDateTime .now().atZone(ZoneId .systemDefault())
208-
209- return (now.equals(startTime) || now.isAfter(startTime)) &&
210- now.isBefore(endTime)
211- }
212-
213- fun isEnded (): Boolean {
214- return ! isActive()
215- }
216- }
217-
218-
219208 private fun reportEvent (eventType : String , details : Map <String , String > = mapOf()) {
220209 findActiveProject()?.let {
221210 ActivityMonitor .getInstance(it).reportDigmathonEvent(eventType, details)
222211 }
223212 }
224213
225214 fun addInsightsViewed (insightsTypesViewed : List <String >) {
226- if (digmathonInfo.get().isActive()) {
227- this .viewedInsights.addAll(insightsTypesViewed)
215+ if (digmathonInfo.get().isActive() && ! allInsightsExists(insightsTypesViewed)) {
216+ // add only new insight types
217+ insightsTypesViewed.forEach {
218+ this .viewedInsights.computeIfAbsent(it) { Instant .now() }
219+ }
228220 flushInsightsViewedToPersistence()
221+ PersistenceService .getInstance().setDigmathonInsightsViewedLastUpdated(Instant .now())
229222 }
230223 }
231224
225+ private fun allInsightsExists (insightsTypesViewed : List <String >): Boolean {
226+ return this .viewedInsights.keys.containsAll(insightsTypesViewed)
227+ }
228+
229+
232230 private fun flushInsightsViewedToPersistence () {
233- PersistenceService .getInstance().setDigmathonInsightsViewed(viewedInsights.joinToString(" ," ))
231+ try {
232+ val json = objectMapper.writeValueAsString(viewedInsights)
233+ PersistenceService .getInstance().setDigmathonInsightsViewed(json)
234+ } catch (e: Throwable ) {
235+ ErrorReporter .getInstance().reportError(" DigmathonService.flushInsightsViewedToPersistence" , e)
236+ }
237+ }
238+
239+ private fun readInsightsViewedFromPersistence (): MutableMap <String , Instant > {
240+ val json = PersistenceService .getInstance().getDigmathonInsightsViewed() ? : return mutableMapOf ()
241+ return try {
242+ val ref = object : TypeReference <Map <String , Instant >>() {}
243+ val viewedInsights = objectMapper.readValue(json, ref)
244+ viewedInsights.toMutableMap()
245+ } catch (e: Throwable ) {
246+ ErrorReporter .getInstance().reportError(" DigmathonService.readInsightsViewedFromPersistence" , e)
247+ mutableMapOf ()
248+ }
249+ }
250+
251+
252+ fun getDigmathonInsightsViewedLastUpdated (): Instant ? {
253+ return PersistenceService .getInstance().getDigmathonInsightsViewedLastUpdated()
254+ }
255+
256+ fun updateDigmathonInsightsViewedLastUpdated () {
257+ PersistenceService .getInstance().setDigmathonInsightsViewedLastUpdated(Instant .now())
234258 }
235259
236260 fun setFinishDigmathonGameForUser () {
237- PersistenceService .getInstance().setFinishDigmathonGameForUser()
261+ PersistenceService .getInstance().setFinishDigmathonGameForUser(true )
238262 isUserFinishedDigmathon = true
239263 fireUserFinishedDigmathon()
240- reportEvent(" user finished game " )
264+ reportEvent(" user finished digmathon " )
241265 }
242266
243267
@@ -246,4 +270,20 @@ class DigmathonService : Disposable {
246270 .userFinishedDigmathon()
247271 }
248272
273+
274+ data class DigmathonInfo (val startTime : ZonedDateTime , val endTime : ZonedDateTime ) {
275+
276+ fun isActive (): Boolean {
277+ val now = LocalDateTime .now().atZone(ZoneId .systemDefault())
278+
279+ return (now.equals(startTime) || now.isAfter(startTime)) &&
280+ now.isBefore(endTime)
281+ }
282+
283+ fun isEnded (): Boolean {
284+ return ! isActive()
285+ }
286+ }
287+
288+
249289}
0 commit comments