1 /*
2  * Copyright (C) 2019 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.car.notification;
18 
19 import android.annotation.ColorInt;
20 import android.app.ActivityManager;
21 import android.app.Notification;
22 import android.content.Context;
23 import android.content.pm.PackageInfo;
24 import android.content.pm.PackageManager;
25 import android.content.res.TypedArray;
26 import android.graphics.Color;
27 import android.os.Bundle;
28 import android.os.Process;
29 import android.os.UserHandle;
30 import android.os.UserManager;
31 import android.service.notification.StatusBarNotification;
32 import android.util.Log;
33 
34 import com.android.internal.graphics.ColorUtils;
35 
36 public class NotificationUtils {
37     private static final String TAG = "NotificationUtils";
38     private static final int MAX_FIND_COLOR_STEPS = 15;
39     private static final double MIN_COLOR_CONTRAST = 0.00001;
40     private static final double MIN_CONTRAST_RATIO = 4.5;
41     private static final double MIN_LIGHTNESS = 0;
42     private static final float MAX_LIGHTNESS = 1;
43     private static final float LIGHT_COLOR_LUMINANCE_THRESHOLD = 0.5f;
44 
NotificationUtils()45     private NotificationUtils() {
46     }
47 
48     /**
49      * Returns the color assigned to the given attribute.
50      */
getAttrColor(Context context, int attr)51     public static int getAttrColor(Context context, int attr) {
52         TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
53         int colorAccent = ta.getColor(0, 0);
54         ta.recycle();
55         return colorAccent;
56     }
57 
58     /**
59      * Validates if the notification posted by the application meets at least one of the below
60      * conditions.
61      *
62      * <ul>
63      * <li>application is signed with platform key.
64      * <li>application is a system and privileged app.
65      * </ul>
66      */
isSystemPrivilegedOrPlatformKey(Context context, AlertEntry alertEntry)67     public static boolean isSystemPrivilegedOrPlatformKey(Context context, AlertEntry alertEntry) {
68         return isSystemPrivilegedOrPlatformKeyInner(context, alertEntry,
69                 /* checkForPrivilegedApp= */ true);
70     }
71 
72     /**
73      * Validates if the notification posted by the application meets at least one of the below
74      * conditions.
75      *
76      * <ul>
77      * <li>application is signed with platform key.
78      * <li>application is a system app.
79      * </ul>
80      */
isSystemOrPlatformKey(Context context, AlertEntry alertEntry)81     public static boolean isSystemOrPlatformKey(Context context, AlertEntry alertEntry) {
82         return isSystemPrivilegedOrPlatformKeyInner(context, alertEntry,
83                 /* checkForPrivilegedApp= */ false);
84     }
85 
86     /**
87      * Validates if the notification posted by the application is a system application.
88      */
isSystemApp(Context context, StatusBarNotification statusBarNotification)89     public static boolean isSystemApp(Context context,
90             StatusBarNotification statusBarNotification) {
91         PackageInfo packageInfo = getPackageInfo(context, statusBarNotification);
92         if (packageInfo == null) return false;
93 
94         return packageInfo.applicationInfo.isSystemApp();
95     }
96 
97     /**
98      * Validates if the notification posted by the application is signed with platform key.
99      */
isSignedWithPlatformKey(Context context, StatusBarNotification statusBarNotification)100     public static boolean isSignedWithPlatformKey(Context context,
101             StatusBarNotification statusBarNotification) {
102         PackageInfo packageInfo = getPackageInfo(context, statusBarNotification);
103         if (packageInfo == null) return false;
104 
105         return packageInfo.applicationInfo.isSignedWithPlatformKey();
106     }
107 
108     /**
109      * Choose a correct notification layout for this heads-up notification.
110      * Note that the layout chosen can be different for the same notification
111      * in the notification center.
112      */
getNotificationViewType(AlertEntry alertEntry)113     public static CarNotificationTypeItem getNotificationViewType(AlertEntry alertEntry) {
114         String category = alertEntry.getNotification().category;
115         if (category != null) {
116             switch (category) {
117                 case Notification.CATEGORY_CAR_EMERGENCY:
118                     return CarNotificationTypeItem.EMERGENCY;
119                 case Notification.CATEGORY_NAVIGATION:
120                     return CarNotificationTypeItem.NAVIGATION;
121                 case Notification.CATEGORY_CALL:
122                     return CarNotificationTypeItem.CALL;
123                 case Notification.CATEGORY_CAR_WARNING:
124                     return CarNotificationTypeItem.WARNING;
125                 case Notification.CATEGORY_CAR_INFORMATION:
126                     return CarNotificationTypeItem.INFORMATION;
127                 case Notification.CATEGORY_MESSAGE:
128                     return CarNotificationTypeItem.MESSAGE;
129                 default:
130                     break;
131             }
132         }
133         Bundle extras = alertEntry.getNotification().extras;
134         if (extras.containsKey(Notification.EXTRA_TITLE_BIG)
135                 && extras.containsKey(Notification.EXTRA_SUMMARY_TEXT)) {
136             return CarNotificationTypeItem.INBOX;
137         }
138         // progress, media, big text, big picture, and basic templates
139         return CarNotificationTypeItem.BASIC;
140     }
141 
142     /**
143      * Resolves a Notification's color such that it has enough contrast to be used as the
144      * color for the Notification's action and header text.
145      *
146      * @param backgroundColor the background color to ensure the contrast against.
147      * @return a color of the same hue as {@code notificationColor} with enough contrast against
148      * the backgrounds.
149      */
resolveContrastColor( @olorInt int notificationColor, @ColorInt int backgroundColor)150     public static int resolveContrastColor(
151             @ColorInt int notificationColor, @ColorInt int backgroundColor) {
152         return getContrastedForegroundColor(notificationColor, backgroundColor, MIN_CONTRAST_RATIO);
153     }
154 
155     /**
156      * Returns true if a color is considered a light color.
157      */
isColorLight(int backgroundColor)158     public static boolean isColorLight(int backgroundColor) {
159         return Color.luminance(backgroundColor) > LIGHT_COLOR_LUMINANCE_THRESHOLD;
160     }
161 
162     /**
163      * Returns the current user id for this instance of the notification app/library.
164      */
getCurrentUser(Context context)165     public static int getCurrentUser(Context context) {
166         UserManager userManager = context.getSystemService(UserManager.class);
167         UserHandle processUser = Process.myUserHandle();
168         boolean isSecondaryUserNotifications =
169                 userManager.isVisibleBackgroundUsersSupported()
170                         && !processUser.isSystem()
171                         && processUser.getIdentifier() != ActivityManager.getCurrentUser();
172         return isSecondaryUserNotifications ? processUser.getIdentifier()
173                 : ActivityManager.getCurrentUser();
174     }
175 
isSystemPrivilegedOrPlatformKeyInner(Context context, AlertEntry alertEntry, boolean checkForPrivilegedApp)176     private static boolean isSystemPrivilegedOrPlatformKeyInner(Context context,
177             AlertEntry alertEntry, boolean checkForPrivilegedApp) {
178         PackageInfo packageInfo = getPackageInfo(context, alertEntry.getStatusBarNotification());
179         if (packageInfo == null) return false;
180 
181         // Only include the privilegedApp check if the caller wants this check.
182         boolean isPrivilegedApp =
183                 (!checkForPrivilegedApp) || packageInfo.applicationInfo.isPrivilegedApp();
184 
185         return (packageInfo.applicationInfo.isSignedWithPlatformKey() ||
186                 (packageInfo.applicationInfo.isSystemApp()
187                         && isPrivilegedApp));
188     }
189 
getPackageInfo(Context context, StatusBarNotification statusBarNotification)190     private static PackageInfo getPackageInfo(Context context,
191             StatusBarNotification statusBarNotification) {
192         PackageManager packageManager = context.getPackageManager();
193         PackageInfo packageInfo = null;
194         try {
195             packageInfo = packageManager.getPackageInfoAsUser(
196                     statusBarNotification.getPackageName(), /* flags= */ 0,
197                     ActivityManager.getCurrentUser());
198         } catch (PackageManager.NameNotFoundException ex) {
199             Log.e(TAG, "package not found: " + statusBarNotification.getPackageName());
200         }
201         return packageInfo;
202     }
203 
204     /**
205      * Finds a suitable color such that there's enough contrast.
206      *
207      * @param foregroundColor  the color to start searching from.
208      * @param backgroundColor  the color to ensure contrast against. Assumed to be lighter than
209      *                         {@param foregroundColor}
210      * @param minContrastRatio the minimum contrast ratio required.
211      * @return a color with the same hue as {@param foregroundColor}, potentially darkened to
212      * meet the contrast ratio.
213      */
findContrastColorAgainstLightBackground( @olorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio)214     private static int findContrastColorAgainstLightBackground(
215             @ColorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio) {
216         if (ColorUtils.calculateContrast(foregroundColor, backgroundColor) >= minContrastRatio) {
217             return foregroundColor;
218         }
219 
220         double[] lab = new double[3];
221         ColorUtils.colorToLAB(foregroundColor, lab);
222 
223         double low = MIN_LIGHTNESS;
224         double high = lab[0];
225         double a = lab[1];
226         double b = lab[2];
227         for (int i = 0; i < MAX_FIND_COLOR_STEPS && high - low > MIN_COLOR_CONTRAST; i++) {
228             double l = (low + high) / 2;
229             foregroundColor = ColorUtils.LABToColor(l, a, b);
230             if (ColorUtils.calculateContrast(foregroundColor, backgroundColor) > minContrastRatio) {
231                 low = l;
232             } else {
233                 high = l;
234             }
235         }
236         return ColorUtils.LABToColor(low, a, b);
237     }
238 
239     /**
240      * Finds a suitable color such that there's enough contrast.
241      *
242      * @param foregroundColor  the foregroundColor to start searching from.
243      * @param backgroundColor  the foregroundColor to ensure contrast against. Assumed to be
244      *                         darker than {@param foregroundColor}
245      * @param minContrastRatio the minimum contrast ratio required.
246      * @return a foregroundColor with the same hue as {@param foregroundColor}, potentially
247      * lightened to meet the contrast ratio.
248      */
findContrastColorAgainstDarkBackground( @olorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio)249     private static int findContrastColorAgainstDarkBackground(
250             @ColorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio) {
251         if (ColorUtils.calculateContrast(foregroundColor, backgroundColor) >= minContrastRatio) {
252             return foregroundColor;
253         }
254 
255         float[] hsl = new float[3];
256         ColorUtils.colorToHSL(foregroundColor, hsl);
257 
258         float low = hsl[2];
259         float high = MAX_LIGHTNESS;
260         for (int i = 0; i < MAX_FIND_COLOR_STEPS && high - low > MIN_COLOR_CONTRAST; i++) {
261             float l = (low + high) / 2;
262             hsl[2] = l;
263             foregroundColor = ColorUtils.HSLToColor(hsl);
264             if (ColorUtils.calculateContrast(foregroundColor, backgroundColor)
265                     > minContrastRatio) {
266                 high = l;
267             } else {
268                 low = l;
269             }
270         }
271         return foregroundColor;
272     }
273 
274     /**
275      * Finds a foregroundColor with sufficient contrast over backgroundColor that has the same or
276      * darker hue as the original foregroundColor.
277      *
278      * @param foregroundColor  the foregroundColor to start searching from
279      * @param backgroundColor  the foregroundColor to ensure contrast against
280      * @param minContrastRatio the minimum contrast ratio required
281      */
getContrastedForegroundColor( @olorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio)282     private static int getContrastedForegroundColor(
283             @ColorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio) {
284         boolean isBackgroundDarker =
285                 Color.luminance(foregroundColor) > Color.luminance(backgroundColor);
286         return isBackgroundDarker
287                 ? findContrastColorAgainstDarkBackground(
288                 foregroundColor, backgroundColor, minContrastRatio)
289                 : findContrastColorAgainstLightBackground(
290                         foregroundColor, backgroundColor, minContrastRatio);
291     }
292 }
293