• 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.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