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