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