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