1 /*
2  * 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.app.INotificationManager
21 import android.app.NotificationChannel
22 import android.app.NotificationManager.IMPORTANCE_DEFAULT
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 androidx.test.core.app.ApplicationProvider
31 import androidx.test.ext.junit.runners.AndroidJUnit4
32 import com.android.settings.R
33 import com.android.settingslib.spaprivileged.model.app.IPackageManagers
34 import com.android.settingslib.spaprivileged.model.app.userId
35 import com.google.common.truth.Truth.assertThat
36 import kotlinx.coroutines.flow.first
37 import kotlinx.coroutines.flow.flowOf
38 import kotlinx.coroutines.test.runTest
39 import org.junit.Before
40 import org.junit.Rule
41 import org.junit.Test
42 import org.junit.runner.RunWith
43 import org.mockito.Mock
44 import org.mockito.junit.MockitoJUnit
45 import org.mockito.junit.MockitoRule
46 import org.mockito.kotlin.any
47 import org.mockito.kotlin.eq
48 import org.mockito.kotlin.verify
49 import org.mockito.kotlin.whenever
50 
51 @RunWith(AndroidJUnit4::class)
52 class AppNotificationRepositoryTest {
53     @get:Rule
54     val mockito: MockitoRule = MockitoJUnit.rule()
55 
56     private val context: Context = ApplicationProvider.getApplicationContext()
57 
58     @Mock
59     private lateinit var packageManagers: IPackageManagers
60 
61     @Mock
62     private lateinit var usageStatsManager: IUsageStatsManager
63 
64     @Mock
65     private lateinit var notificationManager: INotificationManager
66 
67     private lateinit var repository: AppNotificationRepository
68 
69     @Before
setUpnull70     fun setUp() {
71         repository = AppNotificationRepository(
72             context,
73             packageManagers,
74             usageStatsManager,
75             notificationManager,
76         )
77     }
78 
mockOnlyHasDefaultChannelnull79     private fun mockOnlyHasDefaultChannel(): NotificationChannel {
80         whenever(notificationManager.onlyHasDefaultChannel(APP.packageName, APP.uid))
81             .thenReturn(true)
82         val channel =
83             NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, null, IMPORTANCE_DEFAULT)
84         whenever(
85             notificationManager.getNotificationChannelForPackage(
86                 APP.packageName, APP.uid, NotificationChannel.DEFAULT_CHANNEL_ID, null, true
87             )
88         ).thenReturn(channel)
89         return channel
90     }
91 
mockIsEnablednull92     private fun mockIsEnabled(app: ApplicationInfo, enabled: Boolean) {
93         whenever(notificationManager.areNotificationsEnabledForPackage(app.packageName, app.uid))
94             .thenReturn(enabled)
95     }
96 
mockChannelCountnull97     private fun mockChannelCount(app: ApplicationInfo, count: Int) {
98         whenever(
99             notificationManager.getNumNotificationChannelsForPackage(
100                 app.packageName,
101                 app.uid,
102                 false,
103             )
104         ).thenReturn(count)
105     }
106 
mockBlockedChannelCountnull107     private fun mockBlockedChannelCount(app: ApplicationInfo, count: Int) {
108         whenever(notificationManager.getBlockedChannelCount(app.packageName, app.uid))
109             .thenReturn(count)
110     }
111 
mockSentCountnull112     private fun mockSentCount(app: ApplicationInfo, sentCount: Int) {
113         val events = (1..sentCount).map {
114             UsageEvents.Event().apply {
115                 mEventType = UsageEvents.Event.NOTIFICATION_INTERRUPTION
116             }
117         }
118         whenever(
119             usageStatsManager.queryEventsForPackageForUser(
120                 any(), any(), eq(app.userId), eq(app.packageName), any()
121             )
122         ).thenReturn(UsageEvents(events, arrayOf()))
123     }
124 
125     @Test
<lambda>null126     fun getAggregatedUsageEvents() = runTest {
127         val events = listOf(
128             UsageEvents.Event().apply {
129                 mEventType = UsageEvents.Event.NOTIFICATION_INTERRUPTION
130                 mPackage = PACKAGE_NAME
131                 mTimeStamp = 2
132             },
133             UsageEvents.Event().apply {
134                 mEventType = UsageEvents.Event.NOTIFICATION_INTERRUPTION
135                 mPackage = PACKAGE_NAME
136                 mTimeStamp = 3
137             },
138             UsageEvents.Event().apply {
139                 mEventType = UsageEvents.Event.NOTIFICATION_INTERRUPTION
140                 mPackage = PACKAGE_NAME
141                 mTimeStamp = 6
142             },
143         )
144         whenever(usageStatsManager.queryEventsForUser(any(), any(), eq(USER_ID), any()))
145             .thenReturn(UsageEvents(events, arrayOf()))
146 
147         val usageEvents = repository.getAggregatedUsageEvents(flowOf(USER_ID)).first()
148 
149         assertThat(usageEvents).containsExactly(
150             PACKAGE_NAME, NotificationSentState(lastSent = 6, sentCount = 3),
151         )
152     }
153 
154     @Test
isEnablednull155     fun isEnabled() {
156         mockIsEnabled(app = APP, enabled = true)
157 
158         val isEnabled = repository.isEnabled(APP)
159 
160         assertThat(isEnabled).isTrue()
161     }
162 
163     @Test
isChangeable_importanceLockednull164     fun isChangeable_importanceLocked() {
165         whenever(notificationManager.isImportanceLocked(APP.packageName, APP.uid)).thenReturn(true)
166 
167         val isChangeable = repository.isChangeable(APP)
168 
169         assertThat(isChangeable).isFalse()
170     }
171 
172     @Test
isChangeable_appTargetSnull173     fun isChangeable_appTargetS() {
174         val targetSApp = ApplicationInfo().apply {
175             targetSdkVersion = Build.VERSION_CODES.S
176         }
177 
178         val isChangeable = repository.isChangeable(targetSApp)
179 
180         assertThat(isChangeable).isTrue()
181     }
182 
183     @Test
isChangeable_appTargetTiramisuWithoutNotificationPermissionnull184     fun isChangeable_appTargetTiramisuWithoutNotificationPermission() {
185         val targetTiramisuApp = ApplicationInfo().apply {
186             targetSdkVersion = Build.VERSION_CODES.TIRAMISU
187         }
188         with(packageManagers) {
189             whenever(targetTiramisuApp.hasRequestPermission(Manifest.permission.POST_NOTIFICATIONS))
190                 .thenReturn(false)
191         }
192 
193         val isChangeable = repository.isChangeable(targetTiramisuApp)
194 
195         assertThat(isChangeable).isFalse()
196     }
197 
198     @Test
isChangeable_appTargetTiramisuWithNotificationPermissionnull199     fun isChangeable_appTargetTiramisuWithNotificationPermission() {
200         val targetTiramisuApp = ApplicationInfo().apply {
201             targetSdkVersion = Build.VERSION_CODES.TIRAMISU
202         }
203         with(packageManagers) {
204             whenever(targetTiramisuApp.hasRequestPermission(Manifest.permission.POST_NOTIFICATIONS))
205                 .thenReturn(true)
206         }
207 
208         val isChangeable = repository.isChangeable(targetTiramisuApp)
209 
210         assertThat(isChangeable).isTrue()
211     }
212 
213     @Test
setEnabled_toTrueWhenOnlyHasDefaultChannelnull214     fun setEnabled_toTrueWhenOnlyHasDefaultChannel() {
215         val channel = mockOnlyHasDefaultChannel()
216 
217         repository.setEnabled(app = APP, enabled = true)
218 
219         verify(notificationManager)
220             .updateNotificationChannelForPackage(APP.packageName, APP.uid, channel)
221         assertThat(channel.importance).isEqualTo(IMPORTANCE_UNSPECIFIED)
222     }
223 
224     @Test
setEnabled_toFalseWhenOnlyHasDefaultChannelnull225     fun setEnabled_toFalseWhenOnlyHasDefaultChannel() {
226         val channel = mockOnlyHasDefaultChannel()
227 
228         repository.setEnabled(app = APP, enabled = false)
229 
230         verify(notificationManager)
231             .updateNotificationChannelForPackage(APP.packageName, APP.uid, channel)
232         assertThat(channel.importance).isEqualTo(IMPORTANCE_NONE)
233     }
234 
235     @Test
setEnabled_toTrueWhenNotOnlyHasDefaultChannelnull236     fun setEnabled_toTrueWhenNotOnlyHasDefaultChannel() {
237         whenever(notificationManager.onlyHasDefaultChannel(APP.packageName, APP.uid))
238             .thenReturn(false)
239 
240         repository.setEnabled(app = APP, enabled = true)
241 
242         verify(notificationManager)
243             .setNotificationsEnabledForPackage(APP.packageName, APP.uid, true)
244     }
245 
246     @Test
getNotificationSummary_notEnablednull247     fun getNotificationSummary_notEnabled() {
248         mockIsEnabled(app = APP, enabled = false)
249 
250         val summary = repository.getNotificationSummary(APP)
251 
252         assertThat(summary).isEqualTo(context.getString(R.string.notifications_disabled))
253     }
254 
255     @Test
getNotificationSummary_noChannelnull256     fun getNotificationSummary_noChannel() {
257         mockIsEnabled(app = APP, enabled = true)
258         mockChannelCount(app = APP, count = 0)
259         mockSentCount(app = APP, sentCount = 1)
260 
261         val summary = repository.getNotificationSummary(APP)
262 
263         assertThat(summary).isEqualTo("About 1 notification per week")
264     }
265 
266     @Test
getNotificationSummary_allChannelsBlockednull267     fun getNotificationSummary_allChannelsBlocked() {
268         mockIsEnabled(app = APP, enabled = true)
269         mockChannelCount(app = APP, count = 2)
270         mockBlockedChannelCount(app = APP, count = 2)
271 
272         val summary = repository.getNotificationSummary(APP)
273 
274         assertThat(summary).isEqualTo(context.getString(R.string.notifications_disabled))
275     }
276 
277     @Test
getNotificationSummary_noChannelBlockednull278     fun getNotificationSummary_noChannelBlocked() {
279         mockIsEnabled(app = APP, enabled = true)
280         mockChannelCount(app = APP, count = 2)
281         mockSentCount(app = APP, sentCount = 2)
282         mockBlockedChannelCount(app = APP, count = 0)
283 
284         val summary = repository.getNotificationSummary(APP)
285 
286         assertThat(summary).isEqualTo("About 2 notifications per week")
287     }
288 
289     @Test
getNotificationSummary_someChannelsBlockednull290     fun getNotificationSummary_someChannelsBlocked() {
291         mockIsEnabled(app = APP, enabled = true)
292         mockChannelCount(app = APP, count = 2)
293         mockSentCount(app = APP, sentCount = 3)
294         mockBlockedChannelCount(app = APP, count = 1)
295 
296         val summary = repository.getNotificationSummary(APP)
297 
298         assertThat(summary).isEqualTo("About 3 notifications per week / 1 category turned off")
299     }
300 
301     @Test
calculateFrequencySummary_dailynull302     fun calculateFrequencySummary_daily() {
303         val summary = repository.calculateFrequencySummary(4)
304 
305         assertThat(summary).isEqualTo("About 1 notification per day")
306     }
307 
308     @Test
calculateFrequencySummary_weeklynull309     fun calculateFrequencySummary_weekly() {
310         val summary = repository.calculateFrequencySummary(3)
311 
312         assertThat(summary).isEqualTo("About 3 notifications per week")
313     }
314 
315     private companion object {
316         const val USER_ID = 0
317         const val PACKAGE_NAME = "package.name"
<lambda>null318         val APP = ApplicationInfo().apply {
319             packageName = PACKAGE_NAME
320             uid = 123
321         }
322     }
323 }