1 /* <lambda>null2 * 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.settings.spa.notification 18 19 import android.Manifest 20 import android.annotation.IntRange 21 import android.app.INotificationManager 22 import android.app.NotificationChannel 23 import android.app.NotificationManager.IMPORTANCE_NONE 24 import android.app.NotificationManager.IMPORTANCE_UNSPECIFIED 25 import android.app.usage.IUsageStatsManager 26 import android.app.usage.UsageEvents 27 import android.content.Context 28 import android.content.pm.ApplicationInfo 29 import android.os.Build 30 import android.os.RemoteException 31 import android.os.ServiceManager 32 import android.util.Log 33 import com.android.settings.R 34 import com.android.settingslib.spa.framework.util.formatString 35 import com.android.settingslib.spaprivileged.model.app.IPackageManagers 36 import com.android.settingslib.spaprivileged.model.app.PackageManagers 37 import com.android.settingslib.spaprivileged.model.app.userId 38 import java.util.concurrent.TimeUnit 39 import kotlin.math.max 40 import kotlin.math.roundToInt 41 import kotlinx.coroutines.flow.Flow 42 import kotlinx.coroutines.flow.map 43 44 /** 45 * This contains how often an app sends notifications and how recently it sent one. 46 */ 47 data class NotificationSentState( 48 @IntRange(from = 0) 49 var lastSent: Long = 0, 50 51 @IntRange(from = 0) 52 var sentCount: Int = 0, 53 ) 54 55 interface IAppNotificationRepository { 56 /** Gets the notification summary for the given application. */ 57 fun getNotificationSummary(app: ApplicationInfo): String 58 } 59 60 class AppNotificationRepository( 61 private val context: Context, 62 private val packageManagers: IPackageManagers = PackageManagers, 63 private val usageStatsManager: IUsageStatsManager = IUsageStatsManager.Stub.asInterface( 64 ServiceManager.getService(Context.USAGE_STATS_SERVICE) 65 ), 66 private val notificationManager: INotificationManager = INotificationManager.Stub.asInterface( 67 ServiceManager.getService(Context.NOTIFICATION_SERVICE) 68 ), 69 ) : IAppNotificationRepository { getAggregatedUsageEventsnull70 fun getAggregatedUsageEvents(userIdFlow: Flow<Int>): Flow<Map<String, NotificationSentState>> = 71 userIdFlow.map { userId -> 72 val aggregatedStats = mutableMapOf<String, NotificationSentState>() 73 queryEventsForUser(userId).forEachNotificationEvent { event -> 74 aggregatedStats.getOrPut(event.packageName, ::NotificationSentState).apply { 75 lastSent = max(lastSent, event.timeStamp) 76 sentCount++ 77 } 78 } 79 aggregatedStats 80 } 81 queryEventsForUsernull82 private fun queryEventsForUser(userId: Int): UsageEvents? { 83 val now = System.currentTimeMillis() 84 val startTime = now - TimeUnit.DAYS.toMillis(DAYS_TO_CHECK) 85 return try { 86 usageStatsManager.queryEventsForUser(startTime, now, userId, context.packageName) 87 } catch (e: RemoteException) { 88 Log.e(TAG, "Failed IUsageStatsManager.queryEventsForUser(): ", e) 89 null 90 } 91 } 92 isEnablednull93 fun isEnabled(app: ApplicationInfo): Boolean = 94 notificationManager.areNotificationsEnabledForPackage(app.packageName, app.uid) 95 96 fun isChangeable(app: ApplicationInfo): Boolean { 97 if (notificationManager.isImportanceLocked(app.packageName, app.uid)) { 98 return false 99 } 100 101 // If the app targets T but has not requested the permission, we cannot change the 102 // permission state. 103 return app.targetSdkVersion < Build.VERSION_CODES.TIRAMISU || 104 with(packageManagers) { 105 app.hasRequestPermission(Manifest.permission.POST_NOTIFICATIONS) 106 } 107 } 108 setEnablednull109 fun setEnabled(app: ApplicationInfo, enabled: Boolean): Boolean { 110 if (onlyHasDefaultChannel(app)) { 111 getChannel(app, NotificationChannel.DEFAULT_CHANNEL_ID)?.let { channel -> 112 channel.importance = if (enabled) IMPORTANCE_UNSPECIFIED else IMPORTANCE_NONE 113 updateChannel(app, channel) 114 } 115 } 116 return try { 117 notificationManager.setNotificationsEnabledForPackage(app.packageName, app.uid, enabled) 118 true 119 } catch (e: Exception) { 120 Log.w(TAG, "Error calling INotificationManager", e) 121 false 122 } 123 } 124 getNotificationSummarynull125 override fun getNotificationSummary(app: ApplicationInfo): String { 126 if (!isEnabled(app)) return context.getString(R.string.notifications_disabled) 127 val channelCount = getChannelCount(app) 128 if (channelCount == 0) { 129 return calculateFrequencySummary(getSentCount(app)) 130 } 131 val blockedChannelCount = getBlockedChannelCount(app) 132 if (channelCount == blockedChannelCount) { 133 return context.getString(R.string.notifications_disabled) 134 } 135 val frequencySummary = calculateFrequencySummary(getSentCount(app)) 136 if (blockedChannelCount == 0) return frequencySummary 137 return context.getString( 138 R.string.notifications_enabled_with_info, 139 frequencySummary, 140 context.formatString( 141 R.string.notifications_categories_off, "count" to blockedChannelCount 142 ) 143 ) 144 } 145 getSentCountnull146 private fun getSentCount(app: ApplicationInfo): Int { 147 var sentCount = 0 148 queryEventsForPackageForUser(app).forEachNotificationEvent { sentCount++ } 149 return sentCount 150 } 151 queryEventsForPackageForUsernull152 private fun queryEventsForPackageForUser(app: ApplicationInfo): UsageEvents? { 153 val now = System.currentTimeMillis() 154 val startTime = now - TimeUnit.DAYS.toMillis(DAYS_TO_CHECK) 155 return try { 156 usageStatsManager.queryEventsForPackageForUser( 157 startTime, now, app.userId, app.packageName, context.packageName 158 ) 159 } catch (e: RemoteException) { 160 Log.e(TAG, "Failed IUsageStatsManager.queryEventsForPackageForUser(): ", e) 161 null 162 } 163 } 164 getChannelCountnull165 private fun getChannelCount(app: ApplicationInfo): Int = try { 166 notificationManager.getNumNotificationChannelsForPackage(app.packageName, app.uid, false) 167 } catch (e: Exception) { 168 Log.w(TAG, "Error calling INotificationManager", e) 169 0 170 } 171 getBlockedChannelCountnull172 private fun getBlockedChannelCount(app: ApplicationInfo): Int = try { 173 notificationManager.getBlockedChannelCount(app.packageName, app.uid) 174 } catch (e: Exception) { 175 Log.w(TAG, "Error calling INotificationManager", e) 176 0 177 } 178 calculateFrequencySummarynull179 fun calculateFrequencySummary(sentCount: Int): String { 180 val dailyFrequency = (sentCount.toFloat() / DAYS_TO_CHECK).roundToInt() 181 return if (dailyFrequency > 0) { 182 context.formatString( 183 R.string.notifications_sent_daily, 184 "count" to dailyFrequency, 185 ) 186 } else { 187 context.formatString( 188 R.string.notifications_sent_weekly, 189 "count" to sentCount, 190 ) 191 } 192 } 193 updateChannelnull194 private fun updateChannel(app: ApplicationInfo, channel: NotificationChannel) { 195 notificationManager.updateNotificationChannelForPackage(app.packageName, app.uid, channel) 196 } 197 onlyHasDefaultChannelnull198 private fun onlyHasDefaultChannel(app: ApplicationInfo): Boolean = 199 notificationManager.onlyHasDefaultChannel(app.packageName, app.uid) 200 201 private fun getChannel(app: ApplicationInfo, channelId: String): NotificationChannel? = 202 notificationManager.getNotificationChannelForPackage( 203 app.packageName, app.uid, channelId, null, true 204 ) 205 206 companion object { 207 private const val TAG = "AppNotificationsRepo" 208 209 private const val DAYS_TO_CHECK = 7L 210 211 private fun UsageEvents?.forEachNotificationEvent(action: (UsageEvents.Event) -> Unit) { 212 this ?: return 213 val event = UsageEvents.Event() 214 while (getNextEvent(event)) { 215 if (event.eventType == UsageEvents.Event.NOTIFICATION_INTERRUPTION) { 216 action(event) 217 } 218 } 219 } 220 } 221 } 222