From d93050240a3e2994452f207bc29590e160a01ba1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 21 Jan 2020 18:48:19 +0100 Subject: [PATCH] Start reworking networkConnectivityCheck (WIP) --- .../android/internal/di/MatrixComponent.kt | 2 - .../network/NetworkConnectivityChecker.kt | 233 +++++++++++------- .../android/internal/session/SessionModule.kt | 5 + .../room/timeline/DefaultPaginationTask.kt | 5 +- .../session/room/timeline/DefaultTimeline.kt | 2 - .../internal/session/sync/job/SyncService.kt | 22 +- .../internal/session/sync/job/SyncThread.kt | 19 +- .../android/internal/task/ConfigurableTask.kt | 3 - .../android/internal/task/TaskConstraints.kt | 22 -- .../android/internal/task/TaskExecutor.kt | 14 +- .../riotx/core/services/VectorSyncService.kt | 17 +- 11 files changed, 189 insertions(+), 155 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt index e8fa659d8d..a912e421df 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MatrixComponent.kt @@ -56,8 +56,6 @@ internal interface MatrixComponent { fun sessionParamsStore(): SessionParamsStore - fun networkConnectivityChecker(): NetworkConnectivityChecker - fun backgroundDetectionObserver(): BackgroundDetectionObserver fun sessionManager(): SessionManager diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt index 6565b8685b..4af6fe8b1c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/NetworkConnectivityChecker.kt @@ -18,104 +18,36 @@ package im.vector.matrix.android.internal.network import android.content.Context import androidx.annotation.WorkerThread +import com.novoda.merlin.Endpoint import com.novoda.merlin.Merlin import com.novoda.merlin.MerlinsBeard -import im.vector.matrix.android.internal.di.MatrixScope +import com.novoda.merlin.ResponseCodeValidator +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.util.BackgroundDetectionObserver +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Collections +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine -@MatrixScope -internal class NetworkConnectivityChecker @Inject constructor(private val context: Context, - private val backgroundDetectionObserver: BackgroundDetectionObserver) - : BackgroundDetectionObserver.Listener { - - private val merlin = Merlin.Builder() - .withConnectableCallbacks() - .withDisconnectableCallbacks() - .build(context) - - private val merlinsBeard = MerlinsBeard.Builder().build(context) - - private val listeners = Collections.synchronizedSet(LinkedHashSet()) - private var hasInternetAccess = merlinsBeard.isConnected - - init { - backgroundDetectionObserver.register(this) - } - +interface NetworkConnectivityChecker { /** * Returns true when internet is available */ @WorkerThread - fun hasInternetAccess(): Boolean { - // If we are in background we have unbound merlin, so we have to check - return if (backgroundDetectionObserver.isInBackground) { - merlinsBeard.hasInternetAccess() - } else { - hasInternetAccess - } - } + fun hasInternetAccess(forcePing: Boolean): Boolean - override fun onMoveToForeground() { - merlin.bind() - merlinsBeard.hasInternetAccess { - hasInternetAccess = it - } - merlin.registerDisconnectable { - if (hasInternetAccess) { - Timber.v("On Disconnect") - hasInternetAccess = false - val localListeners = listeners.toList() - localListeners.forEach { - it.onDisconnect() - } - } - } - merlin.registerConnectable { - if (!hasInternetAccess) { - Timber.v("On Connect") - hasInternetAccess = true - val localListeners = listeners.toList() - localListeners.forEach { - it.onConnect() - } - } - } - } + /** + * Wait until we get internet connection. + */ + suspend fun waitUntilConnected() - override fun onMoveToBackground() { - merlin.unbind() - } - - // In background you won't get notification as merlin is unbound - suspend fun waitUntilConnected() { - if (hasInternetAccess) { - return - } else { - Timber.v("Waiting for network...") - suspendCoroutine { continuation -> - register(object : Listener { - override fun onConnect() { - unregister(this) - Timber.v("Connected to network...") - continuation.resume(Unit) - } - }) - } - } - } - - fun register(listener: Listener) { - listeners.add(listener) - } - - fun unregister(listener: Listener) { - listeners.remove(listener) - } + fun register(listener: Listener) + fun unregister(listener: Listener) interface Listener { fun onConnect() { @@ -125,3 +57,138 @@ internal class NetworkConnectivityChecker @Inject constructor(private val contex } } } + +@SessionScope +internal class MerlinNetworkConnectivityChecker @Inject constructor(context: Context, + homeServerConnectionConfig: HomeServerConnectionConfig, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val backgroundDetectionObserver: BackgroundDetectionObserver) + : NetworkConnectivityChecker { + + private val waitingForNetwork = AtomicBoolean(false) + private val isMerlinBounded = AtomicBoolean(false) + private val endpointString = "${homeServerConnectionConfig.homeServerUri}/_matrix/client/versions" + private val endpoint = Endpoint.from(endpointString) + private val responseCodeValidator = ResponseCodeValidator { responseCode -> + responseCode == 204 || responseCode == 400 || responseCode == 404 + } + + private val merlin = Merlin.Builder() + .withEndpoint(endpoint) + .withResponseCodeValidator(responseCodeValidator) + .withAllCallbacks() + .build(context) + + private val merlinsBeard = MerlinsBeard.Builder() + .withEndpoint(endpoint) + .withResponseCodeValidator(responseCodeValidator) + .build(context) + + private val hasInternetAccess = AtomicBoolean(merlinsBeard.isConnected) + + private val listeners = Collections.synchronizedSet(LinkedHashSet()) + + private val backgroundDetectionObserverListener = object : BackgroundDetectionObserver.Listener { + override fun onMoveToForeground() { + bindMerlinIfNeeded() + } + + override fun onMoveToBackground() { + unbindMerlinIfNeeded() + } + } + + /** + * Returns true when internet is available + */ + @WorkerThread + override fun hasInternetAccess(forcePing: Boolean): Boolean { + return if (forcePing) { + merlinsBeard.hasInternetAccess() + } else { + hasInternetAccess.get() + } + } + + private fun bindMerlinIfNeeded() { + if (isMerlinBounded.get()) { + return + } + Timber.v("Bind merlin") + isMerlinBounded.set(true) + merlin.bind() + merlinsBeard.hasInternetAccess { + hasInternetAccess.set(it) + } + merlin.registerBindable { + Timber.v("On Network available: ${it.isAvailable}") + } + merlin.registerDisconnectable { + Timber.v("On Disconnect") + hasInternetAccess.set(false) + val localListeners = listeners.toList() + localListeners.forEach { + it.onDisconnect() + } + } + merlin.registerConnectable { + Timber.v("On Connect") + hasInternetAccess.set(true) + val localListeners = listeners.toList() + localListeners.forEach { + it.onConnect() + } + } + } + + private fun unbindMerlinIfNeeded() { + if (backgroundDetectionObserver.isInBackground && !waitingForNetwork.get() && isMerlinBounded.get()) { + isMerlinBounded.set(false) + Timber.v("Unbind merlin") + merlin.unbind() + } + } + + override suspend fun waitUntilConnected() { + val hasInternetAccess = withContext(coroutineDispatchers.io) { + merlinsBeard.hasInternetAccess() + } + if (hasInternetAccess) { + return + } else { + waitingForNetwork.set(true) + bindMerlinIfNeeded() + Timber.v("Waiting for network...") + suspendCoroutine { continuation -> + register(object : NetworkConnectivityChecker.Listener { + override fun onConnect() { + unregister(this) + waitingForNetwork.set(false) + unbindMerlinIfNeeded() + Timber.v("Connected to network...") + continuation.resume(Unit) + } + }) + } + } + } + + override fun register(listener: NetworkConnectivityChecker.Listener) { + if (listeners.isEmpty()) { + if (backgroundDetectionObserver.isInBackground) { + unbindMerlinIfNeeded() + } else { + bindMerlinIfNeeded() + } + backgroundDetectionObserver.register(backgroundDetectionObserverListener) + } + listeners.add(listener) + } + + override fun unregister(listener: NetworkConnectivityChecker.Listener) { + listeners.remove(listener) + if (listeners.isEmpty()) { + backgroundDetectionObserver.unregister(backgroundDetectionObserverListener) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 7739405aef..1e22df7ab3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -35,6 +35,8 @@ import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.SessionRealmConfigurationFactory import im.vector.matrix.android.internal.di.* import im.vector.matrix.android.internal.network.AccessTokenInterceptor +import im.vector.matrix.android.internal.network.MerlinNetworkConnectivityChecker +import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.RetrofitFactory import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater @@ -175,6 +177,9 @@ internal abstract class SessionModule { @Binds abstract fun bindSession(session: DefaultSession): Session + @Binds + abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: MerlinNetworkConnectivityChecker): NetworkConnectivityChecker + @Binds @IntoSet abstract fun bindGroupSummaryUpdater(groupSummaryUpdater: GroupSummaryUpdater): LiveEntityObserver diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt index aa7b4321dc..32f7388d74 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultPaginationTask.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.session.room.timeline +import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.filter.FilterRepository import im.vector.matrix.android.internal.session.room.RoomAPI @@ -37,10 +38,12 @@ internal class DefaultPaginationTask @Inject constructor( private val roomAPI: RoomAPI, private val filterRepository: FilterRepository, private val tokenChunkEventPersistor: TokenChunkEventPersistor, - private val eventBus: EventBus + private val eventBus: EventBus, + private val networkConnectivityChecker: NetworkConnectivityChecker ) : PaginationTask { override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result { + networkConnectivityChecker.waitUntilConnected() val filter = filterRepository.getRoomFilter() val chunk = executeRequest(eventBus) { apiCall = roomAPI.getRoomMessagesFrom(params.roomId, params.from, params.direction.value, params.limit, filter) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 3ca4062161..573a46f10a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -38,7 +38,6 @@ import im.vector.matrix.android.internal.database.query.FilterContent import im.vector.matrix.android.internal.database.query.findAllInRoomWithSendStates import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.whereInRoom -import im.vector.matrix.android.internal.task.TaskConstraints import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.util.Debouncer @@ -504,7 +503,6 @@ internal class DefaultTimeline( Timber.v("Should fetch $limit items $direction") cancelableBag += paginationTask .configureWith(params) { - this.constraints = TaskConstraints(connectedToNetwork = true) this.callback = object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { when (data) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt index df050d7f30..c6845ad0c3 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncService.kt @@ -28,7 +28,10 @@ import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean @@ -97,14 +100,6 @@ abstract class SyncService : Service() { } private suspend fun doSync() { - if (!networkConnectivityChecker.hasInternetAccess()) { - Timber.v("No network reschedule to avoid wasting resources") - sessionId?.also { - onRescheduleAsked(it, isInitialSync, delay = 10_000L) - } - stopMe() - return - } Timber.v("Execute sync request with timeout 0") val params = SyncTask.Params(TIME_OUT) try { @@ -120,9 +115,11 @@ abstract class SyncService : Service() { if (throwable.isTokenError()) { stopMe() } else { - Timber.v("Retry to sync in 5s") - delay(DELAY_FAILURE) - doSync() + Timber.v("Should be rescheduled to avoid wasting resources") + sessionId?.also { + onRescheduleAsked(it, isInitialSync, delay = 10_000L) + } + stopMe() } } } @@ -165,6 +162,5 @@ abstract class SyncService : Service() { companion object { const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID" private const val TIME_OUT = 0L - private const val DELAY_FAILURE = 5_000L } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt index c550d40e1e..878d7ca5dc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/job/SyncThread.kt @@ -25,7 +25,12 @@ import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.util.BackgroundDetectionObserver -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import timber.log.Timber import java.net.SocketTimeoutException import javax.inject.Inject @@ -98,16 +103,16 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, backgroundDetectionObserver.register(this) while (state != SyncState.Killing) { Timber.v("Entering loop, state: $state") - if (!networkConnectivityChecker.hasInternetAccess()) { - Timber.v("No network. Waiting...") - updateStateTo(SyncState.NoNetwork) - synchronized(lock) { lock.wait() } - Timber.v("...unlocked") - } else if (!isStarted) { + if (!isStarted) { Timber.v("Sync is Paused. Waiting...") updateStateTo(SyncState.Paused) synchronized(lock) { lock.wait() } Timber.v("...unlocked") + } else if (!networkConnectivityChecker.hasInternetAccess(forcePing = false)) { + Timber.v("No network. Waiting...") + updateStateTo(SyncState.NoNetwork) + synchronized(lock) { lock.wait() } + Timber.v("...unlocked") } else if (!isTokenValid) { Timber.v("Token is invalid. Waiting...") updateStateTo(SyncState.InvalidToken) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt index 1bf939b91b..b87d2df191 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/ConfigurableTask.kt @@ -36,7 +36,6 @@ internal data class ConfigurableTask( val id: UUID, val callbackThread: TaskThread, val executionThread: TaskThread, - val constraints: TaskConstraints, val retryCount: Int, val callback: MatrixCallback @@ -48,7 +47,6 @@ internal data class ConfigurableTask( var id: UUID = UUID.randomUUID(), var callbackThread: TaskThread = TaskThread.MAIN, var executionThread: TaskThread = TaskThread.IO, - var constraints: TaskConstraints = TaskConstraints(), var retryCount: Int = 0, var callback: MatrixCallback = object : MatrixCallback {} ) { @@ -59,7 +57,6 @@ internal data class ConfigurableTask( id = id, callbackThread = callbackThread, executionThread = executionThread, - constraints = constraints, retryCount = retryCount, callback = callback ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt deleted file mode 100644 index d259576a11..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskConstraints.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - - * Copyright 2019 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - - */ -package im.vector.matrix.android.internal.task - -data class TaskConstraints( - val connectedToNetwork: Boolean = false -) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt index 244cc83901..8dcf85f707 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/task/TaskExecutor.kt @@ -22,14 +22,18 @@ import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.toCancelable -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.EmptyCoroutineContext @MatrixScope -internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers, - private val networkConnectivityChecker: NetworkConnectivityChecker) { +internal class TaskExecutor @Inject constructor(private val coroutineDispatchers: MatrixCoroutineDispatchers) { private val executorScope = CoroutineScope(SupervisorJob()) @@ -40,10 +44,6 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers withContext(task.executionThread.toDispatcher()) { Timber.v("Enqueue task $task") retry(task.retryCount) { - if (task.constraints.connectedToNetwork) { - Timber.v("Waiting network for $task") - networkConnectivityChecker.waitUntilConnected() - } Timber.v("Execute task $task on ${Thread.currentThread().name}") task.execute(task.params) } diff --git a/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt index 314e12db05..7ca96c9c13 100644 --- a/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt +++ b/vector/src/main/java/im/vector/riotx/core/services/VectorSyncService.kt @@ -15,15 +15,13 @@ */ package im.vector.riotx.core.services -import android.app.AlarmManager import android.app.NotificationManager -import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.os.Build import im.vector.matrix.android.internal.session.sync.job.SyncService import im.vector.riotx.R import im.vector.riotx.core.extensions.vectorComponent +import im.vector.riotx.fdroid.receiver.AlarmSyncBroadcastReceiver import im.vector.riotx.features.notifications.NotificationUtils class VectorSyncService : SyncService() { @@ -69,17 +67,6 @@ class VectorSyncService : SyncService() { } private fun reschedule(sessionId: String, delay: Long) { - val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - PendingIntent.getForegroundService(this, 0, newIntent(this, sessionId), 0) - } else { - PendingIntent.getService(this, 0, newIntent(this, sessionId), 0) - } - val firstMillis = System.currentTimeMillis() + delay - val alarmMgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - alarmMgr.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) - } else { - alarmMgr.set(AlarmManager.RTC_WAKEUP, firstMillis, pendingIntent) - } + AlarmSyncBroadcastReceiver.scheduleAlarm(applicationContext, sessionId, delay) } }