1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package social.grain
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.os.Build
import android.os.SystemClock
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import social.grain.data.push.GrainMessagingService
/**
* Clearing the launcher's badge.
*
* iOS drives the app icon badge from the unseen count directly, so reading the
* notifications takes it down. Android draws that dot from whatever the app
* still has posted in the shade instead, which means a push read inside the
* app — rather than by tapping the notification — used to leave the dot up
* with nothing behind it.
*/
@RunWith(AndroidJUnit4::class)
class NotificationBadgeTest {
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val context = instrumentation.targetContext
private val manager = NotificationManagerCompat.from(context)
private val socialId = 4242
private val otherId = 4343
private val otherChannel = "test_other_channel"
@Before
fun grantPostingAndCreateChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
instrumentation.uiAutomation.grantRuntimePermission(
context.packageName,
Manifest.permission.POST_NOTIFICATIONS,
)
}
GrainMessagingService.ensureChannel(context)
context.getSystemService(NotificationManager::class.java)?.createNotificationChannel(
NotificationChannel(otherChannel, "Other", NotificationManager.IMPORTANCE_LOW),
)
}
@After
fun tidyUp() {
manager.cancel(socialId)
manager.cancel(otherId)
context.getSystemService(NotificationManager::class.java)
?.deleteNotificationChannel(otherChannel)
}
private fun pendingIntent() = PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
private fun isPosted(id: Int) = manager.activeNotifications.any { it.id == id }
/** Posting and cancelling both settle asynchronously, so poll rather than assume. */
private fun awaitPosted(id: Int, expected: Boolean): Boolean {
repeat(50) {
if (isPosted(id) == expected) return true
SystemClock.sleep(100)
}
return false
}
@Test
fun clearingTakesDownSocialPushesAndLeavesEverythingElseUp() {
manager.notify(
socialId,
GrainMessagingService.notification(context, "Someone", "liked it", pendingIntent()),
)
manager.notify(
otherId,
NotificationCompat.Builder(context, otherChannel)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Uploading")
.build(),
)
assertTrue("the social push never posted", awaitPosted(socialId, expected = true))
assertTrue("the other notification never posted", awaitPosted(otherId, expected = true))
GrainMessagingService.clearPosted(context)
assertTrue("the social push is still up", awaitPosted(socialId, expected = false))
assertTrue("an unrelated notification was cancelled too", isPosted(otherId))
}
}