Skip to content

Commit 52cfe9c

Browse files
committed
NAVAND-777: use scopes instead of CancellableHandler
1 parent 8abb014 commit 52cfe9c

12 files changed

Lines changed: 265 additions & 339 deletions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.mapbox.navigation.core.internal.utils
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.Job
5+
import kotlinx.coroutines.SupervisorJob
6+
import kotlin.coroutines.CoroutineContext
7+
import kotlin.coroutines.EmptyCoroutineContext
8+
9+
internal object CoroutineUtils {
10+
11+
fun createChildScope(
12+
parentJob: Job,
13+
additionalContext: CoroutineContext = EmptyCoroutineContext
14+
): CoroutineScope = CoroutineScope(SupervisorJob(parentJob) + additionalContext)
15+
}

libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/CancellableHandler.kt

Lines changed: 0 additions & 34 deletions
This file was deleted.

libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/PlannedRouteRefreshController.kt

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,49 @@ import androidx.annotation.VisibleForTesting
44
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
55
import com.mapbox.navigation.base.route.NavigationRoute
66
import com.mapbox.navigation.base.route.RouteRefreshOptions
7+
import com.mapbox.navigation.core.internal.utils.CoroutineUtils
78
import com.mapbox.navigation.utils.internal.logI
89
import com.mapbox.navigation.utils.internal.logW
10+
import kotlinx.coroutines.CancellationException
911
import kotlinx.coroutines.CoroutineScope
12+
import kotlinx.coroutines.cancel
13+
import kotlinx.coroutines.delay
14+
import kotlinx.coroutines.job
15+
import kotlinx.coroutines.launch
1016

1117
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
1218
internal class PlannedRouteRefreshController @VisibleForTesting constructor(
1319
private val routeRefresherExecutor: RouteRefresherExecutor,
1420
private val routeRefreshOptions: RouteRefreshOptions,
1521
private val stateHolder: RouteRefreshStateHolder,
1622
private val listener: RouteRefresherListener,
17-
private val cancellableHandler: CancellableHandler,
23+
private val parentScope: CoroutineScope,
1824
private val retryStrategy: RetryRouteRefreshStrategy,
1925
) {
2026

2127
constructor(
2228
routeRefresherExecutor: RouteRefresherExecutor,
2329
routeRefreshOptions: RouteRefreshOptions,
2430
stateHolder: RouteRefreshStateHolder,
25-
scope: CoroutineScope,
31+
parentScope: CoroutineScope,
2632
listener: RouteRefresherListener,
2733
) : this(
2834
routeRefresherExecutor,
2935
routeRefreshOptions,
3036
stateHolder,
3137
listener,
32-
CancellableHandler(scope),
38+
parentScope,
3339
RetryRouteRefreshStrategy(maxAttemptsCount = MAX_RETRY_COUNT)
3440
)
3541

42+
private var plannedRefreshScope =
43+
CoroutineUtils.createChildScope(parentScope.coroutineContext.job)
3644
private var paused = false
3745
var routesToRefresh: List<NavigationRoute>? = null
3846
private set
3947

4048
fun startRoutesRefreshing(routes: List<NavigationRoute>) {
41-
cancellableHandler.cancelAll()
49+
recreateScope()
4250
routesToRefresh = null
4351
if (routes.isEmpty()) {
4452
logI("Routes are empty, nothing to refresh", RouteRefreshLog.LOG_CATEGORY)
@@ -69,7 +77,7 @@ internal class PlannedRouteRefreshController @VisibleForTesting constructor(
6977
fun pause() {
7078
if (!paused) {
7179
paused = true
72-
cancellableHandler.cancelAll()
80+
recreateScope()
7381
}
7482
}
7583

@@ -99,11 +107,15 @@ internal class PlannedRouteRefreshController @VisibleForTesting constructor(
99107
}
100108

101109
private fun postAttempt(attemptBlock: suspend () -> Unit) {
102-
cancellableHandler.postDelayed(
103-
timeout = routeRefreshOptions.intervalMillis,
104-
block = attemptBlock,
105-
cancellationCallback = { stateHolder.onCancel() }
106-
)
110+
plannedRefreshScope.launch {
111+
try {
112+
delay(routeRefreshOptions.intervalMillis)
113+
attemptBlock()
114+
} catch (ex: CancellationException) {
115+
stateHolder.onCancel()
116+
throw ex
117+
}
118+
}
107119
}
108120

109121
private suspend fun executePlannedRefresh(
@@ -137,6 +149,11 @@ internal class PlannedRouteRefreshController @VisibleForTesting constructor(
137149
)
138150
}
139151

152+
private fun recreateScope() {
153+
plannedRefreshScope.cancel()
154+
plannedRefreshScope = CoroutineUtils.createChildScope(parentScope.coroutineContext.job)
155+
}
156+
140157
companion object {
141158

142159
const val MAX_RETRY_COUNT = 2

libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshController.kt

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@ package com.mapbox.navigation.core.routerefresh
33
import com.mapbox.navigation.base.ExperimentalPreviewMapboxNavigationAPI
44
import com.mapbox.navigation.base.route.NavigationRoute
55
import com.mapbox.navigation.utils.internal.logI
6-
import kotlinx.coroutines.CoroutineScope
7-
import kotlinx.coroutines.cancel
6+
import kotlinx.coroutines.Job
87

98
@OptIn(ExperimentalPreviewMapboxNavigationAPI::class)
109
class RouteRefreshController internal constructor(
11-
private val routeRefreshScope: CoroutineScope,
10+
private val routeRefreshParentJob: Job,
1211
private val plannedRouteRefreshController: PlannedRouteRefreshController,
1312
private val immediateRouteRefreshController: ImmediateRouteRefreshController,
1413
private val stateHolder: RouteRefreshStateHolder,
@@ -56,7 +55,7 @@ class RouteRefreshController internal constructor(
5655
refreshObserversManager.unregisterAllObservers()
5756
stateHolder.unregisterAllRouteRefreshStateObservers()
5857
// first unregister observers, then cancel scope - otherwise we dispatch CANCELLED state from onDestroy
59-
routeRefreshScope.cancel()
58+
routeRefreshParentJob.cancel()
6059
}
6160

6261
internal fun requestPlannedRouteRefresh(routes: List<NavigationRoute>) {

libnavigation-core/src/main/java/com/mapbox/navigation/core/routerefresh/RouteRefreshControllerProvider.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ import com.mapbox.navigation.core.RoutesProgressDataProvider
77
import com.mapbox.navigation.core.directions.session.DirectionsSession
88
import com.mapbox.navigation.core.ev.EVDynamicDataHolder
99
import com.mapbox.navigation.core.ev.EVRefreshDataProvider
10+
import com.mapbox.navigation.core.internal.utils.CoroutineUtils
1011
import com.mapbox.navigation.core.routealternatives.AlternativeMetadataProvider
1112
import com.mapbox.navigation.utils.internal.Time
1213
import kotlinx.coroutines.CoroutineDispatcher
13-
import kotlinx.coroutines.CoroutineScope
1414
import kotlinx.coroutines.SupervisorJob
1515
import java.util.Date
1616

@@ -50,22 +50,22 @@ internal object RouteRefreshControllerProvider {
5050
routeRefreshOptions.intervalMillis * 3
5151
)
5252

53-
val routeRefreshParentScope = CoroutineScope(SupervisorJob())
53+
val routeRefreshParentJob = SupervisorJob()
5454
val plannedRouteRefreshController = PlannedRouteRefreshController(
5555
routeRefresherExecutor,
5656
routeRefreshOptions,
5757
stateHolder,
58-
CoroutineScope(routeRefreshParentScope.coroutineContext + immediateDispatcher),
58+
CoroutineUtils.createChildScope(routeRefreshParentJob, immediateDispatcher),
5959
routeRefresherResultProcessor
6060
)
6161
val immediateRouteRefreshController = ImmediateRouteRefreshController(
6262
routeRefresherExecutor,
6363
stateHolder,
64-
CoroutineScope(routeRefreshParentScope.coroutineContext + dispatcher),
64+
CoroutineUtils.createChildScope(routeRefreshParentJob, dispatcher),
6565
routeRefresherResultProcessor
6666
)
6767
return RouteRefreshController(
68-
routeRefreshParentScope,
68+
routeRefreshParentJob,
6969
plannedRouteRefreshController,
7070
immediateRouteRefreshController,
7171
stateHolder,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.mapbox.navigation.core.internal.utils
2+
3+
import kotlinx.coroutines.Job
4+
import kotlinx.coroutines.SupervisorJob
5+
import kotlinx.coroutines.cancel
6+
import kotlinx.coroutines.delay
7+
import kotlinx.coroutines.launch
8+
import org.junit.After
9+
import org.junit.Assert.assertFalse
10+
import org.junit.Assert.assertTrue
11+
import org.junit.Before
12+
import org.junit.Test
13+
14+
class CoroutineUtilsTest {
15+
16+
private lateinit var parentJob: Job
17+
18+
@Before
19+
fun setUp() {
20+
parentJob = SupervisorJob()
21+
}
22+
23+
@After
24+
fun tearDown() {
25+
parentJob.cancel()
26+
}
27+
28+
@Test
29+
fun createChildScope_parentCancellationCancelsChildren() {
30+
val scope1 = CoroutineUtils.createChildScope(parentJob)
31+
val scope2 = CoroutineUtils.createChildScope(parentJob)
32+
33+
val job1 = scope1.launch {
34+
delay(5000)
35+
}
36+
val job2 = scope2.launch {
37+
delay(5000)
38+
}
39+
40+
assertFalse(job1.isCancelled)
41+
assertFalse(job2.isCancelled)
42+
43+
parentJob.cancel()
44+
45+
assertTrue(job1.isCancelled)
46+
assertTrue(job2.isCancelled)
47+
}
48+
49+
@Test
50+
fun createChildScope_childDoesNotCancelOtherChildren() {
51+
val scope1 = CoroutineUtils.createChildScope(parentJob)
52+
val scope2 = CoroutineUtils.createChildScope(parentJob)
53+
54+
val job1 = scope1.launch {
55+
delay(5000)
56+
}
57+
val job2 = scope2.launch {
58+
delay(5000)
59+
}
60+
61+
assertFalse(job1.isCancelled)
62+
assertFalse(job2.isCancelled)
63+
64+
scope1.cancel()
65+
66+
assertTrue(job1.isCancelled)
67+
assertFalse(job2.isCancelled)
68+
}
69+
}

0 commit comments

Comments
 (0)