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