merge: W52K —— 「后台保持连接」开关 + 前台服务(默认关;开了才有常驻通知)
This commit is contained in:
commit
579eccdfd1
10 files changed
+630
No files matched your search
@@ -0,0 +1,32 @@
|
||||
package top.yeij.cyrene.core.keepalive
|
||||
|
||||
import android.app.NotificationManager
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* 常驻通知的**对外形状**(渠道 id / 重要级)。文案与图标测不了(要 Android 运行时),
|
||||
* 但这两项能测,而且测它们是有意义的:
|
||||
*
|
||||
* - 重要级**建渠道那一刻就定死**了(应用改不动,只有用户能改)。它要是被写成 DEFAULT / HIGH,
|
||||
* 这个「保活」功能就会开始响铃、震动、弹横幅——那不是保活,是骚扰,用户只能自己去关掉那条渠道。
|
||||
* - 渠道 id 是用户静音设置的挂靠点:改了它等于换一条新渠道,用户之前对旧渠道的一切设置失效。
|
||||
*/
|
||||
class KeepAliveNotificationTest {
|
||||
|
||||
@Test
|
||||
fun `the channel never rings or vibrates`() {
|
||||
assertTrue(
|
||||
"渠道重要级只许是 MIN / LOW,现在是 $KEEP_ALIVE_CHANNEL_IMPORTANCE",
|
||||
KEEP_ALIVE_CHANNEL_IMPORTANCE == NotificationManager.IMPORTANCE_MIN ||
|
||||
KEEP_ALIVE_CHANNEL_IMPORTANCE == NotificationManager.IMPORTANCE_LOW,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the channel id is a stable literal`() {
|
||||
// 恒定不变是刻意的:换 id = 换渠道 = 用户对旧渠道做的静音设置全部作废。
|
||||
assertEquals("keep_alive", KEEP_ALIVE_CHANNEL_ID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package top.yeij.cyrene.core.keepalive
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import top.yeij.cyrene.core.prefs.AppPrefs
|
||||
|
||||
/**
|
||||
* 「后台保持连接」的判据(纯函数)。
|
||||
*
|
||||
* 钉的是四件事:
|
||||
* 1. **默认关**——这个功能会常驻一条通知、多耗电,主人拍板默认关,任何人把它改成默认开都是一次
|
||||
* 产品决定,不该悄悄发生;
|
||||
* 2. 三个条件缺一不可(开关 / 登录态 / 通知权限),任何一条不成立都不许跑;
|
||||
* 3. 几种状态各自该走哪一步(登出一定停、关开关一定停、**自举中一定不动**);
|
||||
* 4. 四个入参的**全表 16 格**都与判据一致——尤其是「登录态未定 ⇒ 不动」这一条,写成穷举才排除得掉
|
||||
* 「某个组合下热启动会把还跑着的服务收掉」这种走神。
|
||||
*/
|
||||
class KeepAlivePolicyTest {
|
||||
|
||||
@Test
|
||||
fun `the feature is off until the user turns it on`() {
|
||||
assertFalse("默认必须是关", AppPrefs().keepAliveEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all three conditions are required`() {
|
||||
assertFalse(shouldKeepAliveRun(enabled = false, loggedIn = true, notificationsAllowed = true))
|
||||
assertFalse(shouldKeepAliveRun(enabled = true, loggedIn = false, notificationsAllowed = true))
|
||||
assertFalse(shouldKeepAliveRun(enabled = true, loggedIn = true, notificationsAllowed = false))
|
||||
assertTrue(shouldKeepAliveRun(enabled = true, loggedIn = true, notificationsAllowed = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `logging out stops the service even though the switch stays on`() {
|
||||
// 登出只关连接,不改开关(AppSettings 是 App 级的,不随登出清空)——所以这里必须由判据
|
||||
// 自己把服务收掉,不能指望开关被重置。
|
||||
assertEquals(
|
||||
KeepAliveAction.Stop,
|
||||
keepAliveAction(enabled = true, sessionKnown = true, loggedIn = false, notificationsAllowed = true),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an enabled switch with an undecided session is left alone`() {
|
||||
// 热启动(Activity 重建、进程还活着)时自举会先经过 Unknown:这一刻把还跑着的服务收掉,
|
||||
// 用户会看到通知消失又回来,而中间那段进程正好没人保护。
|
||||
for (loggedIn in listOf(true, false)) {
|
||||
for (allowed in listOf(true, false)) {
|
||||
assertEquals(
|
||||
"登录态未定(sessionKnown=false)时不许动手:loggedIn=$loggedIn allowed=$allowed",
|
||||
KeepAliveAction.Leave,
|
||||
keepAliveAction(enabled = true, sessionKnown = false, loggedIn = loggedIn, notificationsAllowed = allowed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `switching off stops the service without waiting for a session`() {
|
||||
assertEquals(
|
||||
KeepAliveAction.Stop,
|
||||
keepAliveAction(enabled = false, sessionKnown = false, loggedIn = false, notificationsAllowed = true),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `every combination of the four inputs agrees with the judgement`() {
|
||||
val bools = listOf(true, false)
|
||||
for (enabled in bools) {
|
||||
for (known in bools) {
|
||||
for (loggedIn in bools) {
|
||||
for (allowed in bools) {
|
||||
val wanted = known && shouldKeepAliveRun(enabled, loggedIn, allowed)
|
||||
val expected = when {
|
||||
wanted -> KeepAliveAction.Start
|
||||
// 唯一「不许有主张」的一格:开关开着,但登录态还没定。
|
||||
enabled && !known -> KeepAliveAction.Leave
|
||||
else -> KeepAliveAction.Stop
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
"enabled=$enabled known=$known loggedIn=$loggedIn allowed=$allowed",
|
||||
expected,
|
||||
keepAliveAction(enabled, known, loggedIn, allowed),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package top.yeij.cyrene.ui.settings
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* 开关下面那行字(纯函数)。
|
||||
*
|
||||
* 这一行的价值全在「说真话」,所以钉的也是真话:
|
||||
* 1. **开的代价自己说出来**(常驻通知 + 多耗电)——不说的代价会变成用户某天「莫名多了条通知」的意外;
|
||||
* 2. **跑不了的时候不许说在跑**(通知权限被拒 / 被撤时,一个字都不提「已开」);
|
||||
* 3. 四个分支互不相同:任何一次改文案都不该把某一档悄悄吞掉。
|
||||
*/
|
||||
class KeepAliveSummaryTest {
|
||||
|
||||
@Test
|
||||
fun `the off state says what it costs to stay off`() {
|
||||
val text = keepAliveSummary(enabled = false, notificationsAllowed = true, permissionJustDenied = false)
|
||||
|
||||
assertTrue("关着要说清后果(消息不丢但连接会断):$text", text.contains("消息不会丢"))
|
||||
assertTrue(text.contains("断开"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the on state spells out the price`() {
|
||||
val text = keepAliveSummary(enabled = true, notificationsAllowed = true, permissionJustDenied = false)
|
||||
|
||||
assertTrue("必须明说会常驻一条通知:$text", text.contains("通知"))
|
||||
assertTrue("必须明说会多耗电:$text", text.contains("电"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an enabled switch without the notification permission never claims to be working`() {
|
||||
val text = keepAliveSummary(enabled = true, notificationsAllowed = false, permissionJustDenied = false)
|
||||
|
||||
assertFalse("权限不在,绝不能写成「已开」:$text", text.contains("已开"))
|
||||
assertTrue("要说清为什么不起作用:$text", text.contains("起不来"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a just denied request reports the denial instead of pretending success`() {
|
||||
val text = keepAliveSummary(enabled = false, notificationsAllowed = false, permissionJustDenied = true)
|
||||
|
||||
assertTrue("要说清被拒了:$text", text.contains("拒绝"))
|
||||
assertFalse("被拒时不许写成「已开」:$text", text.contains("已开"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the four states are told apart`() {
|
||||
val texts = listOf(
|
||||
keepAliveSummary(enabled = false, notificationsAllowed = true, permissionJustDenied = false),
|
||||
keepAliveSummary(enabled = true, notificationsAllowed = true, permissionJustDenied = false),
|
||||
keepAliveSummary(enabled = true, notificationsAllowed = false, permissionJustDenied = false),
|
||||
keepAliveSummary(enabled = false, notificationsAllowed = false, permissionJustDenied = true),
|
||||
)
|
||||
|
||||
assertEquals("四个分支必须四句不同的话", 4, texts.toSet().size)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user