11package com.google.ai.sample
22
33import android.app.Service
4- import android.content.Context
54import android.content.Intent
6- import android.os.Handler
75import android.os.IBinder
8- import android.os.Looper
96import android.util.Log
107import kotlinx.coroutines.CoroutineScope
118import kotlinx.coroutines.Dispatchers
129import kotlinx.coroutines.Job
1310import kotlinx.coroutines.delay
11+ import kotlinx.coroutines.isActive
1412import kotlinx.coroutines.launch
13+ import org.json.JSONException
1514import org.json.JSONObject
15+ import java.io.IOException
1616import java.net.HttpURLConnection
17+ import java.net.MalformedURLException
18+ import java.net.SocketTimeoutException
1719import java.net.URL
20+ import java.time.OffsetDateTime
21+ import java.time.format.DateTimeParseException
1822
1923class TrialTimerService : Service () {
2024
@@ -31,7 +35,11 @@ class TrialTimerService : Service() {
3135 const val EXTRA_CURRENT_UTC_TIME_MS = " extra_current_utc_time_ms"
3236 private const val TAG = " TrialTimerService"
3337 private const val CHECK_INTERVAL_MS = 60 * 1000L // 1 minute
34- private const val WORLD_TIME_API_URL = " https://worldtimeapi.org/api/timezone/Etc/UTC"
38+ private const val TIME_API_URL = " http://worldclockapi.com/api/json/utc/now" // Changed API URL
39+ private const val CONNECTION_TIMEOUT_MS = 15000 // 15 seconds
40+ private const val READ_TIMEOUT_MS = 15000 // 15 seconds
41+ private const val MAX_RETRIES = 3
42+ private val RETRY_DELAYS_MS = listOf (5000L , 15000L , 30000L ) // 5s, 15s, 30s
3543 }
3644
3745 override fun onStartCommand (intent : Intent ? , flags : Int , startId : Int ): Int {
@@ -46,79 +54,138 @@ class TrialTimerService : Service() {
4654 stopTimerLogic()
4755 }
4856 }
49- return START_STICKY // Keep service running if killed by system
57+ return START_STICKY
5058 }
5159
5260 private fun startTimerLogic () {
5361 isTimerRunning = true
5462 scope.launch {
55- while (isTimerRunning) {
63+ var attempt = 0
64+ while (isTimerRunning && isActive) {
65+ var success = false
5666 try {
57- val url = URL (WORLD_TIME_API_URL )
67+ Log .d(TAG , " Attempting to fetch internet time (attempt ${attempt + 1 } /$MAX_RETRIES ). URL: $TIME_API_URL " )
68+ val url = URL (TIME_API_URL )
5869 val connection = url.openConnection() as HttpURLConnection
5970 connection.requestMethod = " GET"
60- connection.connect()
71+ connection.connectTimeout = CONNECTION_TIMEOUT_MS
72+ connection.readTimeout = READ_TIMEOUT_MS
73+ connection.connect() // Explicit connect call
6174
62- if (connection.responseCode == HttpURLConnection .HTTP_OK ) {
75+ val responseCode = connection.responseCode
76+ Log .d(TAG , " Time API response code: $responseCode " )
77+
78+ if (responseCode == HttpURLConnection .HTTP_OK ) {
6379 val inputStream = connection.inputStream
6480 val result = inputStream.bufferedReader().use { it.readText() }
6581 inputStream.close()
82+ connection.disconnect()
83+
6684 val jsonObject = JSONObject (result)
67- val currentUtcTimeMs = jsonObject.getLong(" unixtime" ) * 1000L
68- Log .d(TAG , " Successfully fetched internet time: $currentUtcTimeMs " )
85+ val currentDateTimeStr = jsonObject.getString(" currentDateTime" )
86+ // Parse ISO 8601 string to milliseconds since epoch
87+ val currentUtcTimeMs = OffsetDateTime .parse(currentDateTimeStr).toInstant().toEpochMilli()
88+
89+ Log .d(TAG , " Successfully fetched and parsed internet time: $currentUtcTimeMs ($currentDateTimeStr )" )
6990
7091 val trialState = TrialManager .getTrialState(applicationContext, currentUtcTimeMs)
92+ Log .d(TAG , " Current trial state from TrialManager: $trialState " )
7193 when (trialState) {
7294 TrialManager .TrialState .NOT_YET_STARTED_AWAITING_INTERNET -> {
7395 TrialManager .startTrialIfNecessaryWithInternetTime(applicationContext, currentUtcTimeMs)
7496 sendBroadcast(Intent (ACTION_INTERNET_TIME_AVAILABLE ).putExtra(EXTRA_CURRENT_UTC_TIME_MS , currentUtcTimeMs))
7597 }
7698 TrialManager .TrialState .ACTIVE_INTERNET_TIME_CONFIRMED -> {
77- // Trial is active, continue checking
7899 sendBroadcast(Intent (ACTION_INTERNET_TIME_AVAILABLE ).putExtra(EXTRA_CURRENT_UTC_TIME_MS , currentUtcTimeMs))
79100 }
80101 TrialManager .TrialState .EXPIRED_INTERNET_TIME_CONFIRMED -> {
81102 Log .d(TAG , " Trial expired based on internet time." )
82103 sendBroadcast(Intent (ACTION_TRIAL_EXPIRED ))
83- stopTimerLogic() // Stop further checks if expired
104+ stopTimerLogic()
84105 }
85106 TrialManager .TrialState .PURCHASED -> {
86107 Log .d(TAG , " App is purchased. Stopping timer." )
87108 stopTimerLogic()
88109 }
89- else -> {
90- // Should not happen if logic is correct
91- Log .w(TAG , " Unhandled trial state: $trialState " )
110+ TrialManager .TrialState .INTERNET_UNAVAILABLE_CANNOT_VERIFY -> {
111+ // This case might occur if TrialManager was called with null time before,
112+ // but now we have time. So we should re-broadcast available time.
113+ Log .w(TAG , " TrialManager reported INTERNET_UNAVAILABLE, but we just fetched time. Broadcasting available." )
114+ sendBroadcast(Intent (ACTION_INTERNET_TIME_AVAILABLE ).putExtra(EXTRA_CURRENT_UTC_TIME_MS , currentUtcTimeMs))
92115 }
93116 }
117+ success = true
118+ attempt = 0 // Reset attempts on success
94119 } else {
95- Log .e(TAG , " Failed to fetch internet time. Response code: ${connection.responseCode} " )
96- sendBroadcast(Intent (ACTION_INTERNET_TIME_UNAVAILABLE ))
120+ Log .e(TAG , " Failed to fetch internet time. HTTP Response code: $responseCode - ${connection.responseMessage} " )
121+ connection.disconnect()
122+ // For server-side errors (5xx), retry is useful. For client errors (4xx), less so unless temporary.
123+ if (responseCode >= 500 ) {
124+ // Retry for server errors
125+ } else {
126+ // For other errors (e.g. 404), might not be worth retrying indefinitely the same way
127+ // but we will follow the general retry logic for now.
128+ }
97129 }
130+ } catch (e: SocketTimeoutException ) {
131+ Log .e(TAG , " Failed to fetch internet time: Socket Timeout" , e)
132+ } catch (e: MalformedURLException ) {
133+ Log .e(TAG , " Failed to fetch internet time: Malformed URL" , e)
134+ stopTimerLogic() // URL is wrong, no point in retrying
135+ return @launch
136+ } catch (e: IOException ) {
137+ Log .e(TAG , " Failed to fetch internet time: IO Exception (e.g., network issue)" , e)
138+ } catch (e: JSONException ) {
139+ Log .e(TAG , " Failed to parse JSON response from time API" , e)
140+ // API might have changed format or returned error HTML, don't retry indefinitely for this specific error on this attempt.
141+ } catch (e: DateTimeParseException ) {
142+ Log .e(TAG , " Failed to parse date/time string from time API response" , e)
98143 } catch (e: Exception ) {
99- Log .e(TAG , " Error fetching internet time or processing trial state" , e)
100- sendBroadcast(Intent (ACTION_INTERNET_TIME_UNAVAILABLE ))
144+ Log .e(TAG , " An unexpected error occurred while fetching or processing internet time" , e)
145+ }
146+
147+ if (! isTimerRunning || ! isActive) break // Exit loop if timer stopped
148+
149+ if (! success) {
150+ attempt++
151+ if (attempt < MAX_RETRIES ) {
152+ val delayMs = RETRY_DELAYS_MS .getOrElse(attempt - 1 ) { RETRY_DELAYS_MS .last() }
153+ Log .d(TAG , " Time fetch failed. Retrying in ${delayMs / 1000 } s..." )
154+ sendBroadcast(Intent (ACTION_INTERNET_TIME_UNAVAILABLE )) // Notify UI about current unavailability before retry
155+ delay(delayMs)
156+ } else {
157+ Log .e(TAG , " Failed to fetch internet time after $MAX_RETRIES attempts. Broadcasting unavailability." )
158+ sendBroadcast(Intent (ACTION_INTERNET_TIME_UNAVAILABLE ))
159+ attempt = 0 // Reset attempts for next full CHECK_INTERVAL_MS cycle
160+ delay(CHECK_INTERVAL_MS ) // Wait for the normal check interval after max retries failed
161+ }
162+ } else {
163+ // Success, wait for the normal check interval
164+ delay(CHECK_INTERVAL_MS )
101165 }
102- delay(CHECK_INTERVAL_MS )
103166 }
167+ Log .d(TAG , " Timer coroutine ended." )
104168 }
105169 }
106170
107171 private fun stopTimerLogic () {
108- isTimerRunning = false
109- job.cancel() // Cancel all coroutines started by this scope
110- stopSelf() // Stop the service itself
111- Log .d(TAG , " Timer stopped and service is stopping." )
172+ if (isTimerRunning) {
173+ Log .d(TAG , " Stopping timer logic..." )
174+ isTimerRunning = false
175+ job.cancel() // Cancel all coroutines started by this scope
176+ stopSelf() // Stop the service itself
177+ Log .d(TAG , " Timer stopped and service is stopping." )
178+ }
112179 }
113180
114181 override fun onBind (intent : Intent ? ): IBinder ? {
115- return null // We are not using binding
182+ return null
116183 }
117184
118185 override fun onDestroy () {
119186 super .onDestroy()
120- stopTimerLogic() // Ensure timer is stopped when service is destroyed
121- Log .d( TAG , " Service Destroyed " )
187+ Log .d( TAG , " Service Destroyed. Ensuring timer is stopped. " )
188+ stopTimerLogic( )
122189 }
123190}
124191
0 commit comments