Skip to content

Commit 410e6be

Browse files
committed
Remove and mute notifications of floods in the tool of tests
1 parent 557864a commit 410e6be

3 files changed

Lines changed: 136 additions & 19 deletions

File tree

‎tests/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ class ChatTest : AuthorizedTest() {
270270
* Helpers are in [`Extensions.kt`](core/src/main/kotlin/tgx/test/Extensions.kt): `findElement`, `waitForElement`, `failIfPresent`, `openUrl`, `runShellCommand` and others. Helpers of chats are in [`Chats.kt`](ui/src/main/kotlin/tgx/test/Chats.kt): `openSavedMessages`, the only chat where tests send messages, `sendMessage`, `lastMessages`, `visibleTopOfChat` below bars of the chat, e.g. of a pinned message, `openMessageMenu` by a tap at the time of a message, `setAppearance(AppTheme.NIGHT_BLUE, bubbles = false)`, which sets the theme and the chat style without screens of settings, and `textCopiedBy { }`, which returns what the block copied. Any `UiDevice` and `UiObject2` methods can be used as well, except `executeShellCommand`: use `runShellCommand` or `androidx.test.shell.Shell` instead.
271271
* To see ids of elements currently displayed on device: `adb exec-out uiautomator dump /dev/tty`.
272272
* On Windows MSYS2 and Git Bash convert arguments that look like paths, e.g. `/dev/tty`, to Windows paths. Prepend `MSYS2_ARG_CONV_EXCL='*'` to `adb` commands with paths on device.
273-
* Notifications of new messages come without a second account and without sending anything from [`NotificationFlood`](ui/app/kotlin/tgx/test/NotificationFlood.kt), which builds of tests have inside app process: it loads incoming messages of chats of the account from their history and gives the app updates of TDLib about notifications with new ids and dates, as if they had just come. Builds of tests get notifications from TDLib like release builds, and the flood starts without them. A test starts it by a broadcast, e.g. `runShellCommand("am broadcast -n ${getApplicationId()}/tgx.test.NotificationFloodReceiver --ei count 3000 --ei perMinute 3000 --ei chats 5")`, and waits for `done` in logcat of tag `NotificationFlood`, where each second it logs the notifications it sent with the number of mappings and the native heap of the app. When the system caches the app in background, which ends a long flood by "excessive binder traffic during cached", it warns `the app is cached`.
273+
* Notifications of new messages come without a second account and without sending anything from [`NotificationFlood`](ui/app/kotlin/tgx/test/NotificationFlood.kt), which builds of tests have inside app process: it loads incoming messages of chats of the account from their history and gives the app updates of TDLib about notifications with new ids and dates, as if they had just come. Builds of tests get notifications from TDLib like release builds, and the flood starts without them. A test starts it by a broadcast, e.g. `runShellCommand("am broadcast -n ${getApplicationId()}/tgx.test.NotificationFloodReceiver --ei count 3000 --ei perMinute 3000 --ei chats 5")`, and waits for `done` in logcat of tag `NotificationFlood`, where each second it logs the notifications it sent with the number of mappings and the native heap of the app. When the system caches the app in background, which ends a long flood by "excessive binder traffic during cached", it warns `the app is cached`. After the flood `--ez removeAfter true` removes its notifications as TDLib does when they are read, and `--ez muteAfter true` turns notifications of its chats off in the app. Each second it also logs the icons of notifications that the app keeps, which `tgx.test.NotificationIconsReceiver` logs on demand.
274274

275275
#### Profiles of native memory
276276

‎tests/ui/app/AndroidManifest.xml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99
android:name="tgx.test.NotificationFloodReceiver"
1010
android:exported="true"
1111
android:permission="android.permission.DUMP" />
12+
<receiver
13+
android:name="tgx.test.NotificationIconsReceiver"
14+
android:exported="true"
15+
android:permission="android.permission.DUMP" />
16+
<receiver
17+
android:name="tgx.test.CallIconReceiver"
18+
android:exported="true"
19+
android:permission="android.permission.DUMP" />
1220
<receiver
1321
android:name="tgx.test.AppearanceReceiver"
1422
android:exported="true"

‎tests/ui/app/kotlin/tgx/test/NotificationFlood.kt‎

Lines changed: 127 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@ import android.os.Build
88
import android.os.Debug
99
import android.os.SystemClock
1010
import android.util.Log
11+
import android.util.LruCache
1112
import org.drinkless.tdlib.TdApi
13+
import org.thunderdog.challegram.data.TD
1214
import org.thunderdog.challegram.telegram.Tdlib
1315
import org.thunderdog.challegram.telegram.TdlibManager
16+
import org.thunderdog.challegram.telegram.TdlibNotificationUtils
17+
import tgx.td.fromUserId
1418
import java.io.File
1519
import java.util.concurrent.CountDownLatch
1620
import java.util.concurrent.TimeUnit
@@ -33,7 +37,9 @@ class NotificationFloodReceiver : BroadcastReceiver() {
3337
count = intent.getIntExtra("count", 100),
3438
perMinute = intent.getIntExtra("perMinute", 1000),
3539
chatCount = intent.getIntExtra("chats", 1),
36-
textOnly = intent.getBooleanExtra("textOnly", true)
40+
textOnly = intent.getBooleanExtra("textOnly", true),
41+
removeAfter = intent.getBooleanExtra("removeAfter", false),
42+
muteAfter = intent.getBooleanExtra("muteAfter", false)
3743
).start()
3844
}
3945
}
@@ -48,13 +54,19 @@ class NotificationFloodReceiver : BroadcastReceiver() {
4854
* Like TDLib with the options that the app sets, each chat has one group of messages, and a group shows at
4955
* most [groupSizeMax] notifications: older ones are removed by the update that adds a new one. Delays,
5056
* merging of updates and limits of the number of groups of TDLib are left out.
57+
*
58+
* After the flood, [removeAfter] (`--ez removeAfter true`) removes its notifications as TDLib does
59+
* when they are read, and [muteAfter] (`--ez muteAfter true`) turns notifications of its chats off
60+
* in the app.
5161
*/
5262
class NotificationFlood(
5363
private val tdlib: Tdlib,
5464
private val count: Int,
5565
private val perMinute: Int,
5666
private val chatCount: Int,
57-
private val textOnly: Boolean
67+
private val textOnly: Boolean,
68+
private val removeAfter: Boolean,
69+
private val muteAfter: Boolean
5870
) {
5971
companion object {
6072
@Volatile
@@ -134,6 +146,15 @@ class NotificationFlood(
134146
logProgress()
135147
// Notifications of the last updates are still being shown
136148
Thread.sleep(10_000)
149+
if (removeAfter) {
150+
removeNotifications()
151+
}
152+
if (muteAfter) {
153+
muteChats()
154+
}
155+
if (removeAfter || muteAfter) {
156+
Thread.sleep(2_000)
157+
}
137158
logRetained("after")
138159
Log.i(TAG, "done")
139160
}
@@ -176,7 +197,9 @@ class NotificationFlood(
176197
private fun logProgress() {
177198
val maps = File("/proc/self/maps").useLines { it.count() }
178199
val heap = Debug.getNativeHeapAllocatedSize() / 1024
179-
Log.i(TAG, "sent $sent notifications in $updateCount updates, maps $maps, native heap $heap KB")
200+
val icons = iconsOf()
201+
val heapText = "native heap $heap KB"
202+
Log.i(TAG, "sent $sent notifications in $updateCount updates, maps $maps, $heapText, $icons")
180203
// The system kills a cached app for binder traffic of notifications ("excessive binder traffic
181204
// during cached"), which ends a flood without a crash of the app
182205
val state = ActivityManager.RunningAppProcessInfo()
@@ -240,27 +263,113 @@ class NotificationFlood(
240263
return messages
241264
}
242265

243-
private fun execute(function: TdApi.Function<*>): TdApi.Object {
244-
val latch = CountDownLatch(1)
245-
var result: TdApi.Object? = null
246-
tdlib.client().send(function) {
247-
result = it
248-
latch.countDown()
266+
private fun execute(function: TdApi.Function<*>): TdApi.Object = execute(tdlib, function)
267+
268+
private fun removeNotifications() {
269+
// Like TDLib when messages are read: each group loses its shown notifications
270+
for (group in groups) {
271+
val removed = group.shown.toIntArray()
272+
group.shown.clear()
273+
val type = TdApi.NotificationGroupTypeMessages()
274+
send(TdApi.UpdateNotificationGroup(
275+
group.id, type, group.chatId, group.chatId, -1, 0, arrayOf(), removed
276+
))
249277
}
250-
check(latch.await(30, TimeUnit.SECONDS)) { "No result of $function" }
251-
return result.also {
252-
check(it !is TdApi.Error) { "$function failed: $it" }
253-
}!!
254278
}
255279

256-
private fun copyOf(message: TdApi.Message): TdApi.Message {
257-
// Each notification has its own message: TDLib sends a new object for each new message
258-
val copy = TdApi.Message()
259-
for (field in TdApi.Message::class.java.fields) {
280+
private fun muteChats() {
281+
// Notifications of the chats are off in the app, also of mentions and pinned messages, as if
282+
// the user muted them, while the server keeps the settings of the account
283+
for (group in groups) {
284+
val settings = copyOf(tdlib.chatStrict(group.chatId).notificationSettings)
285+
settings.useDefaultMuteFor = false
286+
settings.muteFor = Int.MAX_VALUE
287+
settings.useDefaultDisableMentionNotifications = false
288+
settings.disableMentionNotifications = true
289+
settings.useDefaultDisablePinnedMessageNotifications = false
290+
settings.disablePinnedMessageNotifications = true
291+
send(TdApi.UpdateChatNotificationSettings(group.chatId, settings))
292+
}
293+
}
294+
295+
private fun <T : Any> copyOf(value: T): T {
296+
// Each notification has its own message and each change its own settings: TDLib sends a new
297+
// object for each of them
298+
val copy = value.javaClass.getDeclaredConstructor().newInstance()
299+
for (field in value.javaClass.fields) {
260300
if (!java.lang.reflect.Modifier.isStatic(field.modifiers)) {
261-
field.set(copy, field.get(message))
301+
field.set(copy, field.get(value))
262302
}
263303
}
264304
return copy
265305
}
266306
}
307+
308+
/**
309+
* Icons of notifications that the app keeps: their number, size, and how often notifications found
310+
* them made, e.g. `adb shell am broadcast -n org.example.tgx/tgx.test.NotificationIconsReceiver`
311+
* logs them with tag `NotificationFlood`
312+
*/
313+
class NotificationIconsReceiver : BroadcastReceiver() {
314+
override fun onReceive(context: Context, intent: Intent) {
315+
Log.i(TAG, iconsOf())
316+
}
317+
}
318+
319+
fun iconsOf(): String {
320+
// The cache of the app is private, and only tools read it
321+
val icons = TdlibManager.instance().notificationIcons()
322+
?: return "icons 0, 0 KB, hits 0, misses 0, reuse off"
323+
val field = icons.javaClass.getDeclaredField("icons").apply { isAccessible = true }
324+
val cache = field.get(icons) as LruCache<*, *>
325+
val count = cache.snapshot().size
326+
val size = cache.size() / 1024
327+
return "icons $count, $size KB, hits ${cache.hitCount()}, misses ${cache.missCount()}"
328+
}
329+
330+
private fun execute(tdlib: Tdlib, function: TdApi.Function<*>): TdApi.Object {
331+
val latch = CountDownLatch(1)
332+
var result: TdApi.Object? = null
333+
tdlib.client().send(function) {
334+
result = it
335+
latch.countDown()
336+
}
337+
check(latch.await(30, TimeUnit.SECONDS)) { "No result of $function" }
338+
return result.also {
339+
check(it !is TdApi.Error) { "$function failed: $it" }
340+
}!!
341+
}
342+
343+
/**
344+
* The icon of a call and the icon of a notification of the same private chat, made as
345+
* `TGCallService` and notifications make them, e.g.
346+
* `adb shell am broadcast -n org.example.tgx/tgx.test.CallIconReceiver` logs with tag
347+
* `NotificationFlood` "call icon: same true, icons 1, after the chat 0" for the first private chat
348+
* of the main list: whether both are one bitmap, the icons kept, and the icons kept after no
349+
* notification of the chat is shown anymore
350+
*/
351+
class CallIconReceiver : BroadcastReceiver() {
352+
override fun onReceive(context: Context, intent: Intent) {
353+
val tdlib = TdlibManager.instance().current()
354+
Thread {
355+
val chats = execute(tdlib, TdApi.GetChats(TdApi.ChatListMain(), 100)) as TdApi.Chats
356+
val chat = chats.chatIds.asSequence()
357+
.mapNotNull { tdlib.chat(it) }
358+
.firstOrNull { it.type is TdApi.ChatTypePrivate && !tdlib.isSelfChat(it) }
359+
if (chat == null) {
360+
Log.i(TAG, "call icon: no private chat")
361+
return@Thread
362+
}
363+
val user = tdlib.cache().user((chat.type as TdApi.ChatTypePrivate).userId)!!
364+
// A call doesn't wait for the photo, a notification does: both find it loaded
365+
user.profilePhoto?.small?.let { execute(tdlib, TdApi.DownloadFile(it.id, 32, 0, 0, true)) }
366+
val callIcon = TdlibNotificationUtils.buildLargeIcon(tdlib, fromUserId(user.id),
367+
user.profilePhoto?.small, tdlib.cache().userAccentColor(user), TD.getLetters(user), false,
368+
true)
369+
val messageIcon = TdlibNotificationUtils.buildLargeIcon(tdlib, chat, true)
370+
val kept = iconsOf()
371+
TdlibManager.instance().notificationIcons()?.retainChats(tdlib.id(), emptySet())
372+
Log.i(TAG, "call icon: same ${callIcon === messageIcon}, $kept, after the chat ${iconsOf()}")
373+
}.start()
374+
}
375+
}

0 commit comments

Comments
 (0)