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