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