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