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