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