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