Skip to content

Commit ae0f43b

Browse files
authored
[port] Fix memory leak due to skipped internal animators not unregistered (#2431) (#2444)
1 parent 312d55d commit ae0f43b

File tree

3 files changed

+92
-10
lines changed

3 files changed

+92
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
Mapbox welcomes participation and contributions from everyone.
44

5+
# 10.17.1
6+
## Bug fixes 🐞
7+
* Fix memory leak when camera animations are skipped.
8+
59
# 10.17.0 April 02, 2024
610
## Bug fixes 🐞
711
* Fix map being pixelated on some devices when `ContextMode.SHARED` is used (e.g. in AndroidAuto extension).

plugin-animation/src/main/java/com/mapbox/maps/plugin/animation/CameraAnimationsPluginImpl.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -413,23 +413,24 @@ class CameraAnimationsPluginImpl : CameraAnimationsPlugin, MapCameraPlugin {
413413

414414
private fun finishAnimation(animation: Animator, finishStatus: AnimationFinishStatus) {
415415
(animation as? CameraAnimator<*>)?.apply {
416-
if (skipped) {
417-
return
418-
}
419-
runningAnimatorsQueue.remove(animation)
420416
if (debugMode) {
421417
val logText = when (finishStatus) {
422418
AnimationFinishStatus.CANCELED -> "was canceled."
423419
AnimationFinishStatus.ENDED -> "ended."
424420
}
425-
logI(TAG, "Animation ${type.name}(${hashCode()}) $logText")
421+
logI(TAG, "Animation ${type.name}(${hashCode()})${if (skipped) " skipped" else ""} $logText")
426422
}
423+
// Even if skipped we need to unregister it
427424
if (isInternal) {
425+
unregisterAnimators(this, cancelAnimators = false)
428426
if (debugMode) {
429-
logI(TAG, "Internal Animator ${type.name}(${hashCode()}) was unregistered")
427+
logI(TAG, "Internal Animator ${type.name}(${hashCode()}) was unregistered (${animators.size})")
430428
}
431-
unregisterAnimators(this, cancelAnimators = false)
432429
}
430+
if (skipped) {
431+
return
432+
}
433+
runningAnimatorsQueue.remove(animation)
433434
if (runningAnimatorsQueue.isEmpty()) {
434435
mapTransformDelegate.setUserAnimationInProgress(false)
435436
}
@@ -443,7 +444,7 @@ class CameraAnimationsPluginImpl : CameraAnimationsPlugin, MapCameraPlugin {
443444
commitChanges()
444445
}
445446
} ?: throw MapboxCameraAnimationException(
446-
"Could not start animation as it must be an instance of CameraAnimator and not null!"
447+
"Could not finish animation as it must be an instance of CameraAnimator and not null!"
447448
)
448449
}
449450
})
@@ -507,6 +508,9 @@ class CameraAnimationsPluginImpl : CameraAnimationsPlugin, MapCameraPlugin {
507508
}
508509
}
509510
animators.addAll(cameraAnimators.map { it as CameraAnimator<*> })
511+
if (debugMode) {
512+
logI(TAG, "Registered ${cameraAnimators.size} animators. Currently, ${animators.size} animators registered.")
513+
}
510514
}
511515
}
512516

plugin-animation/src/test/java/com/mapbox/maps/plugin/animation/CameraAnimationsPluginImplTest.kt

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ class CameraAnimationsPluginImplTest {
102102
animators.forEach {
103103
verify { it.addInternalListener(any()) }
104104
}
105+
animators.forEach {
106+
assertTrue(cameraAnimationsPluginImpl.animators.contains(it))
107+
}
105108
}
106109

107110
@Test
@@ -117,6 +120,7 @@ class CameraAnimationsPluginImplTest {
117120
it.removeInternalListener()
118121
}
119122
}
123+
assertTrue("Animators is not empty", cameraAnimationsPluginImpl.animators.isEmpty())
120124
}
121125

122126
@Test
@@ -316,10 +320,14 @@ class CameraAnimationsPluginImplTest {
316320
every {
317321
mapCameraManagerDelegate.cameraState
318322
} answers { cameraPosition.toCameraState() }
319-
every { mapCameraManagerDelegate.setCamera(any<CameraOptions>()) } answers {
323+
324+
every {
325+
mapCameraManagerDelegate.setCamera(any<CameraOptions>())
326+
} answers {
320327
cameraPosition = firstArg<CameraOptions>()
321328
cameraAnimationsPluginImpl.onCameraMove(cameraPosition.toCameraState())
322329
}
330+
323331
val targetPitch = 5.0
324332
val cameraOptions = CameraOptions.Builder().pitch(targetPitch).build()
325333
val expectedValues = mutableSetOf(targetPitch)
@@ -372,6 +380,72 @@ class CameraAnimationsPluginImplTest {
372380
assertArrayEquals(expectedValues.toDoubleArray(), updatedValues.toDoubleArray(), EPS)
373381
}
374382

383+
@Test
384+
fun `two same immediate animations, second is skipped`() {
385+
var cameraPosition = CameraOptions.Builder().build()
386+
every {
387+
mapCameraManagerDelegate.cameraState
388+
} answers {
389+
cameraPosition.toCameraState()
390+
}
391+
every {
392+
mapCameraManagerDelegate.setCamera(any<CameraOptions>())
393+
} answers {
394+
cameraPosition = firstArg<CameraOptions>()
395+
cameraAnimationsPluginImpl.onCameraMove(cameraPosition.toCameraState())
396+
}
397+
398+
val cameraAnimatorOptions = cameraAnimatorOptions(10.0) {
399+
startValue(10.0)
400+
}
401+
val bearingFirst = CameraBearingAnimator(cameraAnimatorOptions, true) {
402+
duration = 0
403+
}
404+
bearingFirst.addListener(
405+
onStart = {
406+
assertEquals(2, cameraAnimationsPluginImpl.animators.size)
407+
assertEquals(false, (it as CameraAnimator<*>).skipped)
408+
}
409+
)
410+
411+
val bearingSecond = CameraBearingAnimator(cameraAnimatorOptions, true) {
412+
duration = 0
413+
}
414+
bearingSecond.addListener(
415+
onStart = {
416+
assertEquals(1, cameraAnimationsPluginImpl.animators.size)
417+
assertEquals(true, (it as CameraAnimator<*>).skipped)
418+
}
419+
)
420+
421+
cameraAnimationsPluginImpl.playAnimatorsTogether(bearingFirst, bearingSecond)
422+
assertTrue("Animators is not empty", cameraAnimationsPluginImpl.animators.isEmpty())
423+
}
424+
425+
@Test
426+
fun `animation skipped if camera already has target value`() {
427+
val cameraPosition = CameraOptions.Builder().bearing(10.0).build().toCameraState()
428+
// Make sure current camera animations plugin has the right initial value
429+
cameraAnimationsPluginImpl.onCameraMove(cameraPosition)
430+
431+
val cameraAnimatorOptions = cameraAnimatorOptions(10.0) {
432+
startValue(10.0)
433+
}
434+
435+
val bearingAnimator = CameraBearingAnimator(cameraAnimatorOptions, true) {
436+
duration = 0
437+
}
438+
bearingAnimator.addListener(
439+
onStart = {
440+
assertEquals(1, cameraAnimationsPluginImpl.animators.size)
441+
assertEquals(true, (it as CameraAnimator<*>).skipped)
442+
}
443+
)
444+
445+
cameraAnimationsPluginImpl.playAnimatorsTogether(bearingAnimator)
446+
assertTrue("Animators is not empty", cameraAnimationsPluginImpl.animators.isEmpty())
447+
}
448+
375449
@Test
376450
fun testEaseToSequenceDurationZero() {
377451
var cameraPosition = CameraState(
@@ -405,9 +479,9 @@ class CameraAnimationsPluginImplTest {
405479

406480
shadowOf(getMainLooper()).pause()
407481

408-
val handler = Handler(getMainLooper())
409482
cameraAnimationsPluginImpl.easeTo(cameraOptions1, mapAnimationOptions { duration(0) })
410483

484+
val handler = Handler(getMainLooper())
411485
handler.postDelayed(
412486
{
413487
cameraAnimationsPluginImpl.easeTo(

0 commit comments

Comments
 (0)