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