improve long polling service reliability

This commit is contained in:
2025-03-30 19:54:12 +03:00
parent 5b5e8f8446
commit 935d313257
2 changed files with 58 additions and 39 deletions
@@ -137,7 +137,8 @@ class MainActivity : AppCompatActivity() {
} }
} }
LaunchedEffect(longPollStateToApply) { LifecycleResumeEffect(longPollStateToApply) {
Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply")
if (longPollStateToApply != LongPollState.Background) { if (longPollStateToApply != LongPollState.Background) {
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched() if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
&& longPollCurrentState != longPollStateToApply && longPollCurrentState != longPollStateToApply
@@ -151,6 +152,8 @@ class MainActivity : AppCompatActivity() {
inBackground = longPollStateToApply == LongPollState.Background inBackground = longPollStateToApply == LongPollState.Background
) )
} }
onPauseOrDispose {}
} }
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle() val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
@@ -299,12 +302,19 @@ class MainActivity : AppCompatActivity() {
} }
} }
private val longPollingServiceIntent by lazy {
Intent(this, LongPollingService::class.java)
}
private val onlineServiceIntent by lazy {
Intent(this, OnlineService::class.java)
}
private fun toggleLongPollService( private fun toggleLongPollService(
enable: Boolean, enable: Boolean,
inBackground: Boolean = AppSettings.Experimental.longPollInBackground inBackground: Boolean = AppSettings.Experimental.longPollInBackground
) { ) {
if (enable) { if (enable) {
val longPollIntent = Intent(this, LongPollingService::class.java) val longPollIntent = longPollingServiceIntent
if (inBackground) { if (inBackground) {
ContextCompat.startForegroundService(this, longPollIntent) ContextCompat.startForegroundService(this, longPollIntent)
@@ -312,15 +322,15 @@ class MainActivity : AppCompatActivity() {
startService(longPollIntent) startService(longPollIntent)
} }
} else { } else {
stopService(Intent(this, LongPollingService::class.java)) stopService(longPollingServiceIntent)
} }
} }
private fun toggleOnlineService(enable: Boolean) { private fun toggleOnlineService(enable: Boolean) {
if (enable) { if (enable) {
startService(Intent(this, OnlineService::class.java)) startService(onlineServiceIntent)
} else { } else {
stopService(Intent(this, OnlineService::class.java)) stopService(onlineServiceIntent)
} }
} }
@@ -30,11 +30,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.time.Duration.Companion.seconds
class LongPollingService : Service() { class LongPollingService : Service() {
@@ -42,20 +44,9 @@ class LongPollingService : Service() {
private val job = SupervisorJob() private val job = SupervisorJob()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> private val exceptionHandler =
Log.e(TAG, "error: $throwable") CoroutineExceptionHandler { _, throwable ->
handleError(throwable)
if (throwable !is NoAccessTokenException) {
throwable.printStackTrace()
}
if (throwable is LongPollException) {
// TODO: 23-Mar-25, Danil Nikolaev: restart LongPoll
return@CoroutineExceptionHandler
}
longPollController.updateCurrentState(LongPollState.Exception)
longPollController.setStateToApply(LongPollState.Exception)
} }
private val coroutineContext: CoroutineContext private val coroutineContext: CoroutineContext
@@ -68,6 +59,8 @@ class LongPollingService : Service() {
private var currentJob: Job? = null private var currentJob: Job? = null
private val inBackground get() = AppSettings.Experimental.longPollInBackground
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Log.d(STATE_TAG, "onCreate()") Log.d(STATE_TAG, "onCreate()")
@@ -81,21 +74,12 @@ class LongPollingService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (startId > 1) return START_STICKY if (startId > 1) return START_STICKY
val inBackground = AppSettings.Experimental.longPollInBackground
Log.d( Log.d(
STATE_TAG, STATE_TAG,
"onStartCommand: asForeground: $inBackground; flags: $flags; startId: $startId;\ninstance: $this" "onStartCommand: asForeground: $inBackground; flags: $flags; startId: $startId;\ninstance: $this"
) )
if (currentJob != null) { startJob()
currentJob?.cancel()
currentJob = null
}
coroutineScope.launch {
currentJob = startPolling().also { it.join() }
}
val openCategorySettingsIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val openCategorySettingsIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS) Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
@@ -113,11 +97,6 @@ class LongPollingService : Service() {
PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_IMMUTABLE
) )
longPollController.updateCurrentState(
if (inBackground) LongPollState.Background
else LongPollState.InApp
)
if (inBackground) { if (inBackground) {
val notification = val notification =
NotificationsUtils.createNotification( NotificationsUtils.createNotification(
@@ -139,17 +118,33 @@ class LongPollingService : Service() {
return START_STICKY return START_STICKY
} }
private fun startJob() {
if (currentJob != null) {
currentJob?.cancel()
currentJob = null
}
coroutineScope.launch {
currentJob = startPolling().also { it.join() }
}
}
private fun startPolling(): Job { private fun startPolling(): Job {
if (job.isCompleted || job.isCancelled) { if (job.isCompleted || job.isCancelled) {
Log.d(STATE_TAG, "job is completed or cancelled") Log.d(STATE_TAG, "Job is completed or cancelled")
throw Exception("Job is over") throw Exception("Job is over")
} }
Log.d(STATE_TAG, "job started") Log.d(STATE_TAG, "Starting job...")
return coroutineScope.launch(coroutineContext) { return coroutineScope.launch(coroutineContext) {
longPollController.updateCurrentState(
if (inBackground) LongPollState.Background
else LongPollState.InApp
)
if (UserConfig.accessToken.isEmpty()) { if (UserConfig.accessToken.isEmpty()) {
throw NoAccessTokenException throw NoAccessTokenException()
} }
var serverInfo = getServerInfo() var serverInfo = getServerInfo()
@@ -251,10 +246,24 @@ class LongPollingService : Service() {
} }
} }
private fun handleError(throwable: Throwable) {
Log.e(TAG, "error: $throwable")
if (throwable !is NoAccessTokenException) {
throwable.printStackTrace()
}
coroutineScope.launch {
delay(5.seconds)
startJob()
}
longPollController.updateCurrentState(LongPollState.Exception)
}
override fun onDestroy() { override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy") Log.d(STATE_TAG, "onDestroy")
longPollController.updateCurrentState(LongPollState.Stopped) longPollController.updateCurrentState(LongPollState.Stopped)
updatesParser.clearListeners()
try { try {
AppSettings.edit { putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true) } AppSettings.edit { putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true) }
job.cancel() job.cancel()
@@ -281,4 +290,4 @@ class LongPollingService : Service() {
} }
private data class LongPollException(override val message: String) : Throwable() private data class LongPollException(override val message: String) : Throwable()
private data object NoAccessTokenException : Throwable() private class NoAccessTokenException : Throwable()