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