• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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