1 /*
2  * Copyright (C) 2015 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 android.view;
18 
19 import android.annotation.Nullable;
20 import android.app.AppOpsManager;
21 import android.app.Notification;
22 import android.compat.annotation.UnsupportedAppUsage;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.content.res.TypedArray;
26 import android.graphics.Canvas;
27 import android.graphics.Outline;
28 import android.graphics.Rect;
29 import android.graphics.drawable.Drawable;
30 import android.util.ArraySet;
31 import android.util.AttributeSet;
32 import android.widget.ImageView;
33 import android.widget.RemoteViews;
34 
35 import com.android.internal.R;
36 import com.android.internal.widget.CachingIconView;
37 
38 import java.util.ArrayList;
39 
40 /**
41  * A header of a notification view
42  *
43  * @hide
44  */
45 @RemoteViews.RemoteView
46 public class NotificationHeaderView extends ViewGroup {
47     public static final int NO_COLOR = Notification.COLOR_INVALID;
48     private final int mChildMinWidth;
49     private final int mContentEndMargin;
50     private final int mGravity;
51     private View mAppName;
52     private View mHeaderText;
53     private View mSecondaryHeaderText;
54     private OnClickListener mExpandClickListener;
55     private OnClickListener mAppOpsListener;
56     private HeaderTouchListener mTouchListener = new HeaderTouchListener();
57     private ImageView mExpandButton;
58     private CachingIconView mIcon;
59     private View mProfileBadge;
60     private View mOverlayIcon;
61     private View mCameraIcon;
62     private View mMicIcon;
63     private View mAppOps;
64     private View mAudiblyAlertedIcon;
65     private int mIconColor;
66     private int mOriginalNotificationColor;
67     private boolean mExpanded;
68     private boolean mShowExpandButtonAtEnd;
69     private boolean mShowWorkBadgeAtEnd;
70     private int mHeaderTextMarginEnd;
71     private Drawable mBackground;
72     private boolean mEntireHeaderClickable;
73     private boolean mExpandOnlyOnButton;
74     private boolean mAcceptAllTouches;
75     private int mTotalWidth;
76 
77     ViewOutlineProvider mProvider = new ViewOutlineProvider() {
78         @Override
79         public void getOutline(View view, Outline outline) {
80             if (mBackground != null) {
81                 outline.setRect(0, 0, getWidth(), getHeight());
82                 outline.setAlpha(1f);
83             }
84         }
85     };
86 
NotificationHeaderView(Context context)87     public NotificationHeaderView(Context context) {
88         this(context, null);
89     }
90 
91     @UnsupportedAppUsage
NotificationHeaderView(Context context, @Nullable AttributeSet attrs)92     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) {
93         this(context, attrs, 0);
94     }
95 
NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)96     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
97         this(context, attrs, defStyleAttr, 0);
98     }
99 
NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)100     public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
101         super(context, attrs, defStyleAttr, defStyleRes);
102         Resources res = getResources();
103         mChildMinWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_min_width);
104         mContentEndMargin = res.getDimensionPixelSize(R.dimen.notification_content_margin_end);
105         mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand);
106 
107         int[] attrIds = { android.R.attr.gravity };
108         TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes);
109         mGravity = ta.getInt(0, 0);
110         ta.recycle();
111     }
112 
113     @Override
onFinishInflate()114     protected void onFinishInflate() {
115         super.onFinishInflate();
116         mAppName = findViewById(com.android.internal.R.id.app_name_text);
117         mHeaderText = findViewById(com.android.internal.R.id.header_text);
118         mSecondaryHeaderText = findViewById(com.android.internal.R.id.header_text_secondary);
119         mExpandButton = findViewById(com.android.internal.R.id.expand_button);
120         mIcon = findViewById(com.android.internal.R.id.icon);
121         mProfileBadge = findViewById(com.android.internal.R.id.profile_badge);
122         mCameraIcon = findViewById(com.android.internal.R.id.camera);
123         mMicIcon = findViewById(com.android.internal.R.id.mic);
124         mOverlayIcon = findViewById(com.android.internal.R.id.overlay);
125         mAppOps = findViewById(com.android.internal.R.id.app_ops);
126         mAudiblyAlertedIcon = findViewById(com.android.internal.R.id.alerted_icon);
127     }
128 
129     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)130     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
131         final int givenWidth = MeasureSpec.getSize(widthMeasureSpec);
132         final int givenHeight = MeasureSpec.getSize(heightMeasureSpec);
133         int wrapContentWidthSpec = MeasureSpec.makeMeasureSpec(givenWidth,
134                 MeasureSpec.AT_MOST);
135         int wrapContentHeightSpec = MeasureSpec.makeMeasureSpec(givenHeight,
136                 MeasureSpec.AT_MOST);
137         int totalWidth = getPaddingStart();
138         int iconWidth = getPaddingEnd();
139         for (int i = 0; i < getChildCount(); i++) {
140             final View child = getChildAt(i);
141             if (child.getVisibility() == GONE) {
142                 // We'll give it the rest of the space in the end
143                 continue;
144             }
145             final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
146             int childWidthSpec = getChildMeasureSpec(wrapContentWidthSpec,
147                     lp.leftMargin + lp.rightMargin, lp.width);
148             int childHeightSpec = getChildMeasureSpec(wrapContentHeightSpec,
149                     lp.topMargin + lp.bottomMargin, lp.height);
150             child.measure(childWidthSpec, childHeightSpec);
151             if ((child == mExpandButton && mShowExpandButtonAtEnd)
152                     || child == mProfileBadge
153                     || child == mAppOps) {
154                 iconWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
155             } else {
156                 totalWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
157             }
158         }
159 
160         // Ensure that there is at least enough space for the icons
161         int endMargin = Math.max(mHeaderTextMarginEnd, iconWidth);
162         if (totalWidth > givenWidth - endMargin) {
163             int overFlow = totalWidth - givenWidth + endMargin;
164             // We are overflowing, lets shrink the app name first
165             overFlow = shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mAppName,
166                     mChildMinWidth);
167 
168             // still overflowing, we shrink the header text
169             overFlow = shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mHeaderText, 0);
170 
171             // still overflowing, finally we shrink the secondary header text
172             shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mSecondaryHeaderText,
173                     0);
174         }
175         totalWidth += getPaddingEnd();
176         mTotalWidth = Math.min(totalWidth, givenWidth);
177         setMeasuredDimension(givenWidth, givenHeight);
178     }
179 
shrinkViewForOverflow(int heightSpec, int overFlow, View targetView, int minimumWidth)180     private int shrinkViewForOverflow(int heightSpec, int overFlow, View targetView,
181             int minimumWidth) {
182         final int oldWidth = targetView.getMeasuredWidth();
183         if (overFlow > 0 && targetView.getVisibility() != GONE && oldWidth > minimumWidth) {
184             // we're still too big
185             int newSize = Math.max(minimumWidth, oldWidth - overFlow);
186             int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
187             targetView.measure(childWidthSpec, heightSpec);
188             overFlow -= oldWidth - newSize;
189         }
190         return overFlow;
191     }
192 
193     @Override
onLayout(boolean changed, int l, int t, int r, int b)194     protected void onLayout(boolean changed, int l, int t, int r, int b) {
195         int left = getPaddingStart();
196         int end = getMeasuredWidth();
197         final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
198         if (centerAligned) {
199             left += getMeasuredWidth() / 2 - mTotalWidth / 2;
200         }
201         int childCount = getChildCount();
202         int ownHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
203         for (int i = 0; i < childCount; i++) {
204             View child = getChildAt(i);
205             if (child.getVisibility() == GONE) {
206                 continue;
207             }
208             int childHeight = child.getMeasuredHeight();
209             MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
210             int layoutLeft;
211             int layoutRight;
212             int top = (int) (getPaddingTop() + (ownHeight - childHeight) / 2.0f);
213             int bottom = top + childHeight;
214             if ((child == mExpandButton && mShowExpandButtonAtEnd)
215                     || child == mProfileBadge
216                     || child == mAppOps) {
217                 if (end == getMeasuredWidth()) {
218                     layoutRight = end - mContentEndMargin;
219                 } else {
220                     layoutRight = end - params.getMarginEnd();
221                 }
222                 layoutLeft = layoutRight - child.getMeasuredWidth();
223                 end = layoutLeft - params.getMarginStart();
224             } else {
225                 left += params.getMarginStart();
226                 int right = left + child.getMeasuredWidth();
227                 layoutLeft = left;
228                 layoutRight = right;
229                 left = right + params.getMarginEnd();
230             }
231             if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
232                 int ltrLeft = layoutLeft;
233                 layoutLeft = getWidth() - layoutRight;
234                 layoutRight = getWidth() - ltrLeft;
235             }
236             child.layout(layoutLeft, top, layoutRight, bottom);
237         }
238         updateTouchListener();
239     }
240 
241     @Override
generateLayoutParams(AttributeSet attrs)242     public LayoutParams generateLayoutParams(AttributeSet attrs) {
243         return new ViewGroup.MarginLayoutParams(getContext(), attrs);
244     }
245 
246     /**
247      * Set a {@link Drawable} to be displayed as a background on the header.
248      */
setHeaderBackgroundDrawable(Drawable drawable)249     public void setHeaderBackgroundDrawable(Drawable drawable) {
250         if (drawable != null) {
251             setWillNotDraw(false);
252             mBackground = drawable;
253             mBackground.setCallback(this);
254             setOutlineProvider(mProvider);
255         } else {
256             setWillNotDraw(true);
257             mBackground = null;
258             setOutlineProvider(null);
259         }
260         invalidate();
261     }
262 
263     @Override
onDraw(Canvas canvas)264     protected void onDraw(Canvas canvas) {
265         if (mBackground != null) {
266             mBackground.setBounds(0, 0, getWidth(), getHeight());
267             mBackground.draw(canvas);
268         }
269     }
270 
271     @Override
verifyDrawable(Drawable who)272     protected boolean verifyDrawable(Drawable who) {
273         return super.verifyDrawable(who) || who == mBackground;
274     }
275 
276     @Override
drawableStateChanged()277     protected void drawableStateChanged() {
278         if (mBackground != null && mBackground.isStateful()) {
279             mBackground.setState(getDrawableState());
280         }
281     }
282 
updateTouchListener()283     private void updateTouchListener() {
284         if (mExpandClickListener == null && mAppOpsListener == null) {
285             setOnTouchListener(null);
286             return;
287         }
288         setOnTouchListener(mTouchListener);
289         mTouchListener.bindTouchRects();
290     }
291 
292     /**
293      * Sets onclick listener for app ops icons.
294      */
setAppOpsOnClickListener(OnClickListener l)295     public void setAppOpsOnClickListener(OnClickListener l) {
296         mAppOpsListener = l;
297         mAppOps.setOnClickListener(mAppOpsListener);
298         mCameraIcon.setOnClickListener(mAppOpsListener);
299         mMicIcon.setOnClickListener(mAppOpsListener);
300         mOverlayIcon.setOnClickListener(mAppOpsListener);
301         updateTouchListener();
302     }
303 
304     @Override
setOnClickListener(@ullable OnClickListener l)305     public void setOnClickListener(@Nullable OnClickListener l) {
306         mExpandClickListener = l;
307         mExpandButton.setOnClickListener(mExpandClickListener);
308         updateTouchListener();
309     }
310 
311     @RemotableViewMethod
setOriginalIconColor(int color)312     public void setOriginalIconColor(int color) {
313         mIconColor = color;
314     }
315 
getOriginalIconColor()316     public int getOriginalIconColor() {
317         return mIconColor;
318     }
319 
320     @RemotableViewMethod
setOriginalNotificationColor(int color)321     public void setOriginalNotificationColor(int color) {
322         mOriginalNotificationColor = color;
323     }
324 
getOriginalNotificationColor()325     public int getOriginalNotificationColor() {
326         return mOriginalNotificationColor;
327     }
328 
329     @RemotableViewMethod
setExpanded(boolean expanded)330     public void setExpanded(boolean expanded) {
331         mExpanded = expanded;
332         updateExpandButton();
333     }
334 
335     /**
336      * Shows or hides 'app op in use' icons based on app usage.
337      */
showAppOpsIcons(ArraySet<Integer> appOps)338     public void showAppOpsIcons(ArraySet<Integer> appOps) {
339         if (mOverlayIcon == null || mCameraIcon == null || mMicIcon == null || appOps == null) {
340             return;
341         }
342 
343         mOverlayIcon.setVisibility(appOps.contains(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
344                 ? View.VISIBLE : View.GONE);
345         mCameraIcon.setVisibility(appOps.contains(AppOpsManager.OP_CAMERA)
346                 ? View.VISIBLE : View.GONE);
347         mMicIcon.setVisibility(appOps.contains(AppOpsManager.OP_RECORD_AUDIO)
348                 ? View.VISIBLE : View.GONE);
349     }
350 
351     /** Updates icon visibility based on the noisiness of the notification. */
setRecentlyAudiblyAlerted(boolean audiblyAlerted)352     public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) {
353         mAudiblyAlertedIcon.setVisibility(audiblyAlerted ? View.VISIBLE : View.GONE);
354     }
355 
updateExpandButton()356     private void updateExpandButton() {
357         int drawableId;
358         int contentDescriptionId;
359         if (mExpanded) {
360             drawableId = R.drawable.ic_collapse_notification;
361             contentDescriptionId = R.string.expand_button_content_description_expanded;
362         } else {
363             drawableId = R.drawable.ic_expand_notification;
364             contentDescriptionId = R.string.expand_button_content_description_collapsed;
365         }
366         mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
367         mExpandButton.setColorFilter(mOriginalNotificationColor);
368         mExpandButton.setContentDescription(mContext.getText(contentDescriptionId));
369     }
370 
setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd)371     public void setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd) {
372         if (showWorkBadgeAtEnd != mShowWorkBadgeAtEnd) {
373             setClipToPadding(!showWorkBadgeAtEnd);
374             mShowWorkBadgeAtEnd = showWorkBadgeAtEnd;
375         }
376     }
377 
378     /**
379      * Sets whether or not the expand button appears at the end of the NotificationHeaderView. If
380      * both this and {@link #setShowWorkBadgeAtEnd(boolean)} have been set to true, then the
381      * expand button will appear closer to the end than the work badge.
382      */
setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd)383     public void setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd) {
384         if (showExpandButtonAtEnd != mShowExpandButtonAtEnd) {
385             setClipToPadding(!showExpandButtonAtEnd);
386             mShowExpandButtonAtEnd = showExpandButtonAtEnd;
387         }
388     }
389 
getWorkProfileIcon()390     public View getWorkProfileIcon() {
391         return mProfileBadge;
392     }
393 
getIcon()394     public CachingIconView getIcon() {
395         return mIcon;
396     }
397 
398     /**
399      * Sets the margin end for the text portion of the header, excluding right-aligned elements
400      * @param headerTextMarginEnd margin size
401      */
402     @RemotableViewMethod
setHeaderTextMarginEnd(int headerTextMarginEnd)403     public void setHeaderTextMarginEnd(int headerTextMarginEnd) {
404         if (mHeaderTextMarginEnd != headerTextMarginEnd) {
405             mHeaderTextMarginEnd = headerTextMarginEnd;
406             requestLayout();
407         }
408     }
409 
410     /**
411      * Get the current margin end value for the header text
412      * @return margin size
413      */
getHeaderTextMarginEnd()414     public int getHeaderTextMarginEnd() {
415         return mHeaderTextMarginEnd;
416     }
417 
418     public class HeaderTouchListener implements View.OnTouchListener {
419 
420         private final ArrayList<Rect> mTouchRects = new ArrayList<>();
421         private Rect mExpandButtonRect;
422         private Rect mAppOpsRect;
423         private int mTouchSlop;
424         private boolean mTrackGesture;
425         private float mDownX;
426         private float mDownY;
427 
HeaderTouchListener()428         public HeaderTouchListener() {
429         }
430 
bindTouchRects()431         public void bindTouchRects() {
432             mTouchRects.clear();
433             addRectAroundView(mIcon);
434             mExpandButtonRect = addRectAroundView(mExpandButton);
435             mAppOpsRect = addRectAroundView(mAppOps);
436             addWidthRect();
437             mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
438         }
439 
addWidthRect()440         private void addWidthRect() {
441             Rect r = new Rect();
442             r.top = 0;
443             r.bottom = (int) (32 * getResources().getDisplayMetrics().density);
444             r.left = 0;
445             r.right = getWidth();
446             mTouchRects.add(r);
447         }
448 
addRectAroundView(View view)449         private Rect addRectAroundView(View view) {
450             final Rect r = getRectAroundView(view);
451             mTouchRects.add(r);
452             return r;
453         }
454 
getRectAroundView(View view)455         private Rect getRectAroundView(View view) {
456             float size = 48 * getResources().getDisplayMetrics().density;
457             float width = Math.max(size, view.getWidth());
458             float height = Math.max(size, view.getHeight());
459             final Rect r = new Rect();
460             if (view.getVisibility() == GONE) {
461                 view = getFirstChildNotGone();
462                 r.left = (int) (view.getLeft() - width / 2.0f);
463             } else {
464                 r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f);
465             }
466             r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f);
467             r.bottom = (int) (r.top + height);
468             r.right = (int) (r.left + width);
469             return r;
470         }
471 
472         @Override
onTouch(View v, MotionEvent event)473         public boolean onTouch(View v, MotionEvent event) {
474             float x = event.getX();
475             float y = event.getY();
476             switch (event.getActionMasked() & MotionEvent.ACTION_MASK) {
477                 case MotionEvent.ACTION_DOWN:
478                     mTrackGesture = false;
479                     if (isInside(x, y)) {
480                         mDownX = x;
481                         mDownY = y;
482                         mTrackGesture = true;
483                         return true;
484                     }
485                     break;
486                 case MotionEvent.ACTION_MOVE:
487                     if (mTrackGesture) {
488                         if (Math.abs(mDownX - x) > mTouchSlop
489                                 || Math.abs(mDownY - y) > mTouchSlop) {
490                             mTrackGesture = false;
491                         }
492                     }
493                     break;
494                 case MotionEvent.ACTION_UP:
495                     if (mTrackGesture) {
496                         if (mAppOps.isVisibleToUser() && (mAppOpsRect.contains((int) x, (int) y)
497                                 || mAppOpsRect.contains((int) mDownX, (int) mDownY))) {
498                             mAppOps.performClick();
499                             return true;
500                         }
501                         mExpandButton.performClick();
502                     }
503                     break;
504             }
505             return mTrackGesture;
506         }
507 
isInside(float x, float y)508         private boolean isInside(float x, float y) {
509             if (mAcceptAllTouches) {
510                 return true;
511             }
512             if (mExpandOnlyOnButton) {
513                 return mExpandButtonRect.contains((int) x, (int) y);
514             }
515             for (int i = 0; i < mTouchRects.size(); i++) {
516                 Rect r = mTouchRects.get(i);
517                 if (r.contains((int) x, (int) y)) {
518                     return true;
519                 }
520             }
521             return false;
522         }
523     }
524 
getFirstChildNotGone()525     private View getFirstChildNotGone() {
526         for (int i = 0; i < getChildCount(); i++) {
527             final View child = getChildAt(i);
528             if (child.getVisibility() != GONE) {
529                 return child;
530             }
531         }
532         return this;
533     }
534 
getExpandButton()535     public ImageView getExpandButton() {
536         return mExpandButton;
537     }
538 
539     @Override
hasOverlappingRendering()540     public boolean hasOverlappingRendering() {
541         return false;
542     }
543 
isInTouchRect(float x, float y)544     public boolean isInTouchRect(float x, float y) {
545         if (mExpandClickListener == null) {
546             return false;
547         }
548         return mTouchListener.isInside(x, y);
549     }
550 
551     /**
552      * Sets whether or not all touches to this header view will register as a click. Note that
553      * if the config value for {@code config_notificationHeaderClickableForExpand} is {@code true},
554      * then calling this method with {@code false} will not override that configuration.
555      */
556     @RemotableViewMethod
setAcceptAllTouches(boolean acceptAllTouches)557     public void setAcceptAllTouches(boolean acceptAllTouches) {
558         mAcceptAllTouches = mEntireHeaderClickable || acceptAllTouches;
559     }
560 
561     /**
562      * Sets whether only the expand icon itself should serve as the expand target.
563      */
564     @RemotableViewMethod
setExpandOnlyOnButton(boolean expandOnlyOnButton)565     public void setExpandOnlyOnButton(boolean expandOnlyOnButton) {
566         mExpandOnlyOnButton = expandOnlyOnButton;
567     }
568 }
569