1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.notification.collection.notifcollection
18 
19 import android.service.notification.NotificationListenerService.RankingMap
20 import android.util.ArrayMap
21 import com.android.internal.annotations.VisibleForTesting
22 import com.android.systemui.statusbar.notification.collection.NotificationEntry
23 import java.io.PrintWriter
24 
25 class NotifCollectionInconsistencyTracker(val logger: NotifCollectionLogger) {
26     fun attach(
27         collectedKeySetAccessor: () -> Set<String>,
28         coalescedKeySetAccessor: () -> Set<String>,
29     ) {
30         if (attached) {
31             throw RuntimeException("attach() called twice")
32         }
33         attached = true
34         this.collectedKeySetAccessor = collectedKeySetAccessor
35         this.coalescedKeySetAccessor = coalescedKeySetAccessor
36     }
37 
38     fun logNewMissingNotifications(rankingMap: RankingMap) {
39         val currentCollectedKeys = collectedKeySetAccessor()
40         val currentCoalescedKeys = coalescedKeySetAccessor()
41         val newMissingNotifications = rankingMap.orderedKeys.asSequence()
42             .filter { it !in currentCollectedKeys }
43             .filter { it !in currentCoalescedKeys }
44             .toSet()
45         maybeLogMissingNotifications(missingNotifications, newMissingNotifications)
46         missingNotifications = newMissingNotifications
47     }
48 
49     @VisibleForTesting
50     fun maybeLogMissingNotifications(
51         oldMissingKeys: Set<String>,
52         newMissingKeys: Set<String>,
53     ) {
54         if (oldMissingKeys.isEmpty() && newMissingKeys.isEmpty()) return
55         if (oldMissingKeys == newMissingKeys) return
56         (oldMissingKeys - newMissingKeys).sorted().let { justFound ->
57             if (justFound.isNotEmpty()) {
58                 logger.logFoundNotifications(justFound, newMissingKeys.size)
59             }
60         }
61         (newMissingKeys - oldMissingKeys).sorted().let { goneMissing ->
62             if (goneMissing.isNotEmpty()) {
63                 logger.logMissingNotifications(goneMissing, newMissingKeys.size)
64             }
65         }
66     }
67 
68     fun logNewInconsistentRankings(
69         currentEntriesWithoutRankings: ArrayMap<String, NotificationEntry>?,
70         rankingMap: RankingMap,
71     ) {
72         maybeLogInconsistentRankings(
73             notificationsWithoutRankings,
74             currentEntriesWithoutRankings ?: emptyMap(),
75             rankingMap
76         )
77         notificationsWithoutRankings = currentEntriesWithoutRankings?.keys ?: emptySet()
78     }
79 
80     @VisibleForTesting
81     fun maybeLogInconsistentRankings(
82         oldKeysWithoutRankings: Set<String>,
83         newEntriesWithoutRankings: Map<String, NotificationEntry>,
84         rankingMap: RankingMap,
85     ) {
86         if (oldKeysWithoutRankings.isEmpty() && newEntriesWithoutRankings.isEmpty()) return
87         if (oldKeysWithoutRankings == newEntriesWithoutRankings.keys) return
88         val newlyConsistent: List<String> = oldKeysWithoutRankings
89             .mapNotNull { key ->
90                 key.takeIf { key !in newEntriesWithoutRankings }
91                     .takeIf { key in rankingMap.orderedKeys }
92             }.sorted()
93         if (newlyConsistent.isNotEmpty()) {
94             val totalInconsistent: Int = newEntriesWithoutRankings.size
95             logger.logRecoveredRankings(newlyConsistent, totalInconsistent)
96         }
97         val newlyInconsistent: List<NotificationEntry> = newEntriesWithoutRankings
98             .mapNotNull { (key, entry) ->
99                 entry.takeIf { key !in oldKeysWithoutRankings }
100             }.sortedBy { it.key }
101         if (newlyInconsistent.isNotEmpty()) {
102             val totalInconsistent: Int = newEntriesWithoutRankings.size
103             logger.logMissingRankings(newlyInconsistent, totalInconsistent, rankingMap)
104         }
105     }
106 
107     fun dump(pw: PrintWriter) {
108         pw.println("notificationsWithoutRankings: ${notificationsWithoutRankings.size}")
109         for (key in notificationsWithoutRankings) {
110             pw.println("\t * : $key")
111         }
112         pw.println("missingNotifications: ${missingNotifications.size}")
113         for (key in missingNotifications) {
114             pw.println("\t * : $key")
115         }
116     }
117 
118     private var attached: Boolean = false
119     private lateinit var collectedKeySetAccessor: (() -> Set<String>)
120     private lateinit var coalescedKeySetAccessor: (() -> Set<String>)
121     private var notificationsWithoutRankings = emptySet<String>()
122     private var missingNotifications = emptySet<String>()
123 }
124