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