• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 true if the current notification process is running for a visible background user.
164      */
isVisibleBackgroundUser(Context context)165     public static boolean isVisibleBackgroundUser(Context context) {
166         UserManager userManager = context.getSystemService(UserManager.class);
167         UserHandle processUser = Process.myUserHandle();
168         return userManager.isVisibleBackgroundUsersSupported()
169                 && !processUser.isSystem()
170                 && processUser.getIdentifier() != ActivityManager.getCurrentUser();
171     }
172 
173     /**
174      * Returns the current user id for this instance of the notification app/library.
175      */
getCurrentUser(Context context)176     public static int getCurrentUser(Context context) {
177         UserHandle processUser = Process.myUserHandle();
178         return isVisibleBackgroundUser(context) ? processUser.getIdentifier()
179                 : ActivityManager.getCurrentUser();
180     }
181 
182     /**
183      * @return {@code true} if notification is a valid progress notification.
184      */
isProgress(Notification notification)185     public static boolean isProgress(Notification notification) {
186         Bundle extras = notification.extras;
187         int progressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX);
188         boolean isIndeterminate = extras.getBoolean(
189                 Notification.EXTRA_PROGRESS_INDETERMINATE);
190         boolean hasValidProgress = isIndeterminate || progressMax != 0;
191         return extras.containsKey(Notification.EXTRA_PROGRESS)
192                 && extras.containsKey(Notification.EXTRA_PROGRESS_MAX)
193                 && hasValidProgress
194                 && !notification.hasCompletedProgress();
195     }
196 
isSystemPrivilegedOrPlatformKeyInner(Context context, AlertEntry alertEntry, boolean checkForPrivilegedApp)197     private static boolean isSystemPrivilegedOrPlatformKeyInner(Context context,
198             AlertEntry alertEntry, boolean checkForPrivilegedApp) {
199         PackageInfo packageInfo = getPackageInfo(context, alertEntry.getStatusBarNotification());
200         if (packageInfo == null) return false;
201 
202         // Only include the privilegedApp check if the caller wants this check.
203         boolean isPrivilegedApp =
204                 (!checkForPrivilegedApp) || packageInfo.applicationInfo.isPrivilegedApp();
205 
206         return (packageInfo.applicationInfo.isSignedWithPlatformKey() ||
207                 (packageInfo.applicationInfo.isSystemApp()
208                         && isPrivilegedApp));
209     }
210 
getPackageInfo(Context context, StatusBarNotification statusBarNotification)211     private static PackageInfo getPackageInfo(Context context,
212             StatusBarNotification statusBarNotification) {
213         PackageManager packageManager = context.getPackageManager();
214         PackageInfo packageInfo = null;
215         try {
216             packageInfo = packageManager.getPackageInfoAsUser(
217                     statusBarNotification.getPackageName(), /* flags= */ 0,
218                     ActivityManager.getCurrentUser());
219         } catch (PackageManager.NameNotFoundException ex) {
220             Log.e(TAG, "package not found: " + statusBarNotification.getPackageName());
221         }
222         return packageInfo;
223     }
224 
225     /**
226      * Finds a suitable color such that there's enough contrast.
227      *
228      * @param foregroundColor  the color to start searching from.
229      * @param backgroundColor  the color to ensure contrast against. Assumed to be lighter than
230      *                         {@param foregroundColor}
231      * @param minContrastRatio the minimum contrast ratio required.
232      * @return a color with the same hue as {@param foregroundColor}, potentially darkened to
233      * meet the contrast ratio.
234      */
findContrastColorAgainstLightBackground( @olorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio)235     private static int findContrastColorAgainstLightBackground(
236             @ColorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio) {
237         if (ColorUtils.calculateContrast(foregroundColor, backgroundColor) >= minContrastRatio) {
238             return foregroundColor;
239         }
240 
241         double[] lab = new double[3];
242         ColorUtils.colorToLAB(foregroundColor, lab);
243 
244         double low = MIN_LIGHTNESS;
245         double high = lab[0];
246         double a = lab[1];
247         double b = lab[2];
248         for (int i = 0; i < MAX_FIND_COLOR_STEPS && high - low > MIN_COLOR_CONTRAST; i++) {
249             double l = (low + high) / 2;
250             foregroundColor = ColorUtils.LABToColor(l, a, b);
251             if (ColorUtils.calculateContrast(foregroundColor, backgroundColor) > minContrastRatio) {
252                 low = l;
253             } else {
254                 high = l;
255             }
256         }
257         return ColorUtils.LABToColor(low, a, b);
258     }
259 
260     /**
261      * Finds a suitable color such that there's enough contrast.
262      *
263      * @param foregroundColor  the foregroundColor to start searching from.
264      * @param backgroundColor  the foregroundColor to ensure contrast against. Assumed to be
265      *                         darker than {@param foregroundColor}
266      * @param minContrastRatio the minimum contrast ratio required.
267      * @return a foregroundColor with the same hue as {@param foregroundColor}, potentially
268      * lightened to meet the contrast ratio.
269      */
findContrastColorAgainstDarkBackground( @olorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio)270     private static int findContrastColorAgainstDarkBackground(
271             @ColorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio) {
272         if (ColorUtils.calculateContrast(foregroundColor, backgroundColor) >= minContrastRatio) {
273             return foregroundColor;
274         }
275 
276         float[] hsl = new float[3];
277         ColorUtils.colorToHSL(foregroundColor, hsl);
278 
279         float low = hsl[2];
280         float high = MAX_LIGHTNESS;
281         for (int i = 0; i < MAX_FIND_COLOR_STEPS && high - low > MIN_COLOR_CONTRAST; i++) {
282             float l = (low + high) / 2;
283             hsl[2] = l;
284             foregroundColor = ColorUtils.HSLToColor(hsl);
285             if (ColorUtils.calculateContrast(foregroundColor, backgroundColor)
286                     > minContrastRatio) {
287                 high = l;
288             } else {
289                 low = l;
290             }
291         }
292         return foregroundColor;
293     }
294 
295     /**
296      * Finds a foregroundColor with sufficient contrast over backgroundColor that has the same or
297      * darker hue as the original foregroundColor.
298      *
299      * @param foregroundColor  the foregroundColor to start searching from
300      * @param backgroundColor  the foregroundColor to ensure contrast against
301      * @param minContrastRatio the minimum contrast ratio required
302      */
getContrastedForegroundColor( @olorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio)303     private static int getContrastedForegroundColor(
304             @ColorInt int foregroundColor, @ColorInt int backgroundColor, double minContrastRatio) {
305         boolean isBackgroundDarker =
306                 Color.luminance(foregroundColor) > Color.luminance(backgroundColor);
307         return isBackgroundDarker
308                 ? findContrastColorAgainstDarkBackground(
309                 foregroundColor, backgroundColor, minContrastRatio)
310                 : findContrastColorAgainstLightBackground(
311                         foregroundColor, backgroundColor, minContrastRatio);
312     }
313 }
314