Reproduce lost incoming alert resources after cleanup exceptions
This commit is contained in:
1 parent
f4cfe5ba30
commit
793c2b251b
1 file changed
+217
+217
@@ -0,0 +1,217 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaPlayer
|
||||
import android.media.RingtoneManager
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.utun.chat.ChatApplication
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
/** Реальная очистка менеджера: исключение не отдаёт владение ресурса ОС/GC и не блокирует остальные ресурсы. */
|
||||
class IncomingCallCleanupTest {
|
||||
private lateinit var context: Context
|
||||
private lateinit var power: PowerManager
|
||||
private lateinit var notifications: NotificationManager
|
||||
private fun show(id: Long = 11) = IncomingCallManager.show(id, "123", 99)
|
||||
private fun diagnostic(fragment: String) = assertTrue("missing diagnostic $fragment: ${LogManager.entries.takeLast(12)}",
|
||||
LogManager.entries.any { it.level in listOf("WARN", "ERROR") && fragment in it.message })
|
||||
private fun recover() {
|
||||
power.failures = emptySet(); power.locks.forEach { it.failures = emptySet() }
|
||||
notifications.failures = emptySet()
|
||||
PendingIntent.created.forEach { it.failCancel = false; it.failCancelAfter = false }
|
||||
}
|
||||
|
||||
@Before fun prepare() {
|
||||
assertSame(Looper.getMainLooper(), Looper.myLooper())
|
||||
context = Context(); ChatApplication.instance = context
|
||||
power = context.powerManager!!; notifications = context.notificationManager!!
|
||||
AudioManager.shared = AudioManager(); Build.VERSION.SDK_INT = 36
|
||||
AppLanguage.failContext = false; AppLanguage.failText = false; AppLanguage.language = "en"
|
||||
NotificationCompat.failBuild = false; CallController.endings.clear(); CallController.media = false
|
||||
PendingIntent.reset(); RingtoneManager.reset(); MediaPlayer.reset()
|
||||
IncomingCallManager::class.java.getDeclaredField("initialized").apply { isAccessible = true }.setBoolean(null, false)
|
||||
LogManager.entries.clear(); LogManager.console = true
|
||||
}
|
||||
|
||||
@After fun release() {
|
||||
recover()
|
||||
IncomingCallManager.stopRinging()
|
||||
// Отрицательный контроль старого кода теряет ссылки; host не должен переносить его утечки в следующий сценарий.
|
||||
power.locks.filter { it.held }.forEach { it.release() }
|
||||
PendingIntent.created.filter { !it.cancelled }.forEach { it.cancel() }
|
||||
Looper.getMainLooper().advanceBy(10_000)
|
||||
}
|
||||
|
||||
@Test fun wakeLockQueryAndReleaseFailuresCanBeRetriedAfterAlertWasCleared() {
|
||||
for (failure in listOf("query", "release")) {
|
||||
show(if (failure == "query") 11 else 22)
|
||||
val lock = power.locks.last()
|
||||
lock.failures = setOf(failure)
|
||||
IncomingCallManager.stopRinging()
|
||||
assertFalse(IncomingCallManager.isRinging(if (failure == "query") 11 else 22, false))
|
||||
assertTrue(lock.held); diagnostic("wake lock release failed")
|
||||
lock.failures = emptySet()
|
||||
IncomingCallManager.stopRinging()
|
||||
assertFalse("lost lock on $failure", lock.held)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun releaseAfterEffectFailureDoesNotReleaseAnUnlockedWakeLockTwice() {
|
||||
show()
|
||||
val lock = power.locks.single()
|
||||
lock.failures = setOf("release_after")
|
||||
IncomingCallManager.stopRinging()
|
||||
assertFalse(lock.held); assertEquals(1, lock.releaseCalls)
|
||||
recover(); IncomingCallManager.stopRinging(); IncomingCallManager.stopRinging()
|
||||
assertEquals(1, lock.releaseCalls)
|
||||
show(22)
|
||||
assertEquals(2, power.locks.size); assertTrue(power.locks.last().held)
|
||||
}
|
||||
|
||||
@Test fun replacingAlertCannotOverwriteAWakeLockWhoseCleanupFailed() {
|
||||
show()
|
||||
val old = power.locks.single()
|
||||
old.failures = setOf("release")
|
||||
show(22)
|
||||
assertEquals(1, power.locks.size)
|
||||
assertTrue(IncomingCallManager.isRinging(22, false))
|
||||
assertEquals(2, RingtoneManager.created.size)
|
||||
diagnostic("acquisition skipped")
|
||||
recover(); IncomingCallManager.stopRinging()
|
||||
assertFalse(old.held)
|
||||
show(33)
|
||||
assertEquals(2, power.locks.size); assertTrue(power.locks.last().held)
|
||||
}
|
||||
|
||||
@Test fun partialAcquireAndFailedRollbackKeepAnOwnerUntilStopCanRelease() {
|
||||
power.failures = setOf("acquire", "release")
|
||||
show()
|
||||
val lock = power.locks.single()
|
||||
assertTrue(lock.held); assertEquals(1, RingtoneManager.created.single().playCalls)
|
||||
diagnostic("wake lock failed")
|
||||
recover(); IncomingCallManager.stopRinging()
|
||||
assertFalse(lock.held)
|
||||
}
|
||||
|
||||
@Test fun wakeLocksAreNonCountedAndFailedSetupDoesNotSuppressOtherEffects() {
|
||||
for (failure in listOf("configure", "acquire_before")) {
|
||||
power.failures = setOf(failure)
|
||||
show(if (failure == "configure") 11 else 22)
|
||||
assertEquals(1, RingtoneManager.created.last().playCalls)
|
||||
assertFalse(power.locks.last().held)
|
||||
recover(); IncomingCallManager.stopRinging()
|
||||
}
|
||||
show(33)
|
||||
assertFalse(power.locks.last().counted)
|
||||
assertEquals(30_000L, power.locks.last().timeout)
|
||||
}
|
||||
|
||||
@Test fun failedActionCancellationIsRetriedWithoutRepeatingSuccessfulCleanup() {
|
||||
show()
|
||||
val tokens = PendingIntent.created.toList()
|
||||
val failed = tokens.first().also { it.failCancel = true }
|
||||
IncomingCallManager.stopRinging()
|
||||
assertFalse(failed.cancelled); assertTrue(tokens.drop(1).all { it.cancelled })
|
||||
failed.failCancel = false
|
||||
IncomingCallManager.stopRinging(); IncomingCallManager.stopRinging()
|
||||
assertTrue(failed.cancelled); assertEquals(2, failed.cancelCalls)
|
||||
assertTrue(tokens.drop(1).all { it.cancelCalls == 1 })
|
||||
assertEquals(1, power.locks.single().releaseCalls)
|
||||
assertEquals(1, notifications.cancellations.size)
|
||||
assertEquals(1, RingtoneManager.created.single().stopCalls)
|
||||
}
|
||||
|
||||
@Test fun actionCancellationAfterEffectStillWaitsForASuccessfulAcknowledgment() {
|
||||
show()
|
||||
val token = PendingIntent.created.first().also { it.failCancelAfter = true }
|
||||
IncomingCallManager.stopRinging()
|
||||
assertTrue(token.cancelled)
|
||||
token.failCancelAfter = false
|
||||
IncomingCallManager.stopRinging(); IncomingCallManager.stopRinging()
|
||||
assertEquals(2, token.cancelCalls)
|
||||
}
|
||||
|
||||
@Test fun notificationCancelFailureCanBeRetriedWithoutAnActiveCall() {
|
||||
for (failure in listOf("cancel", "cancel_after")) {
|
||||
show(if (failure == "cancel") 11 else 22)
|
||||
notifications.failures = setOf(failure)
|
||||
val before = notifications.cancellations.size
|
||||
IncomingCallManager.stopRinging(); recover()
|
||||
IncomingCallManager.stopRinging(); IncomingCallManager.stopRinging()
|
||||
assertFalse(notifications.active.containsKey(IncomingCallManager.NOTIF_ID))
|
||||
assertEquals(before + 2, notifications.cancellations.size)
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun publishFailureAfterEffectStillRequiresNotificationCancellation() {
|
||||
notifications.failures = setOf("notify_after")
|
||||
show()
|
||||
assertTrue(notifications.active.containsKey(IncomingCallManager.NOTIF_ID))
|
||||
assertEquals(1, RingtoneManager.created.single().playCalls)
|
||||
diagnostic("publish failed after effect")
|
||||
IncomingCallManager.stopRinging()
|
||||
assertFalse(notifications.active.containsKey(IncomingCallManager.NOTIF_ID))
|
||||
}
|
||||
|
||||
@Test fun initializedManagerCanRetryCleanupWithoutRestartingEffects() {
|
||||
show()
|
||||
power.locks.single().failures = setOf("release")
|
||||
PendingIntent.created.first().failCancel = true; notifications.failures = setOf("cancel")
|
||||
IncomingCallManager.stopRinging(); recover()
|
||||
IncomingCallManager.init()
|
||||
assertFalse(power.locks.single().held)
|
||||
assertTrue(PendingIntent.created.all { it.cancelled }); assertTrue(notifications.active.isEmpty())
|
||||
assertEquals(1, RingtoneManager.created.single().playCalls); assertEquals(1, RingtoneManager.created.single().stopCalls)
|
||||
}
|
||||
|
||||
@Test fun languageChannelFailureDoesNotBlockRetryingStoppedAlertCleanup() {
|
||||
show()
|
||||
power.locks.single().failures = setOf("release")
|
||||
PendingIntent.created.first().failCancel = true; notifications.failures = setOf("cancel")
|
||||
IncomingCallManager.stopRinging(); recover()
|
||||
notifications.failures = setOf("channel")
|
||||
IncomingCallManager.refreshLanguage()
|
||||
assertFalse(power.locks.single().held)
|
||||
assertTrue(PendingIntent.created.all { it.cancelled }); assertTrue(notifications.active.isEmpty())
|
||||
assertEquals(1, RingtoneManager.created.single().playCalls)
|
||||
}
|
||||
|
||||
@Test fun expiredPendingWakeLockIsForgottenWithoutASecondRelease() {
|
||||
show()
|
||||
val lock = power.locks.single()
|
||||
lock.failures = setOf("release")
|
||||
IncomingCallManager.stopRinging()
|
||||
lock.failures = emptySet(); lock.held = false // OS timeout.
|
||||
IncomingCallManager.stopRinging()
|
||||
assertEquals(1, lock.releaseCalls)
|
||||
show(22)
|
||||
assertEquals(2, power.locks.size); assertTrue(power.locks.last().held)
|
||||
}
|
||||
|
||||
@Test fun resourceFailuresResolveIndependentlyAndStaleActionsStayRejected() {
|
||||
show()
|
||||
val lock = power.locks.single().also { it.failures = setOf("release") }
|
||||
val token = PendingIntent.created.last().also { it.failCancel = true }
|
||||
val oldAction = token.send()
|
||||
notifications.failures = setOf("cancel")
|
||||
IncomingCallManager.stopRinging()
|
||||
IncomingCallActionReceiver().onReceive(context, oldAction)
|
||||
assertTrue(CallController.endings.isEmpty())
|
||||
lock.failures = emptySet(); IncomingCallManager.stopRinging(); assertFalse(lock.held)
|
||||
token.failCancel = false; IncomingCallManager.stopRinging(); assertTrue(token.cancelled)
|
||||
notifications.failures = emptySet(); IncomingCallManager.stopRinging()
|
||||
val attempts = notifications.cancellations.size
|
||||
IncomingCallManager.stopRinging()
|
||||
assertEquals(attempts, notifications.cancellations.size)
|
||||
assertTrue(notifications.active.isEmpty()); assertEquals(1, lock.releaseCalls - 1)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user