1 /* 2 * Copyright (C) 2016 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.systemui.statusbar; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static junit.framework.Assert.assertEquals; 22 import static junit.framework.Assert.assertFalse; 23 import static junit.framework.Assert.assertNull; 24 import static junit.framework.Assert.assertTrue; 25 26 import static org.mockito.ArgumentMatchers.any; 27 import static org.mockito.ArgumentMatchers.anyInt; 28 import static org.mockito.ArgumentMatchers.argThat; 29 import static org.mockito.ArgumentMatchers.eq; 30 import static org.mockito.Mockito.doReturn; 31 import static org.mockito.Mockito.mock; 32 import static org.mockito.Mockito.spy; 33 import static org.mockito.Mockito.when; 34 35 import android.app.Notification; 36 import android.content.Context; 37 import android.content.ContextWrapper; 38 import android.content.pm.ApplicationInfo; 39 import android.content.pm.PackageManager; 40 import android.content.res.Resources; 41 import android.graphics.Bitmap; 42 import android.graphics.Color; 43 import android.graphics.drawable.BitmapDrawable; 44 import android.graphics.drawable.Icon; 45 import android.os.Bundle; 46 import android.os.UserHandle; 47 import android.service.notification.StatusBarNotification; 48 import android.view.ViewGroup; 49 50 import androidx.test.filters.SmallTest; 51 import androidx.test.runner.AndroidJUnit4; 52 53 import com.android.internal.statusbar.StatusBarIcon; 54 import com.android.internal.util.ContrastColorUtil; 55 import com.android.systemui.R; 56 import com.android.systemui.SysuiTestCase; 57 58 import org.junit.Before; 59 import org.junit.Rule; 60 import org.junit.Test; 61 import org.junit.rules.ExpectedException; 62 import org.junit.runner.RunWith; 63 import org.mockito.ArgumentMatcher; 64 65 @SmallTest 66 @RunWith(AndroidJUnit4.class) 67 public class StatusBarIconViewTest extends SysuiTestCase { 68 69 private static final int TEST_STATUS_BAR_HEIGHT = 150; 70 71 @Rule 72 public ExpectedException mThrown = ExpectedException.none(); 73 74 private StatusBarIconView mIconView; 75 private StatusBarIcon mStatusBarIcon = mock(StatusBarIcon.class); 76 77 private PackageManager mPackageManagerSpy; 78 private Context mContext; 79 private Resources mMockResources; 80 81 @Before setUp()82 public void setUp() throws Exception { 83 // Set up context such that asking for "mockPackage" resources returns mMockResources. 84 mMockResources = mock(Resources.class); 85 mPackageManagerSpy = spy(getContext().getPackageManager()); 86 doReturn(mMockResources).when(mPackageManagerSpy) 87 .getResourcesForApplication(eq("mockPackage")); 88 doReturn(mMockResources).when(mPackageManagerSpy).getResourcesForApplication(argThat( 89 (ArgumentMatcher<ApplicationInfo>) o -> "mockPackage".equals(o.packageName))); 90 mContext = new ContextWrapper(getContext()) { 91 @Override 92 public PackageManager getPackageManager() { 93 return mPackageManagerSpy; 94 } 95 }; 96 97 mIconView = new StatusBarIconView(mContext, "test_slot", null); 98 mStatusBarIcon = new StatusBarIcon(UserHandle.ALL, "mockPackage", 99 Icon.createWithResource(mContext, R.drawable.ic_android), 0, 0, ""); 100 } 101 102 @Test testSetClearsGrayscale()103 public void testSetClearsGrayscale() { 104 mIconView.setTag(R.id.icon_is_grayscale, true); 105 mIconView.set(mStatusBarIcon); 106 assertNull(mIconView.getTag(R.id.icon_is_grayscale)); 107 } 108 109 @Test testSettingOomingIconDoesNotThrowOom()110 public void testSettingOomingIconDoesNotThrowOom() { 111 when(mMockResources.getDrawable(anyInt(), any())).thenThrow(new OutOfMemoryError("mocked")); 112 mStatusBarIcon.icon = Icon.createWithResource("mockPackage", R.drawable.ic_android); 113 114 assertFalse(mIconView.set(mStatusBarIcon)); 115 } 116 117 @Test testGetContrastedStaticDrawableColor()118 public void testGetContrastedStaticDrawableColor() { 119 mIconView.setStaticDrawableColor(Color.DKGRAY); 120 int color = mIconView.getContrastedStaticDrawableColor(Color.WHITE); 121 assertEquals("Color should not change when we have enough contrast", 122 Color.DKGRAY, color); 123 124 mIconView.setStaticDrawableColor(Color.WHITE); 125 color = mIconView.getContrastedStaticDrawableColor(Color.WHITE); 126 assertTrue("Similar colors should be shifted to satisfy contrast", 127 ContrastColorUtil.satisfiesTextContrast(Color.WHITE, color)); 128 129 mIconView.setStaticDrawableColor(Color.GREEN); 130 color = mIconView.getContrastedStaticDrawableColor(0xcc000000); 131 assertEquals("Transparent backgrounds should fallback to drawable color", 132 color, mIconView.getStaticDrawableColor()); 133 } 134 135 @Test testGiantImageNotAllowed()136 public void testGiantImageNotAllowed() { 137 Bitmap largeBitmap = Bitmap.createBitmap(6000, 6000, Bitmap.Config.ARGB_8888); 138 Icon icon = Icon.createWithBitmap(largeBitmap); 139 StatusBarIcon largeIcon = new StatusBarIcon(UserHandle.ALL, "mockPackage", 140 icon, 0, 0, ""); 141 assertTrue(mIconView.set(largeIcon)); 142 143 // The view should downscale the bitmap. 144 BitmapDrawable drawable = (BitmapDrawable) mIconView.getDrawable(); 145 assertThat(drawable.getBitmap().getWidth()).isLessThan(1000); 146 assertThat(drawable.getBitmap().getHeight()).isLessThan(1000); 147 } 148 149 @Test testNullNotifInfo()150 public void testNullNotifInfo() { 151 Bitmap bitmap = Bitmap.createBitmap(60, 60, Bitmap.Config.ARGB_8888); 152 Icon icon = Icon.createWithBitmap(bitmap); 153 StatusBarIcon largeIcon = new StatusBarIcon(UserHandle.ALL, "mockPackage", 154 icon, 0, 0, ""); 155 mIconView.setNotification(mock(StatusBarNotification.class)); 156 mIconView.getIcon(largeIcon); 157 // no crash? good 158 159 mIconView.setNotification(null); 160 mIconView.getIcon(largeIcon); 161 // no crash? good 162 } 163 164 @Test testNullIcon()165 public void testNullIcon() { 166 Icon mockIcon = mock(Icon.class); 167 when(mockIcon.loadDrawableAsUser(any(), anyInt())).thenReturn(null); 168 mStatusBarIcon.icon = mockIcon; 169 mIconView.set(mStatusBarIcon); 170 171 Bitmap bitmap = Bitmap.createBitmap(60, 60, Bitmap.Config.ARGB_8888); 172 Icon icon = Icon.createWithBitmap(bitmap); 173 StatusBarIcon largeIcon = new StatusBarIcon(UserHandle.ALL, "mockPackage", 174 icon, 0, 0, ""); 175 mIconView.getIcon(largeIcon); 176 // No crash? good 177 } 178 179 @Test testContentDescForNotification_invalidAi_noCrash()180 public void testContentDescForNotification_invalidAi_noCrash() { 181 Notification n = new Notification.Builder(mContext, "test") 182 .setSmallIcon(0) 183 .build(); 184 // should be ApplicationInfo 185 n.extras.putParcelable(Notification.EXTRA_BUILDER_APPLICATION_INFO, new Bundle()); 186 StatusBarIconView.contentDescForNotification(mContext, n); 187 188 // no crash, good 189 } 190 191 @Test testUpdateIconScale_constrainedDrawableSizeLessThanDpIconSize()192 public void testUpdateIconScale_constrainedDrawableSizeLessThanDpIconSize() { 193 int dpIconSize = 60; 194 int dpDrawingSize = 30; 195 // the icon view layout size would be 60x150 196 // (the height is always 150 due to TEST_STATUS_BAR_HEIGHT) 197 setUpIconView(dpIconSize, dpDrawingSize, dpIconSize); 198 mIconView.setNotification(mock(StatusBarNotification.class)); 199 // the raw drawable size is 50x50. When put the drawable into iconView whose 200 // layout size is 60x150, the drawable size would not be constrained and thus keep 50x50 201 setIconDrawableWithSize(/* width= */ 50, /* height= */ 50); 202 mIconView.maybeUpdateIconScaleDimens(); 203 204 // WHEN both the constrained drawable width/height are less than dpIconSize, 205 // THEN the icon is scaled down from dpIconSize to fit the dpDrawingSize 206 float scaleToFitDrawingSize = (float) dpDrawingSize / dpIconSize; 207 assertEquals(scaleToFitDrawingSize, mIconView.getIconScale(), 0.01f); 208 } 209 210 @Test testUpdateIconScale_constrainedDrawableHeightLargerThanDpIconSize()211 public void testUpdateIconScale_constrainedDrawableHeightLargerThanDpIconSize() { 212 int dpIconSize = 60; 213 int dpDrawingSize = 30; 214 // the icon view layout size would be 60x150 215 // (the height is always 150 due to TEST_STATUS_BAR_HEIGHT) 216 setUpIconView(dpIconSize, dpDrawingSize, dpIconSize); 217 mIconView.setNotification(mock(StatusBarNotification.class)); 218 // the raw drawable size is 50x100. When put the drawable into iconView whose 219 // layout size is 60x150, the drawable size would not be constrained and thus keep 50x100 220 setIconDrawableWithSize(/* width= */ 50, /* height= */ 100); 221 mIconView.maybeUpdateIconScaleDimens(); 222 223 // WHEN constrained drawable larger side length 100 >= dpIconSize 224 // THEN the icon is scaled down from larger side length 100 to ensure both side 225 // length fit in dpDrawingSize. 226 float scaleToFitDrawingSize = (float) dpDrawingSize / 100; 227 assertEquals(scaleToFitDrawingSize, mIconView.getIconScale(), 0.01f); 228 } 229 230 @Test testUpdateIconScale_constrainedDrawableWidthLargerThanDpIconSize()231 public void testUpdateIconScale_constrainedDrawableWidthLargerThanDpIconSize() { 232 int dpIconSize = 60; 233 int dpDrawingSize = 30; 234 // the icon view layout size would be 60x150 235 // (the height is always 150 due to TEST_STATUS_BAR_HEIGHT) 236 setUpIconView(dpIconSize, dpDrawingSize, dpIconSize); 237 mIconView.setNotification(mock(StatusBarNotification.class)); 238 // the raw drawable size is 100x50. When put the drawable into iconView whose 239 // layout size is 60x150, the drawable size would be constrained to 60x30 240 setIconDrawableWithSize(/* width= */ 100, /* height= */ 50); 241 mIconView.maybeUpdateIconScaleDimens(); 242 243 // WHEN constrained drawable larger side length 60 >= dpIconSize 244 // THEN the icon is scaled down from larger side length 60 to ensure both side 245 // length fit in dpDrawingSize. 246 float scaleToFitDrawingSize = (float) dpDrawingSize / 60; 247 assertEquals(scaleToFitDrawingSize, mIconView.getIconScale(), 0.01f); 248 } 249 250 @Test testUpdateIconScale_smallerFontAndRawDrawableSizeLessThanDpIconSize()251 public void testUpdateIconScale_smallerFontAndRawDrawableSizeLessThanDpIconSize() { 252 int dpIconSize = 60; 253 int dpDrawingSize = 30; 254 // smaller font scaling causes the spIconSize < dpIconSize 255 int spIconSize = 40; 256 // the icon view layout size would be 40x150 257 // (the height is always 150 due to TEST_STATUS_BAR_HEIGHT) 258 setUpIconView(dpIconSize, dpDrawingSize, spIconSize); 259 mIconView.setNotification(mock(StatusBarNotification.class)); 260 // the raw drawable size is 50x50. When put the drawable into iconView whose 261 // layout size is 40x150, the drawable size would be constrained to 40x40 262 setIconDrawableWithSize(/* width= */ 50, /* height= */ 50); 263 mIconView.maybeUpdateIconScaleDimens(); 264 265 // WHEN both the raw/constrained drawable width/height are less than dpIconSize, 266 // THEN the icon is scaled up from constrained drawable size to the raw drawable size 267 float scaleToBackRawDrawableSize = (float) 50 / 40; 268 // THEN the icon is scaled down from dpIconSize to fit the dpDrawingSize 269 float scaleToFitDrawingSize = (float) dpDrawingSize / dpIconSize; 270 // THEN the scaled icon should be scaled down further to fit spIconSize 271 float scaleToFitSpIconSize = (float) spIconSize / dpIconSize; 272 assertEquals(scaleToBackRawDrawableSize * scaleToFitDrawingSize * scaleToFitSpIconSize, 273 mIconView.getIconScale(), 0.01f); 274 } 275 276 @Test testUpdateIconScale_smallerFontAndConstrainedDrawableSizeLessThanDpIconSize()277 public void testUpdateIconScale_smallerFontAndConstrainedDrawableSizeLessThanDpIconSize() { 278 int dpIconSize = 60; 279 int dpDrawingSize = 30; 280 // smaller font scaling causes the spIconSize < dpIconSize 281 int spIconSize = 40; 282 // the icon view layout size would be 40x150 283 // (the height is always 150 due to TEST_STATUS_BAR_HEIGHT) 284 setUpIconView(dpIconSize, dpDrawingSize, spIconSize); 285 mIconView.setNotification(mock(StatusBarNotification.class)); 286 // the raw drawable size is 70x70. When put the drawable into iconView whose 287 // layout size is 40x150, the drawable size would be constrained to 40x40 288 setIconDrawableWithSize(/* width= */ 70, /* height= */ 70); 289 mIconView.maybeUpdateIconScaleDimens(); 290 291 // WHEN the raw drawable width/height are larger than dpIconSize, 292 // but the constrained drawable width/height are less than dpIconSize, 293 // THEN the icon is scaled up from constrained drawable size to fit dpIconSize 294 float scaleToFitDpIconSize = (float) dpIconSize / 40; 295 // THEN the icon is scaled down from dpIconSize to fit the dpDrawingSize 296 float scaleToFitDrawingSize = (float) dpDrawingSize / dpIconSize; 297 // THEN the scaled icon should be scaled down further to fit spIconSize 298 float scaleToFitSpIconSize = (float) spIconSize / dpIconSize; 299 assertEquals(scaleToFitDpIconSize * scaleToFitDrawingSize * scaleToFitSpIconSize, 300 mIconView.getIconScale(), 0.01f); 301 } 302 303 @Test testUpdateIconScale_smallerFontAndConstrainedDrawableHeightLargerThanDpIconSize()304 public void testUpdateIconScale_smallerFontAndConstrainedDrawableHeightLargerThanDpIconSize() { 305 int dpIconSize = 60; 306 int dpDrawingSize = 30; 307 // smaller font scaling causes the spIconSize < dpIconSize 308 int spIconSize = 40; 309 // the icon view layout size would be 40x150 310 // (the height is always 150 due to TEST_STATUS_BAR_HEIGHT) 311 setUpIconView(dpIconSize, dpDrawingSize, spIconSize); 312 mIconView.setNotification(mock(StatusBarNotification.class)); 313 // the raw drawable size is 50x100. When put the drawable into iconView whose 314 // layout size is 40x150, the drawable size would be constrained to 40x80 315 setIconDrawableWithSize(/* width= */ 50, /* height= */ 100); 316 mIconView.maybeUpdateIconScaleDimens(); 317 318 // WHEN constrained drawable larger side length 80 >= dpIconSize 319 // THEN the icon is scaled down from larger side length 80 to ensure both side 320 // length fit in dpDrawingSize. 321 float scaleToFitDrawingSize = (float) dpDrawingSize / 80; 322 // THEN the scaled icon should be scaled down further to fit spIconSize 323 float scaleToFitSpIconSize = (float) spIconSize / dpIconSize; 324 assertEquals(scaleToFitDrawingSize * scaleToFitSpIconSize, mIconView.getIconScale(), 0.01f); 325 } 326 327 @Test testUpdateIconScale_largerFontAndConstrainedDrawableSizeLessThanDpIconSize()328 public void testUpdateIconScale_largerFontAndConstrainedDrawableSizeLessThanDpIconSize() { 329 int dpIconSize = 60; 330 int dpDrawingSize = 30; 331 // larger font scaling causes the spIconSize > dpIconSize 332 int spIconSize = 80; 333 // the icon view layout size would be 80x150 334 // (the height is always 150 due to TEST_STATUS_BAR_HEIGHT) 335 setUpIconView(dpIconSize, dpDrawingSize, spIconSize); 336 mIconView.setNotification(mock(StatusBarNotification.class)); 337 // the raw drawable size is 50x50. When put the drawable into iconView whose 338 // layout size is 80x150, the drawable size would not be constrained and thus keep 50x50 339 setIconDrawableWithSize(/* width= */ 50, /* height= */ 50); 340 mIconView.maybeUpdateIconScaleDimens(); 341 342 // WHEN both the constrained drawable width/height are less than dpIconSize, 343 // THEN the icon is scaled down from dpIconSize to fit the dpDrawingSize 344 float scaleToFitDrawingSize = (float) dpDrawingSize / dpIconSize; 345 // THEN the scaled icon should be scaled up to fit spIconSize 346 float scaleToFitSpIconSize = (float) spIconSize / dpIconSize; 347 assertEquals(scaleToFitDrawingSize * scaleToFitSpIconSize, mIconView.getIconScale(), 0.01f); 348 } 349 350 @Test testUpdateIconScale_largerFontAndConstrainedDrawableHeightLargerThanDpIconSize()351 public void testUpdateIconScale_largerFontAndConstrainedDrawableHeightLargerThanDpIconSize() { 352 int dpIconSize = 60; 353 int dpDrawingSize = 30; 354 // larger font scaling causes the spIconSize > dpIconSize 355 int spIconSize = 80; 356 // the icon view layout size would be 80x150 357 // (the height is always 150 due to TEST_STATUS_BAR_HEIGHT) 358 setUpIconView(dpIconSize, dpDrawingSize, spIconSize); 359 mIconView.setNotification(mock(StatusBarNotification.class)); 360 // the raw drawable size is 50x100. When put the drawable into iconView whose 361 // layout size is 80x150, the drawable size would not be constrained and thus keep 50x100 362 setIconDrawableWithSize(/* width= */ 50, /* height= */ 100); 363 mIconView.maybeUpdateIconScaleDimens(); 364 365 // WHEN constrained drawable larger side length 100 >= dpIconSize 366 // THEN the icon is scaled down from larger side length 100 to ensure both side 367 // length fit in dpDrawingSize. 368 float scaleToFitDrawingSize = (float) dpDrawingSize / 100; 369 // THEN the scaled icon should be scaled up to fit spIconSize 370 float scaleToFitSpIconSize = (float) spIconSize / dpIconSize; 371 assertEquals(scaleToFitDrawingSize * scaleToFitSpIconSize, mIconView.getIconScale(), 0.01f); 372 } 373 374 @Test testUpdateIconScale_largerFontAndConstrainedDrawableWidthLargerThanDpIconSize()375 public void testUpdateIconScale_largerFontAndConstrainedDrawableWidthLargerThanDpIconSize() { 376 int dpIconSize = 60; 377 int dpDrawingSize = 30; 378 // larger font scaling causes the spIconSize > dpIconSize 379 int spIconSize = 80; 380 // the icon view layout size would be 80x150 381 // (the height is always 150 due to TEST_STATUS_BAR_HEIGHT) 382 setUpIconView(dpIconSize, dpDrawingSize, spIconSize); 383 mIconView.setNotification(mock(StatusBarNotification.class)); 384 // the raw drawable size is 100x50. When put the drawable into iconView whose 385 // layout size is 80x150, the drawable size would not be constrained and thus keep 80x40 386 setIconDrawableWithSize(/* width= */ 100, /* height= */ 50); 387 mIconView.maybeUpdateIconScaleDimens(); 388 389 // WHEN constrained drawable larger side length 80 >= dpIconSize 390 // THEN the icon is scaled down from larger side length 80 to ensure both side 391 // length fit in dpDrawingSize. 392 float scaleToFitDrawingSize = (float) dpDrawingSize / 80; 393 // THEN the scaled icon should be scaled up to fit spIconSize 394 float scaleToFitSpIconSize = (float) spIconSize / dpIconSize; 395 assertEquals(scaleToFitDrawingSize * scaleToFitSpIconSize, 396 mIconView.getIconScale(), 0.01f); 397 } 398 399 /** 400 * Setup iconView dimens for testing. The result icon view layout width would 401 * be spIconSize and height would be 150. 402 * 403 * @param dpIconSize corresponding to status_bar_icon_size 404 * @param dpDrawingSize corresponding to status_bar_icon_drawing_size 405 * @param spIconSize corresponding to status_bar_icon_size_sp under different font scaling 406 */ setUpIconView(int dpIconSize, int dpDrawingSize, int spIconSize)407 private void setUpIconView(int dpIconSize, int dpDrawingSize, int spIconSize) { 408 mIconView.setIncreasedSize(false); 409 mIconView.mOriginalStatusBarIconSize = dpIconSize; 410 mIconView.mStatusBarIconDrawingSize = dpDrawingSize; 411 412 mIconView.mNewStatusBarIconSize = spIconSize; 413 mIconView.mScaleToFitNewIconSize = (float) spIconSize / dpIconSize; 414 415 // the layout width would be spIconSize + 2 * iconPadding, and we assume iconPadding 416 // is 0 here. 417 ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(spIconSize, TEST_STATUS_BAR_HEIGHT); 418 mIconView.setLayoutParams(lp); 419 } 420 setIconDrawableWithSize(int width, int height)421 private void setIconDrawableWithSize(int width, int height) { 422 Bitmap bitmap = Bitmap.createBitmap( 423 width, height, Bitmap.Config.ARGB_8888); 424 Icon icon = Icon.createWithBitmap(bitmap); 425 mStatusBarIcon = new StatusBarIcon(UserHandle.ALL, "mockPackage", 426 icon, 0, 0, ""); 427 // Since we only want to verify icon scale logic here, we directly use 428 // {@link StatusBarIconView#setImageDrawable(Drawable)} to set the image drawable 429 // to iconView instead of call {@link StatusBarIconView#set(StatusBarIcon)}. It's to prevent 430 // the icon drawable size being scaled down when internally calling 431 // {@link StatusBarIconView#getIcon(Context,Context,StatusBarIcon)}. 432 mIconView.setImageDrawable(icon.loadDrawable(mContext)); 433 } 434 } 435