1 /* 2 * Copyright (C) 2014 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.systemui.statusbar.notification.row.wrapper; 18 19 import android.annotation.ColorInt; 20 import android.app.Notification; 21 import android.content.Context; 22 import android.content.res.Configuration; 23 import android.graphics.Color; 24 import android.graphics.ColorMatrix; 25 import android.graphics.ColorMatrixColorFilter; 26 import android.graphics.Paint; 27 import android.graphics.drawable.ColorDrawable; 28 import android.graphics.drawable.Drawable; 29 import android.os.Build; 30 import android.view.NotificationHeaderView; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.TextView; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.internal.graphics.ColorUtils; 37 import com.android.internal.util.ContrastColorUtil; 38 import com.android.systemui.statusbar.CrossFadeHelper; 39 import com.android.systemui.statusbar.TransformableView; 40 import com.android.systemui.statusbar.notification.TransformState; 41 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 42 43 /** 44 * Wraps the actual notification content view; used to implement behaviors which are different for 45 * the individual templates and custom views. 46 */ 47 public abstract class NotificationViewWrapper implements TransformableView { 48 49 protected final View mView; 50 protected final ExpandableNotificationRow mRow; 51 52 protected int mBackgroundColor = 0; 53 wrap(Context ctx, View v, ExpandableNotificationRow row)54 public static NotificationViewWrapper wrap(Context ctx, View v, ExpandableNotificationRow row) { 55 if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) { 56 if ("bigPicture".equals(v.getTag())) { 57 return new NotificationBigPictureTemplateViewWrapper(ctx, v, row); 58 } else if ("bigText".equals(v.getTag())) { 59 return new NotificationBigTextTemplateViewWrapper(ctx, v, row); 60 } else if ("media".equals(v.getTag()) || "bigMediaNarrow".equals(v.getTag())) { 61 return new NotificationMediaTemplateViewWrapper(ctx, v, row); 62 } else if ("messaging".equals(v.getTag())) { 63 return new NotificationMessagingTemplateViewWrapper(ctx, v, row); 64 } 65 Class<? extends Notification.Style> style = 66 row.getEntry().notification.getNotification().getNotificationStyle(); 67 if (Notification.DecoratedCustomViewStyle.class.equals(style)) { 68 return new NotificationDecoratedCustomViewWrapper(ctx, v, row); 69 } 70 return new NotificationTemplateViewWrapper(ctx, v, row); 71 } else if (v instanceof NotificationHeaderView) { 72 return new NotificationHeaderViewWrapper(ctx, v, row); 73 } else { 74 return new NotificationCustomViewWrapper(ctx, v, row); 75 } 76 } 77 NotificationViewWrapper(Context ctx, View view, ExpandableNotificationRow row)78 protected NotificationViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { 79 mView = view; 80 mRow = row; 81 onReinflated(); 82 } 83 84 /** 85 * Notifies this wrapper that the content of the view might have changed. 86 * @param row the row this wrapper is attached to 87 */ onContentUpdated(ExpandableNotificationRow row)88 public void onContentUpdated(ExpandableNotificationRow row) { 89 } 90 onReinflated()91 public void onReinflated() { 92 if (shouldClearBackgroundOnReapply()) { 93 mBackgroundColor = 0; 94 } 95 int backgroundColor = getBackgroundColor(mView); 96 if (backgroundColor != Color.TRANSPARENT) { 97 mBackgroundColor = backgroundColor; 98 mView.setBackground(new ColorDrawable(Color.TRANSPARENT)); 99 } 100 } 101 needsInversion(int defaultBackgroundColor, View view)102 protected boolean needsInversion(int defaultBackgroundColor, View view) { 103 if (view == null) { 104 return false; 105 } 106 107 Configuration configuration = mView.getResources().getConfiguration(); 108 boolean nightMode = (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK) 109 == Configuration.UI_MODE_NIGHT_YES; 110 if (!nightMode) { 111 return false; 112 } 113 114 // Apps targeting Q should fix their dark mode bugs. 115 if (mRow.getEntry().targetSdk >= Build.VERSION_CODES.Q) { 116 return false; 117 } 118 119 int background = getBackgroundColor(view); 120 if (background == Color.TRANSPARENT) { 121 background = defaultBackgroundColor; 122 } 123 if (background == Color.TRANSPARENT) { 124 background = resolveBackgroundColor(); 125 } 126 127 float[] hsl = new float[] {0f, 0f, 0f}; 128 ColorUtils.colorToHSL(background, hsl); 129 130 // Notifications with colored backgrounds should not be inverted 131 if (hsl[1] != 0) { 132 return false; 133 } 134 135 // Invert white or light gray backgrounds. 136 boolean isLightGrayOrWhite = hsl[1] == 0 && hsl[2] > 0.5; 137 if (isLightGrayOrWhite) { 138 return true; 139 } 140 141 // Now let's check if there's unprotected text somewhere, and invert if we find it. 142 if (view instanceof ViewGroup) { 143 return childrenNeedInversion(background, (ViewGroup) view); 144 } else { 145 return false; 146 } 147 } 148 149 @VisibleForTesting childrenNeedInversion(@olorInt int parentBackground, ViewGroup viewGroup)150 boolean childrenNeedInversion(@ColorInt int parentBackground, ViewGroup viewGroup) { 151 if (viewGroup == null) { 152 return false; 153 } 154 155 int backgroundColor = getBackgroundColor(viewGroup); 156 if (Color.alpha(backgroundColor) != 255) { 157 backgroundColor = ContrastColorUtil.compositeColors(backgroundColor, parentBackground); 158 backgroundColor = ColorUtils.setAlphaComponent(backgroundColor, 255); 159 } 160 for (int i = 0; i < viewGroup.getChildCount(); i++) { 161 View child = viewGroup.getChildAt(i); 162 if (child instanceof TextView) { 163 int foreground = ((TextView) child).getCurrentTextColor(); 164 if (ColorUtils.calculateContrast(foreground, backgroundColor) < 3) { 165 return true; 166 } 167 } else if (child instanceof ViewGroup) { 168 if (childrenNeedInversion(backgroundColor, (ViewGroup) child)) { 169 return true; 170 } 171 } 172 } 173 174 return false; 175 } 176 getBackgroundColor(View view)177 protected int getBackgroundColor(View view) { 178 if (view == null) { 179 return Color.TRANSPARENT; 180 } 181 Drawable background = view.getBackground(); 182 if (background instanceof ColorDrawable) { 183 return ((ColorDrawable) background).getColor(); 184 } 185 return Color.TRANSPARENT; 186 } 187 invertViewLuminosity(View view)188 protected void invertViewLuminosity(View view) { 189 Paint paint = new Paint(); 190 ColorMatrix matrix = new ColorMatrix(); 191 ColorMatrix tmp = new ColorMatrix(); 192 // Inversion should happen on Y'UV space to conserve the colors and 193 // only affect the luminosity. 194 matrix.setRGB2YUV(); 195 tmp.set(new float[]{ 196 -1f, 0f, 0f, 0f, 255f, 197 0f, 1f, 0f, 0f, 0f, 198 0f, 0f, 1f, 0f, 0f, 199 0f, 0f, 0f, 1f, 0f 200 }); 201 matrix.postConcat(tmp); 202 tmp.setYUV2RGB(); 203 matrix.postConcat(tmp); 204 paint.setColorFilter(new ColorMatrixColorFilter(matrix)); 205 view.setLayerType(View.LAYER_TYPE_HARDWARE, paint); 206 } 207 shouldClearBackgroundOnReapply()208 protected boolean shouldClearBackgroundOnReapply() { 209 return true; 210 } 211 212 /** 213 * Update the appearance of the expand button. 214 * 215 * @param expandable should this view be expandable 216 * @param onClickListener the listener to invoke when the expand affordance is clicked on 217 */ updateExpandability(boolean expandable, View.OnClickListener onClickListener)218 public void updateExpandability(boolean expandable, View.OnClickListener onClickListener) {} 219 220 /** 221 * @return the notification header if it exists 222 */ getNotificationHeader()223 public NotificationHeaderView getNotificationHeader() { 224 return null; 225 } 226 getHeaderTranslation(boolean forceNoHeader)227 public int getHeaderTranslation(boolean forceNoHeader) { 228 return 0; 229 } 230 231 @Override getCurrentState(int fadingView)232 public TransformState getCurrentState(int fadingView) { 233 return null; 234 } 235 236 @Override transformTo(TransformableView notification, Runnable endRunnable)237 public void transformTo(TransformableView notification, Runnable endRunnable) { 238 // By default we are fading out completely 239 CrossFadeHelper.fadeOut(mView, endRunnable); 240 } 241 242 @Override transformTo(TransformableView notification, float transformationAmount)243 public void transformTo(TransformableView notification, float transformationAmount) { 244 CrossFadeHelper.fadeOut(mView, transformationAmount); 245 } 246 247 @Override transformFrom(TransformableView notification)248 public void transformFrom(TransformableView notification) { 249 // By default we are fading in completely 250 CrossFadeHelper.fadeIn(mView); 251 } 252 253 @Override transformFrom(TransformableView notification, float transformationAmount)254 public void transformFrom(TransformableView notification, float transformationAmount) { 255 CrossFadeHelper.fadeIn(mView, transformationAmount); 256 } 257 258 @Override setVisible(boolean visible)259 public void setVisible(boolean visible) { 260 mView.animate().cancel(); 261 mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); 262 } 263 264 /** 265 * Called to indicate this view is removed 266 */ setRemoved()267 public void setRemoved() { 268 } 269 getCustomBackgroundColor()270 public int getCustomBackgroundColor() { 271 // Parent notifications should always use the normal background color 272 return mRow.isSummaryWithChildren() ? 0 : mBackgroundColor; 273 } 274 resolveBackgroundColor()275 protected int resolveBackgroundColor() { 276 int customBackgroundColor = getCustomBackgroundColor(); 277 if (customBackgroundColor != 0) { 278 return customBackgroundColor; 279 } 280 return mView.getContext().getColor( 281 com.android.internal.R.color.notification_material_background_color); 282 } 283 setLegacy(boolean legacy)284 public void setLegacy(boolean legacy) { 285 } 286 setContentHeight(int contentHeight, int minHeightHint)287 public void setContentHeight(int contentHeight, int minHeightHint) { 288 } 289 setRemoteInputVisible(boolean visible)290 public void setRemoteInputVisible(boolean visible) { 291 } 292 setIsChildInGroup(boolean isChildInGroup)293 public void setIsChildInGroup(boolean isChildInGroup) { 294 } 295 isDimmable()296 public boolean isDimmable() { 297 return true; 298 } 299 disallowSingleClick(float x, float y)300 public boolean disallowSingleClick(float x, float y) { 301 return false; 302 } 303 getMinLayoutHeight()304 public int getMinLayoutHeight() { 305 return 0; 306 } 307 shouldClipToRounding(boolean topRounded, boolean bottomRounded)308 public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) { 309 return false; 310 } 311 setHeaderVisibleAmount(float headerVisibleAmount)312 public void setHeaderVisibleAmount(float headerVisibleAmount) { 313 } 314 315 /** 316 * Get the extra height that needs to be added to this view, such that it can be measured 317 * normally. 318 */ getExtraMeasureHeight()319 public int getExtraMeasureHeight() { 320 return 0; 321 } 322 } 323