1 /* 2 * Copyright (C) 2023 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 package com.android.systemui.statusbar.notification.row 17 18 import android.graphics.BitmapFactory 19 import android.graphics.drawable.BitmapDrawable 20 import android.graphics.drawable.Drawable 21 import android.graphics.drawable.Icon 22 import android.net.Uri 23 import android.testing.TestableLooper.RunWithLooper 24 import androidx.test.ext.junit.runners.AndroidJUnit4 25 import androidx.test.filters.SmallTest 26 import com.android.internal.widget.NotificationDrawableConsumer 27 import com.android.systemui.SysuiTestCase 28 import com.android.systemui.graphics.ImageLoader 29 import com.android.systemui.res.R 30 import com.android.systemui.util.mockito.argumentCaptor 31 import com.android.systemui.util.mockito.mock 32 import com.google.common.truth.Truth.assertThat 33 import kotlinx.coroutines.ExperimentalCoroutinesApi 34 import kotlinx.coroutines.test.StandardTestDispatcher 35 import kotlinx.coroutines.test.TestScope 36 import kotlinx.coroutines.test.advanceTimeBy 37 import kotlinx.coroutines.test.runCurrent 38 import kotlinx.coroutines.test.runTest 39 import org.junit.After 40 import org.junit.Before 41 import org.junit.Test 42 import org.junit.runner.RunWith 43 import org.mockito.Mockito.clearInvocations 44 import org.mockito.Mockito.verify 45 import org.mockito.Mockito.verifyZeroInteractions 46 47 private const val FREE_IMAGE_DELAY_MS = 4000L 48 private const val MAX_IMAGE_SIZE = 512 // size of the test drawables in pixels 49 50 @OptIn(ExperimentalCoroutinesApi::class) 51 @SmallTest 52 @RunWithLooper 53 @RunWith(AndroidJUnit4::class) 54 class BigPictureIconManagerTest : SysuiTestCase() { 55 56 private val testDispatcher = StandardTestDispatcher() 57 private val testScope = TestScope(testDispatcher) 58 private val testableResources = context.orCreateTestableResources 59 private val imageLoader: ImageLoader = ImageLoader(context, testDispatcher) 60 private val statsManager = BigPictureStatsManager(mock(), testDispatcher, mock()) 61 private var mockConsumer: NotificationDrawableConsumer = mock() 62 private val drawableCaptor = argumentCaptor<Drawable>() 63 64 private lateinit var iconManager: BigPictureIconManager 65 <lambda>null66 private val expectedDrawable by lazy { 67 context.resources.getDrawable(R.drawable.dessert_zombiegingerbread, context.theme) 68 } <lambda>null69 private val supportedIcon by lazy { 70 Icon.createWithContentUri( 71 Uri.parse( 72 "android.resource://${context.packageName}/${R.drawable.dessert_zombiegingerbread}" 73 ) 74 ) 75 } <lambda>null76 private val unsupportedIcon by lazy { 77 Icon.createWithBitmap( 78 BitmapFactory.decodeResource(context.resources, R.drawable.dessert_donutburger) 79 ) 80 } <lambda>null81 private val invalidIcon by lazy { Icon.createWithContentUri(Uri.parse("this.is/broken")) } 82 83 @Before setUpnull84 fun setUp() { 85 allowTestableLooperAsMainThread() 86 overrideMaxImageSizes() 87 iconManager = 88 BigPictureIconManager( 89 context, 90 imageLoader, 91 statsManager, 92 scope = testScope, 93 mainDispatcher = testDispatcher, 94 bgDispatcher = testDispatcher 95 ) 96 } 97 98 @After tearDownnull99 fun tearDown() { 100 disallowTestableLooperAsMainThread() 101 } 102 103 @Test onIconUpdated_supportedType_placeholderLoadednull104 fun onIconUpdated_supportedType_placeholderLoaded() = 105 testScope.runTest { 106 // WHEN update with a supported icon 107 iconManager.updateIcon(mockConsumer, supportedIcon).run() 108 109 // THEN consumer is updated with a placeholder 110 verify(mockConsumer).setImageDrawable(drawableCaptor.capture()) 111 assertIsPlaceHolder(drawableCaptor.value) 112 assertSize(drawableCaptor.value) 113 } 114 115 @Test onIconUpdated_unsupportedType_fullImageLoadednull116 fun onIconUpdated_unsupportedType_fullImageLoaded() = 117 testScope.runTest { 118 // WHEN update with an unsupported icon 119 iconManager.updateIcon(mockConsumer, unsupportedIcon).run() 120 121 // THEN consumer is updated with the full image 122 verify(mockConsumer).setImageDrawable(drawableCaptor.capture()) 123 assertIsFullImage(drawableCaptor.value) 124 assertSize(drawableCaptor.value) 125 } 126 127 @Test onIconUpdated_withNull_drawableIsNullnull128 fun onIconUpdated_withNull_drawableIsNull() = 129 testScope.runTest { 130 // WHEN update with null 131 iconManager.updateIcon(mockConsumer, null).run() 132 133 // THEN consumer is updated with null 134 verify(mockConsumer).setImageDrawable(null) 135 } 136 137 @Test onIconUpdated_invalidIcon_drawableIsNullnull138 fun onIconUpdated_invalidIcon_drawableIsNull() = 139 testScope.runTest { 140 // WHEN update with an invalid icon 141 iconManager.updateIcon(mockConsumer, invalidIcon).run() 142 143 // THEN consumer is updated with null 144 verify(mockConsumer).setImageDrawable(null) 145 } 146 147 @Test onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithPlaceholdernull148 fun onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithPlaceholder() = 149 testScope.runTest { 150 // GIVEN a consumer is set 151 iconManager.updateIcon(mockConsumer, supportedIcon).run() 152 clearInvocations(mockConsumer) 153 154 // WHEN a new consumer is set 155 val newConsumer: NotificationDrawableConsumer = mock() 156 iconManager.updateIcon(newConsumer, supportedIcon).run() 157 158 // THEN the new consumer is updated 159 verify(newConsumer).setImageDrawable(drawableCaptor.capture()) 160 assertIsPlaceHolder(drawableCaptor.value) 161 assertSize(drawableCaptor.value) 162 // AND nothing happens on the old consumer 163 verifyZeroInteractions(mockConsumer) 164 } 165 166 @Test onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithFullImagenull167 fun onIconUpdated_consumerAlreadySet_newConsumerIsUpdatedWithFullImage() = 168 testScope.runTest { 169 // GIVEN a consumer is set 170 iconManager.updateIcon(mockConsumer, supportedIcon).run() 171 // AND an icon is loaded 172 iconManager.onViewShown(true) 173 runCurrent() 174 clearInvocations(mockConsumer) 175 176 // WHEN a new consumer is set 177 val newConsumer: NotificationDrawableConsumer = mock() 178 iconManager.updateIcon(newConsumer, supportedIcon).run() 179 180 // THEN the new consumer is updated 181 verify(newConsumer).setImageDrawable(drawableCaptor.capture()) 182 assertIsFullImage(drawableCaptor.value) 183 assertSize(drawableCaptor.value) 184 // AND nothing happens on the old consumer 185 verifyZeroInteractions(mockConsumer) 186 } 187 188 @Test onIconUpdated_iconAlreadySet_loadsNewIconnull189 fun onIconUpdated_iconAlreadySet_loadsNewIcon() = 190 testScope.runTest { 191 // GIVEN an icon is set 192 iconManager.updateIcon(mockConsumer, supportedIcon).run() 193 clearInvocations(mockConsumer) 194 195 // WHEN a new icon is set 196 iconManager.updateIcon(mockConsumer, unsupportedIcon).run() 197 198 // THEN consumer is updated with the new image 199 verify(mockConsumer).setImageDrawable(drawableCaptor.capture()) 200 assertIsFullImage(drawableCaptor.value) 201 assertSize(drawableCaptor.value) 202 } 203 204 @Test onIconUpdated_iconAlreadySetForTheSameIcon_loadsIconAgainnull205 fun onIconUpdated_iconAlreadySetForTheSameIcon_loadsIconAgain() = 206 testScope.runTest { 207 // GIVEN an icon is set 208 iconManager.updateIcon(mockConsumer, supportedIcon).run() 209 // AND the view is shown 210 iconManager.onViewShown(true) 211 runCurrent() 212 clearInvocations(mockConsumer) 213 // WHEN the icon is set again 214 iconManager.updateIcon(mockConsumer, supportedIcon).run() 215 216 // THEN consumer is updated with the new image 217 verify(mockConsumer).setImageDrawable(drawableCaptor.capture()) 218 assertIsFullImage(drawableCaptor.value) 219 assertSize(drawableCaptor.value) 220 } 221 222 @Test onIconUpdated_iconAlreadySetForUnsupportedIcon_loadsNewIconnull223 fun onIconUpdated_iconAlreadySetForUnsupportedIcon_loadsNewIcon() = 224 testScope.runTest { 225 // GIVEN an unsupported icon is set 226 iconManager.updateIcon(mockConsumer, unsupportedIcon).run() 227 // AND the view is shown 228 iconManager.onViewShown(true) 229 runCurrent() 230 clearInvocations(mockConsumer) 231 232 // WHEN a new icon is set 233 iconManager.updateIcon(mockConsumer, supportedIcon).run() 234 235 // THEN consumer is updated with the new image 236 verify(mockConsumer).setImageDrawable(drawableCaptor.capture()) 237 assertIsFullImage(drawableCaptor.value) 238 assertSize(drawableCaptor.value) 239 } 240 241 @Test onIconUpdated_supportedTypeButTooWide_resizedPlaceholderLoadednull242 fun onIconUpdated_supportedTypeButTooWide_resizedPlaceholderLoaded() = 243 testScope.runTest { 244 // GIVEN the max width is smaller than our image 245 testableResources.addOverride( 246 com.android.internal.R.dimen.notification_big_picture_max_width, 247 20 248 ) 249 iconManager.updateMaxImageSizes() 250 251 // WHEN update with a supported icon 252 iconManager.updateIcon(mockConsumer, supportedIcon).run() 253 254 // THEN consumer is updated with the resized placeholder 255 verify(mockConsumer).setImageDrawable(drawableCaptor.capture()) 256 assertIsPlaceHolder(drawableCaptor.value) 257 assertSize(drawableCaptor.value, expectedWidth = 20, expectedHeight = 20) 258 } 259 260 @Test onIconUpdated_supportedTypeButTooHigh_resizedPlaceholderLoadednull261 fun onIconUpdated_supportedTypeButTooHigh_resizedPlaceholderLoaded() = 262 testScope.runTest { 263 // GIVEN the max height is smaller than our image 264 testableResources.addOverride( 265 com.android.internal.R.dimen.notification_big_picture_max_height, 266 20 267 ) 268 iconManager.updateMaxImageSizes() 269 270 // WHEN update with a supported icon 271 iconManager.updateIcon(mockConsumer, supportedIcon).run() 272 273 // THEN consumer is updated with the resized placeholder 274 verify(mockConsumer).setImageDrawable(drawableCaptor.capture()) 275 assertIsPlaceHolder(drawableCaptor.value) 276 assertSize(drawableCaptor.value, expectedWidth = 20, expectedHeight = 20) 277 } 278 279 @Test onViewShown_placeholderShowing_fullImageLoadednull280 fun onViewShown_placeholderShowing_fullImageLoaded() = 281 testScope.runTest { 282 // GIVEN placeholder is showing 283 iconManager.updateIcon(mockConsumer, supportedIcon).run() 284 clearInvocations(mockConsumer) 285 286 // WHEN the view is shown 287 iconManager.onViewShown(true) 288 runCurrent() 289 290 // THEN full image is set 291 verify(mockConsumer).setImageDrawable(drawableCaptor.capture()) 292 assertIsFullImage(drawableCaptor.value) 293 assertSize(drawableCaptor.value) 294 } 295 296 @Test onViewHidden_fullImageShowing_placeHolderSetnull297 fun onViewHidden_fullImageShowing_placeHolderSet() = 298 testScope.runTest { 299 // GIVEN full image is showing and the view is shown 300 iconManager.updateIcon(mockConsumer, supportedIcon).run() 301 iconManager.onViewShown(true) 302 runCurrent() 303 clearInvocations(mockConsumer) 304 305 // WHEN the view goes off the screen 306 iconManager.onViewShown(false) 307 // AND we wait a bit 308 advanceTimeBy(FREE_IMAGE_DELAY_MS) 309 runCurrent() 310 311 // THEN placeholder is set 312 verify(mockConsumer).setImageDrawable(drawableCaptor.capture()) 313 assertIsPlaceHolder(drawableCaptor.value) 314 assertSize(drawableCaptor.value) 315 } 316 317 @Test onViewShownToggled_viewShown_nothingHappensnull318 fun onViewShownToggled_viewShown_nothingHappens() = 319 testScope.runTest { 320 // GIVEN full image is showing and the view is shown 321 iconManager.updateIcon(mockConsumer, supportedIcon).run() 322 iconManager.onViewShown(true) 323 runCurrent() 324 clearInvocations(mockConsumer) 325 326 // WHEN the onViewShown is toggled 327 iconManager.onViewShown(false) 328 runCurrent() 329 iconManager.onViewShown(true) 330 // AND we wait a bit 331 advanceTimeBy(FREE_IMAGE_DELAY_MS) 332 runCurrent() 333 334 // THEN nothing happens 335 verifyZeroInteractions(mockConsumer) 336 } 337 338 @Test onViewShown_unsupportedIconLoaded_nothingHappensnull339 fun onViewShown_unsupportedIconLoaded_nothingHappens() = 340 testScope.runTest { 341 // GIVEN full image is showing for an unsupported icon 342 iconManager.updateIcon(mockConsumer, unsupportedIcon).run() 343 clearInvocations(mockConsumer) 344 345 // WHEN the view is shown 346 iconManager.onViewShown(true) 347 runCurrent() 348 349 // THEN nothing happens 350 verifyZeroInteractions(mockConsumer) 351 } 352 353 @Test onViewShown_nullSetForIcon_nothingHappensnull354 fun onViewShown_nullSetForIcon_nothingHappens() = 355 testScope.runTest { 356 // GIVEN null is set for the icon 357 iconManager.updateIcon(mockConsumer, null).run() 358 clearInvocations(mockConsumer) 359 360 // WHEN the view is shown 361 iconManager.onViewShown(true) 362 runCurrent() 363 364 // THEN nothing happens 365 verifyZeroInteractions(mockConsumer) 366 } 367 368 @Test onViewHidden_unsupportedIconLoadedAndViewIsShown_nothingHappensnull369 fun onViewHidden_unsupportedIconLoadedAndViewIsShown_nothingHappens() = 370 testScope.runTest { 371 // GIVEN full image is showing for an unsupported icon 372 iconManager.updateIcon(mockConsumer, unsupportedIcon).run() 373 // AND the view is shown 374 iconManager.onViewShown(true) 375 runCurrent() 376 clearInvocations(mockConsumer) 377 378 // WHEN the view goes off the screen 379 iconManager.onViewShown(false) 380 // AND we wait a bit 381 advanceTimeBy(FREE_IMAGE_DELAY_MS) 382 runCurrent() 383 384 // THEN nothing happens 385 verifyZeroInteractions(mockConsumer) 386 } 387 388 @Test onViewHidden_placeholderShowing_nothingHappensnull389 fun onViewHidden_placeholderShowing_nothingHappens() = 390 testScope.runTest { 391 // GIVEN placeholder image is showing 392 iconManager.updateIcon(mockConsumer, supportedIcon).run() 393 clearInvocations(mockConsumer) 394 395 // WHEN the view is hidden 396 iconManager.onViewShown(false) 397 // AND we wait a bit 398 advanceTimeBy(FREE_IMAGE_DELAY_MS) 399 runCurrent() 400 401 // THEN nothing happens 402 verifyZeroInteractions(mockConsumer) 403 } 404 405 @Test onViewShown_alreadyShowing_nothingHappensnull406 fun onViewShown_alreadyShowing_nothingHappens() = 407 testScope.runTest { 408 // GIVEN full image is showing and the view is shown 409 iconManager.updateIcon(mockConsumer, supportedIcon).run() 410 iconManager.onViewShown(true) 411 runCurrent() 412 clearInvocations(mockConsumer) 413 414 // WHEN view shown called again 415 iconManager.onViewShown(true) 416 runCurrent() 417 418 // THEN nothing happens 419 verifyZeroInteractions(mockConsumer) 420 } 421 422 @Test onViewHidden_alreadyHidden_nothingHappensnull423 fun onViewHidden_alreadyHidden_nothingHappens() = 424 testScope.runTest { 425 // GIVEN placeholder image is showing and the view is hidden 426 iconManager.updateIcon(mockConsumer, supportedIcon).run() 427 iconManager.onViewShown(false) 428 advanceTimeBy(FREE_IMAGE_DELAY_MS) 429 runCurrent() 430 clearInvocations(mockConsumer) 431 432 // WHEN the view is hidden again 433 iconManager.onViewShown(false) 434 // AND we wait a bit 435 advanceTimeBy(FREE_IMAGE_DELAY_MS) 436 runCurrent() 437 438 // THEN nothing happens 439 verifyZeroInteractions(mockConsumer) 440 } 441 442 @Test cancelJobs_freeImageJobRunning_jobCancellednull443 fun cancelJobs_freeImageJobRunning_jobCancelled() = 444 testScope.runTest { 445 // GIVEN full image is showing 446 iconManager.updateIcon(mockConsumer, supportedIcon).run() 447 iconManager.onViewShown(true) 448 runCurrent() 449 clearInvocations(mockConsumer) 450 // AND the view has just gone off the screen 451 iconManager.onViewShown(false) 452 453 // WHEN cancelJobs is called 454 iconManager.cancelJobs() 455 // AND we wait a bit 456 advanceTimeBy(FREE_IMAGE_DELAY_MS) 457 runCurrent() 458 459 // THEN no more updates are happening 460 verifyZeroInteractions(mockConsumer) 461 } 462 overrideMaxImageSizesnull463 private fun overrideMaxImageSizes() { 464 testableResources.addOverride( 465 com.android.internal.R.dimen.notification_big_picture_max_width, 466 MAX_IMAGE_SIZE 467 ) 468 testableResources.addOverride( 469 com.android.internal.R.dimen.notification_big_picture_max_height, 470 MAX_IMAGE_SIZE 471 ) 472 } 473 assertIsPlaceHoldernull474 private fun assertIsPlaceHolder(drawable: Drawable) { 475 assertThat(drawable).isInstanceOf(PlaceHolderDrawable::class.java) 476 } 477 assertIsFullImagenull478 private fun assertIsFullImage(drawable: Drawable) { 479 assertThat(drawable).isInstanceOf(BitmapDrawable::class.java) 480 } 481 assertSizenull482 private fun assertSize( 483 drawable: Drawable, 484 expectedWidth: Int = expectedDrawable.intrinsicWidth, 485 expectedHeight: Int = expectedDrawable.intrinsicHeight 486 ) { 487 assertThat(drawable.intrinsicWidth).isEqualTo(expectedWidth) 488 assertThat(drawable.intrinsicHeight).isEqualTo(expectedHeight) 489 } 490 } 491