1+ package org.digma.intellij.plugin.digmathon
2+
3+ import com.intellij.collaboration.async.disposingScope
4+ import com.intellij.openapi.Disposable
5+ import com.intellij.openapi.application.ApplicationManager
6+ import com.intellij.openapi.components.Service
7+ import com.intellij.openapi.components.service
8+ import com.intellij.openapi.diagnostic.Logger
9+ import kotlinx.coroutines.CancellationException
10+ import kotlinx.coroutines.cancel
11+ import kotlinx.coroutines.delay
12+ import kotlinx.coroutines.isActive
13+ import kotlinx.coroutines.launch
14+ import org.digma.intellij.plugin.common.allowSlowOperation
15+ import org.digma.intellij.plugin.common.findActiveProject
16+ import org.digma.intellij.plugin.errorreporting.ErrorReporter
17+ import org.digma.intellij.plugin.log.Log
18+ import org.digma.intellij.plugin.notifications.NotificationUtil
19+ import org.digma.intellij.plugin.persistence.PersistenceService
20+ import org.digma.intellij.plugin.posthog.ActivityMonitor
21+ import java.time.LocalDate
22+ import java.time.LocalDateTime
23+ import java.time.ZoneId
24+ import java.time.ZonedDateTime
25+ import java.util.concurrent.atomic.AtomicBoolean
26+ import java.util.concurrent.atomic.AtomicReference
27+ import java.util.function.Supplier
28+ import kotlin.time.Duration.Companion.minutes
29+
30+
31+ @Service(Service .Level .APP )
32+ class DigmathonService : Disposable {
33+
34+
35+ private val logger = Logger .getInstance(this ::class .java)
36+
37+ private val digmathonInfo = AtomicReference (
38+ DigmathonInfo (
39+ LocalDate .of(2024 , 4 , 5 ).atStartOfDay().atZone(ZoneId .systemDefault()),
40+ LocalDate .of(2024 , 4 , 13 ).atStartOfDay().atZone(ZoneId .systemDefault())
41+ )
42+ )
43+
44+ private val isDigmathonActive = AtomicBoolean (digmathonInfo.get().isActive())
45+
46+ val viewedInsights = PersistenceService .getInstance().getDigmathonInsightsViewed()
47+ ?.split(" ," )?.toMutableSet() ? : mutableSetOf ()
48+
49+ var isUserFinishedDigmathon = PersistenceService .getInstance().isFinishDigmathonGameForUser()
50+
51+
52+ companion object {
53+ @JvmStatic
54+ fun getInstance (): DigmathonService {
55+ return service<DigmathonService >()
56+ }
57+ }
58+
59+
60+ override fun dispose () {
61+ // do nothing, used as parent disposable
62+ }
63+
64+
65+ init {
66+
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()) {
88+ end()
89+ } else {
90+
91+ @Suppress(" UnstableApiUsage" )
92+ disposingScope().launch {
93+
94+ // let the project load and hopefully all jcef apps
95+ delay(1 .minutes.inWholeMilliseconds)
96+
97+ while (isActive) {
98+ try {
99+
100+ if (! isDigmathonActive.get() && digmathonInfo.get().isActive()) {
101+ start()
102+ }
103+
104+ if (isDigmathonActive.get() && digmathonInfo.get().isEnded()) {
105+ end()
106+ cancel(" digmathon ended" )
107+ }
108+
109+ delay(1 .minutes.inWholeMilliseconds)
110+
111+ } catch (ce: CancellationException ) {
112+ Log .log(logger::info, " digmathon timer canceled {}" , ce)
113+ } catch (e: Throwable ) {
114+ Log .warnWithException(logger, e, " error in digmathon timer {}" , e)
115+ ErrorReporter .getInstance().reportError(" DigmathonService.timer" , e)
116+ }
117+ }
118+ }
119+ }
120+ }
121+
122+
123+ private fun start () {
124+ isDigmathonActive.set(true )
125+ fireStateChangedEvent()
126+ reportEvent(" start" )
127+ }
128+
129+
130+ private fun end () {
131+ isDigmathonActive.set(false )
132+ DigmathonProductKey ().clear()
133+ fireStateChangedEvent()
134+ fireProductKeyStateChanged()
135+ reportEvent(" end" )
136+ }
137+
138+
139+ // user is active only with these conditions
140+ fun isUserActive (): Boolean {
141+ return digmathonInfo.get().isActive() && getProductKey() != null
142+ }
143+
144+ private fun fireStateChangedEvent () {
145+ ApplicationManager .getApplication().messageBus.syncPublisher(DigmathonActivationEvent .DIGMATHON_ACTIVATION_TOPIC )
146+ .digmathonActivationStateChanged(digmathonInfo.get().isActive())
147+ }
148+
149+
150+ fun getDigmathonState (): DigmathonInfo {
151+ return digmathonInfo.get()
152+ }
153+
154+ fun setProductKey (productKey : String ) {
155+ try {
156+ allowSlowOperation {
157+ DigmathonProductKey ().validateAndSave(productKey)
158+ reportEvent(" set product key" )
159+ }
160+ } catch (e: InvalidProductKeyException ) {
161+ reportEvent(" product key invalid" , mapOf (" productKey" to e.productKey))
162+ ErrorReporter .getInstance().reportError(" ${this ::class .java.simpleName} .setProductKey" , e)
163+ findActiveProject()?.let {
164+ NotificationUtil .showBalloonWarning(it, " invalid Digmathon product key" )
165+ }
166+
167+ } catch (e: Throwable ) {
168+ ErrorReporter .getInstance().reportError(" ${this ::class .java.simpleName} .setProductKey" , e)
169+ } finally {
170+ fireProductKeyStateChanged()
171+ }
172+ }
173+
174+ private fun fireProductKeyStateChanged () {
175+ ApplicationManager .getApplication().messageBus.syncPublisher(DigmathonProductKeyStateChangedEvent .PRODUCT_KEY_STATE_CHANGED_TOPIC )
176+ .productKey(getProductKey())
177+ }
178+
179+
180+ fun getProductKey (): String? {
181+ return try {
182+ allowSlowOperation(Supplier {
183+ DigmathonProductKey ().validateAndGet()
184+ })
185+ } catch (e: InvalidProductKeyException ) {
186+ reportEvent(" product key invalid" , mapOf (" productKey" to e.productKey))
187+ ErrorReporter .getInstance().reportError(" ${this ::class .java.simpleName} .setProductKey" , e)
188+ null
189+ } catch (e: Throwable ) {
190+ ErrorReporter .getInstance().reportError(" ${this ::class .java.simpleName} .setProductKey" , e)
191+ null
192+ }
193+ }
194+
195+
196+ data class DigmathonInfo (val startTime : ZonedDateTime , val endTime : ZonedDateTime ) {
197+
198+ fun isActive (): Boolean {
199+ val now = LocalDateTime .now().atZone(ZoneId .systemDefault())
200+
201+ return (now.equals(startTime) || now.isAfter(startTime)) &&
202+ now.isBefore(endTime)
203+ }
204+
205+ fun isEnded (): Boolean {
206+ return ! isActive()
207+ }
208+ }
209+
210+
211+ private fun reportEvent (eventType : String , details : Map <String , String > = mapOf()) {
212+ findActiveProject()?.let {
213+ ActivityMonitor .getInstance(it).reportDigmathonEvent(eventType, details)
214+ }
215+ }
216+
217+ fun addInsightsViewed (insightsTypesViewed : List <String >) {
218+ if (digmathonInfo.get().isActive()) {
219+ this .viewedInsights.addAll(insightsTypesViewed)
220+ flushInsightsViewedToPersistence()
221+ }
222+ }
223+
224+ private fun flushInsightsViewedToPersistence () {
225+ PersistenceService .getInstance().setDigmathonInsightsViewed(viewedInsights.joinToString(" ," ))
226+ }
227+
228+ fun setFinishDigmathonGameForUser () {
229+ PersistenceService .getInstance().setFinishDigmathonGameForUser()
230+ isUserFinishedDigmathon = true
231+ fireUserFinishedDigmathon()
232+ reportEvent(" user finished game" )
233+ }
234+
235+
236+ private fun fireUserFinishedDigmathon () {
237+ ApplicationManager .getApplication().messageBus.syncPublisher(UserFinishedDigmathonEvent .USER_FINISHED_DIGMATHON_TOPIC )
238+ .userFinishedDigmathon()
239+ }
240+
241+ }
0 commit comments