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