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.annotation.Nullable; 21 import android.app.Notification; 22 import android.content.Context; 23 import android.content.res.Configuration; 24 import android.graphics.Color; 25 import android.graphics.ColorMatrix; 26 import android.graphics.ColorMatrixColorFilter; 27 import android.graphics.Paint; 28 import android.graphics.Rect; 29 import android.graphics.drawable.ColorDrawable; 30 import android.graphics.drawable.Drawable; 31 import android.os.Build; 32 import android.view.NotificationHeaderView; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.widget.TextView; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.internal.graphics.ColorUtils; 39 import com.android.internal.util.ContrastColorUtil; 40 import com.android.internal.widget.CachingIconView; 41 import com.android.settingslib.Utils; 42 import com.android.systemui.statusbar.CrossFadeHelper; 43 import com.android.systemui.statusbar.TransformableView; 44 import com.android.systemui.statusbar.notification.FeedbackIcon; 45 import com.android.systemui.statusbar.notification.NotificationFadeAware; 46 import com.android.systemui.statusbar.notification.TransformState; 47 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 48 49 /** 50 * Wraps the actual notification content view; used to implement behaviors which are different for 51 * the individual templates and custom views. 52 */ 53 public abstract class NotificationViewWrapper implements TransformableView { 54 55 protected final View mView; 56 protected final ExpandableNotificationRow mRow; 57 private final Rect mTmpRect = new Rect(); 58 59 protected int mBackgroundColor = 0; 60 wrap(Context ctx, View v, ExpandableNotificationRow row)61 public static NotificationViewWrapper wrap(Context ctx, View v, ExpandableNotificationRow row) { 62 if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) { 63 if ("bigPicture".equals(v.getTag())) { 64 return new NotificationBigPictureTemplateViewWrapper(ctx, v, row); 65 } else if ("bigText".equals(v.getTag())) { 66 return new NotificationBigTextTemplateViewWrapper(ctx, v, row); 67 } else if ("media".equals(v.getTag()) || "bigMediaNarrow".equals(v.getTag())) { 68 return new NotificationMediaTemplateViewWrapper(ctx, v, row); 69 } else if ("messaging".equals(v.getTag())) { 70 return new NotificationMessagingTemplateViewWrapper(ctx, v, row); 71 } else if ("conversation".equals(v.getTag())) { 72 return new NotificationConversationTemplateViewWrapper(ctx, v, row); 73 } else if ("call".equals(v.getTag())) { 74 return new NotificationCallTemplateViewWrapper(ctx, v, row); 75 } 76 if (row.getEntry().getSbn().getNotification().isStyle( 77 Notification.DecoratedCustomViewStyle.class)) { 78 return new NotificationDecoratedCustomViewWrapper(ctx, v, row); 79 } 80 if (NotificationDecoratedCustomViewWrapper.hasCustomView(v)) { 81 return new NotificationDecoratedCustomViewWrapper(ctx, v, row); 82 } 83 return new NotificationTemplateViewWrapper(ctx, v, row); 84 } else if (v instanceof NotificationHeaderView) { 85 return new NotificationHeaderViewWrapper(ctx, v, row); 86 } else { 87 return new NotificationCustomViewWrapper(ctx, v, row); 88 } 89 } 90 NotificationViewWrapper(Context ctx, View view, ExpandableNotificationRow row)91 protected NotificationViewWrapper(Context ctx, View view, ExpandableNotificationRow row) { 92 mView = view; 93 mRow = row; 94 onReinflated(); 95 } 96 97 /** 98 * Notifies this wrapper that the content of the view might have changed. 99 * @param row the row this wrapper is attached to 100 */ onContentUpdated(ExpandableNotificationRow row)101 public void onContentUpdated(ExpandableNotificationRow row) { 102 } 103 104 /** Shows the given feedback icon, or hides the icon if null. */ setFeedbackIcon(@ullable FeedbackIcon icon)105 public void setFeedbackIcon(@Nullable FeedbackIcon icon) { 106 } 107 onReinflated()108 public void onReinflated() { 109 if (shouldClearBackgroundOnReapply()) { 110 mBackgroundColor = 0; 111 } 112 int backgroundColor = getBackgroundColor(mView); 113 if (backgroundColor != Color.TRANSPARENT) { 114 mBackgroundColor = backgroundColor; 115 mView.setBackground(new ColorDrawable(Color.TRANSPARENT)); 116 } 117 } 118 needsInversion(int defaultBackgroundColor, View view)119 protected boolean needsInversion(int defaultBackgroundColor, View view) { 120 if (view == null) { 121 return false; 122 } 123 124 Configuration configuration = mView.getResources().getConfiguration(); 125 boolean nightMode = (configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK) 126 == Configuration.UI_MODE_NIGHT_YES; 127 if (!nightMode) { 128 return false; 129 } 130 131 // Apps targeting Q should fix their dark mode bugs. 132 if (mRow.getEntry().targetSdk >= Build.VERSION_CODES.Q) { 133 return false; 134 } 135 136 int background = getBackgroundColor(view); 137 if (background == Color.TRANSPARENT) { 138 background = defaultBackgroundColor; 139 } 140 if (background == Color.TRANSPARENT) { 141 background = resolveBackgroundColor(); 142 } 143 144 float[] hsl = new float[] {0f, 0f, 0f}; 145 ColorUtils.colorToHSL(background, hsl); 146 147 // Notifications with colored backgrounds should not be inverted 148 if (hsl[1] != 0) { 149 return false; 150 } 151 152 // Invert white or light gray backgrounds. 153 boolean isLightGrayOrWhite = hsl[1] == 0 && hsl[2] > 0.5; 154 if (isLightGrayOrWhite) { 155 return true; 156 } 157 158 // Now let's check if there's unprotected text somewhere, and invert if we find it. 159 if (view instanceof ViewGroup) { 160 return childrenNeedInversion(background, (ViewGroup) view); 161 } else { 162 return false; 163 } 164 } 165 166 @VisibleForTesting childrenNeedInversion(@olorInt int parentBackground, ViewGroup viewGroup)167 boolean childrenNeedInversion(@ColorInt int parentBackground, ViewGroup viewGroup) { 168 if (viewGroup == null) { 169 return false; 170 } 171 172 int backgroundColor = getBackgroundColor(viewGroup); 173 if (Color.alpha(backgroundColor) != 255) { 174 backgroundColor = ContrastColorUtil.compositeColors(backgroundColor, parentBackground); 175 backgroundColor = ColorUtils.setAlphaComponent(backgroundColor, 255); 176 } 177 for (int i = 0; i < viewGroup.getChildCount(); i++) { 178 View child = viewGroup.getChildAt(i); 179 if (child instanceof TextView) { 180 int foreground = ((TextView) child).getCurrentTextColor(); 181 if (ColorUtils.calculateContrast(foreground, backgroundColor) < 3) { 182 return true; 183 } 184 } else if (child instanceof ViewGroup) { 185 if (childrenNeedInversion(backgroundColor, (ViewGroup) child)) { 186 return true; 187 } 188 } 189 } 190 191 return false; 192 } 193 getBackgroundColor(View view)194 protected int getBackgroundColor(View view) { 195 if (view == null) { 196 return Color.TRANSPARENT; 197 } 198 Drawable background = view.getBackground(); 199 if (background instanceof ColorDrawable) { 200 return ((ColorDrawable) background).getColor(); 201 } 202 return Color.TRANSPARENT; 203 } 204 invertViewLuminosity(View view)205 protected void invertViewLuminosity(View view) { 206 Paint paint = new Paint(); 207 ColorMatrix matrix = new ColorMatrix(); 208 ColorMatrix tmp = new ColorMatrix(); 209 // Inversion should happen on Y'UV space to conserve the colors and 210 // only affect the luminosity. 211 matrix.setRGB2YUV(); 212 tmp.set(new float[]{ 213 -1f, 0f, 0f, 0f, 255f, 214 0f, 1f, 0f, 0f, 0f, 215 0f, 0f, 1f, 0f, 0f, 216 0f, 0f, 0f, 1f, 0f 217 }); 218 matrix.postConcat(tmp); 219 tmp.setYUV2RGB(); 220 matrix.postConcat(tmp); 221 paint.setColorFilter(new ColorMatrixColorFilter(matrix)); 222 view.setLayerType(View.LAYER_TYPE_HARDWARE, paint); 223 } 224 shouldClearBackgroundOnReapply()225 protected boolean shouldClearBackgroundOnReapply() { 226 return true; 227 } 228 229 /** 230 * Update the appearance of the expand button. 231 * 232 * @param expandable should this view be expandable 233 * @param onClickListener the listener to invoke when the expand affordance is clicked on 234 * @param requestLayout the expandability changed during onLayout, so a requestLayout required 235 */ updateExpandability(boolean expandable, View.OnClickListener onClickListener, boolean requestLayout)236 public void updateExpandability(boolean expandable, View.OnClickListener onClickListener, 237 boolean requestLayout) {} 238 239 /** Set the expanded state on the view wrapper */ setExpanded(boolean expanded)240 public void setExpanded(boolean expanded) {} 241 242 /** 243 * @return the notification header if it exists 244 */ getNotificationHeader()245 public NotificationHeaderView getNotificationHeader() { 246 return null; 247 } 248 249 /** 250 * @return the expand button if it exists 251 */ 252 @Nullable getExpandButton()253 public View getExpandButton() { 254 return null; 255 } 256 257 /** 258 * @return the icon if it exists 259 */ 260 @Nullable getIcon()261 public CachingIconView getIcon() { 262 return null; 263 } 264 getOriginalIconColor()265 public int getOriginalIconColor() { 266 return Notification.COLOR_INVALID; 267 } 268 269 /** 270 * @return get the transformation target of the shelf, which usually is the icon 271 */ getShelfTransformationTarget()272 public @Nullable View getShelfTransformationTarget() { 273 return null; 274 } 275 getHeaderTranslation(boolean forceNoHeader)276 public int getHeaderTranslation(boolean forceNoHeader) { 277 return 0; 278 } 279 280 @Override getCurrentState(int fadingView)281 public TransformState getCurrentState(int fadingView) { 282 return null; 283 } 284 285 @Override transformTo(TransformableView notification, Runnable endRunnable)286 public void transformTo(TransformableView notification, Runnable endRunnable) { 287 // By default we are fading out completely 288 CrossFadeHelper.fadeOut(mView, endRunnable); 289 } 290 291 @Override transformTo(TransformableView notification, float transformationAmount)292 public void transformTo(TransformableView notification, float transformationAmount) { 293 CrossFadeHelper.fadeOut(mView, transformationAmount); 294 } 295 296 @Override transformFrom(TransformableView notification)297 public void transformFrom(TransformableView notification) { 298 // By default we are fading in completely 299 CrossFadeHelper.fadeIn(mView); 300 } 301 302 @Override transformFrom(TransformableView notification, float transformationAmount)303 public void transformFrom(TransformableView notification, float transformationAmount) { 304 CrossFadeHelper.fadeIn(mView, transformationAmount, true /* remap */); 305 } 306 307 @Override setVisible(boolean visible)308 public void setVisible(boolean visible) { 309 mView.animate().cancel(); 310 mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); 311 } 312 313 /** 314 * Called to indicate this view is removed 315 */ setRemoved()316 public void setRemoved() { 317 } 318 getCustomBackgroundColor()319 public int getCustomBackgroundColor() { 320 // Parent notifications should always use the normal background color 321 return mRow.isSummaryWithChildren() ? 0 : mBackgroundColor; 322 } 323 resolveBackgroundColor()324 protected int resolveBackgroundColor() { 325 int customBackgroundColor = getCustomBackgroundColor(); 326 if (customBackgroundColor != 0) { 327 return customBackgroundColor; 328 } 329 return Utils.getColorAttr(mView.getContext(), android.R.attr.colorBackground) 330 .getDefaultColor(); 331 } 332 setLegacy(boolean legacy)333 public void setLegacy(boolean legacy) { 334 } 335 setContentHeight(int contentHeight, int minHeightHint)336 public void setContentHeight(int contentHeight, int minHeightHint) { 337 } 338 setRemoteInputVisible(boolean visible)339 public void setRemoteInputVisible(boolean visible) { 340 } 341 setIsChildInGroup(boolean isChildInGroup)342 public void setIsChildInGroup(boolean isChildInGroup) { 343 } 344 isDimmable()345 public boolean isDimmable() { 346 return true; 347 } 348 disallowSingleClick(float x, float y)349 public boolean disallowSingleClick(float x, float y) { 350 return false; 351 } 352 353 /** 354 * Is a given x and y coordinate on a view. 355 * 356 * @param view the view to be checked 357 * @param x the x coordinate, relative to the ExpandableNotificationRow 358 * @param y the y coordinate, relative to the ExpandableNotificationRow 359 * @return {@code true} if it is on the view 360 */ isOnView(View view, float x, float y)361 protected boolean isOnView(View view, float x, float y) { 362 View searchView = (View) view.getParent(); 363 while (searchView != null && !(searchView instanceof ExpandableNotificationRow)) { 364 searchView.getHitRect(mTmpRect); 365 x -= mTmpRect.left; 366 y -= mTmpRect.top; 367 searchView = (View) searchView.getParent(); 368 } 369 view.getHitRect(mTmpRect); 370 return mTmpRect.contains((int) x,(int) y); 371 } 372 getMinLayoutHeight()373 public int getMinLayoutHeight() { 374 return 0; 375 } 376 shouldClipToRounding(boolean topRounded, boolean bottomRounded)377 public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) { 378 return false; 379 } 380 setHeaderVisibleAmount(float headerVisibleAmount)381 public void setHeaderVisibleAmount(float headerVisibleAmount) { 382 } 383 384 /** 385 * Get the extra height that needs to be added to this view, such that it can be measured 386 * normally. 387 */ getExtraMeasureHeight()388 public int getExtraMeasureHeight() { 389 return 0; 390 } 391 392 /** 393 * Set the view to have recently visibly alerted. 394 */ setRecentlyAudiblyAlerted(boolean audiblyAlerted)395 public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) { 396 } 397 398 /** 399 * Apply the faded state as a layer type change to the views which need to have overlapping 400 * contents render precisely. 401 */ setNotificationFaded(boolean faded)402 public void setNotificationFaded(boolean faded) { 403 NotificationFadeAware.setLayerTypeForFaded(getIcon(), faded); 404 NotificationFadeAware.setLayerTypeForFaded(getExpandButton(), faded); 405 } 406 407 /** 408 * Starts or stops the animations in any drawables contained in this Notification. 409 * 410 * @param running Whether the animations should be set to run. 411 */ setAnimationsRunning(boolean running)412 public void setAnimationsRunning(boolean running) { 413 } 414 } 415