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