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 }