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 isAdjustmentSupportedForPackagenull129 fun isAdjustmentSupportedForPackage(app: ApplicationInfo, key: String): Boolean = 130 notificationManager.isAdjustmentSupportedForPackage(key, app.packageName) 131 132 fun setAdjustmentSupportedForPackage(app: ApplicationInfo, key: String, enabled: Boolean): 133 Boolean { 134 return try { 135 notificationManager.setAdjustmentSupportedForPackage(key, app.packageName, enabled) 136 true 137 } catch (e: Exception) { 138 Log.w(TAG, "Error calling INotificationManager", e) 139 false 140 } 141 } 142 isUserUnlockednull143 fun isUserUnlocked(user: Int): Boolean { 144 return try { 145 userManager.isUserUnlocked(user) 146 } catch (e: Exception) { 147 Log.w(TAG, "Error calling UserManager", e) 148 false 149 } 150 } 151 getNotificationSummarynull152 override fun getNotificationSummary(app: ApplicationInfo): String { 153 if (!isEnabled(app)) return context.getString(R.string.notifications_disabled) 154 val channelCount = getChannelCount(app) 155 if (channelCount == 0) { 156 return calculateFrequencySummary(getSentCount(app)) 157 } 158 val blockedChannelCount = getBlockedChannelCount(app) 159 if (channelCount == blockedChannelCount) { 160 return context.getString(R.string.notifications_disabled) 161 } 162 val frequencySummary = calculateFrequencySummary(getSentCount(app)) 163 if (blockedChannelCount == 0) return frequencySummary 164 return context.getString( 165 R.string.notifications_enabled_with_info, 166 frequencySummary, 167 context.formatString( 168 R.string.notifications_categories_off, "count" to blockedChannelCount 169 ) 170 ) 171 } 172 getSentCountnull173 private fun getSentCount(app: ApplicationInfo): Int { 174 var sentCount = 0 175 queryEventsForPackageForUser(app).forEachNotificationEvent { sentCount++ } 176 return sentCount 177 } 178 queryEventsForPackageForUsernull179 private fun queryEventsForPackageForUser(app: ApplicationInfo): UsageEvents? { 180 val now = System.currentTimeMillis() 181 val startTime = now - TimeUnit.DAYS.toMillis(DAYS_TO_CHECK) 182 return try { 183 usageStatsManager.queryEventsForPackageForUser( 184 startTime, now, app.userId, app.packageName, context.packageName 185 ) 186 } catch (e: RemoteException) { 187 Log.e(TAG, "Failed IUsageStatsManager.queryEventsForPackageForUser(): ", e) 188 null 189 } 190 } 191 getChannelCountnull192 private fun getChannelCount(app: ApplicationInfo): Int = try { 193 notificationManager.getNumNotificationChannelsForPackage(app.packageName, app.uid, false) 194 } catch (e: Exception) { 195 Log.w(TAG, "Error calling INotificationManager", e) 196 0 197 } 198 getBlockedChannelCountnull199 private fun getBlockedChannelCount(app: ApplicationInfo): Int = try { 200 notificationManager.getBlockedChannelCount(app.packageName, app.uid) 201 } catch (e: Exception) { 202 Log.w(TAG, "Error calling INotificationManager", e) 203 0 204 } 205 calculateFrequencySummarynull206 fun calculateFrequencySummary(sentCount: Int): String { 207 val dailyFrequency = (sentCount.toFloat() / DAYS_TO_CHECK).roundToInt() 208 return if (dailyFrequency > 0) { 209 context.formatString( 210 R.string.notifications_sent_daily, 211 "count" to dailyFrequency, 212 ) 213 } else { 214 context.formatString( 215 R.string.notifications_sent_weekly, 216 "count" to sentCount, 217 ) 218 } 219 } 220 updateChannelnull221 private fun updateChannel(app: ApplicationInfo, channel: NotificationChannel) { 222 notificationManager.updateNotificationChannelForPackage(app.packageName, app.uid, channel) 223 } 224 onlyHasDefaultChannelnull225 private fun onlyHasDefaultChannel(app: ApplicationInfo): Boolean = 226 notificationManager.onlyHasDefaultChannel(app.packageName, app.uid) 227 228 private fun getChannel(app: ApplicationInfo, channelId: String): NotificationChannel? = 229 notificationManager.getNotificationChannelForPackage( 230 app.packageName, app.uid, channelId, null, true 231 ) 232 233 companion object { 234 private const val TAG = "AppNotificationsRepo" 235 236 private const val DAYS_TO_CHECK = 7L 237 238 private fun UsageEvents?.forEachNotificationEvent(action: (UsageEvents.Event) -> Unit) { 239 this ?: return 240 val event = UsageEvents.Event() 241 while (getNextEvent(event)) { 242 if (event.eventType == UsageEvents.Event.NOTIFICATION_INTERRUPTION) { 243 action(event) 244 } 245 } 246 } 247 } 248 } 249