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.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.ArraySet;
30 import android.util.AttributeSet;
31 import android.widget.ImageView;
32 import android.widget.RemoteViews;
33 
34 import com.android.internal.R;
35 import com.android.internal.widget.CachingIconView;
36 
37 import java.util.ArrayList;
38 
39 /**
40  * A header of a notification view
41  *
42  * @hide
43  */
44 @RemoteViews.RemoteView
45 public class NotificationHeaderView extends ViewGroup {
46     public static final int NO_COLOR = Notification.COLOR_INVALID;
47     private final int mChildMinWidth;
48     private final int mContentEndMargin;
49     private final int mGravity;
50     private View mAppName;
51     private View mHeaderText;
52     private View mSecondaryHeaderText;
53     private OnClickListener mExpandClickListener;
54     private OnClickListener mAppOpsListener;
55     private HeaderTouchListener mTouchListener = new HeaderTouchListener();
56     private ImageView mExpandButton;
57     private CachingIconView mIcon;
58     private View mProfileBadge;
59     private View mOverlayIcon;
60     private View mCameraIcon;
61     private View mMicIcon;
62     private View mAppOps;
63     private int mIconColor;
64     private int mOriginalNotificationColor;
65     private boolean mExpanded;
66     private boolean mShowExpandButtonAtEnd;
67     private boolean mShowWorkBadgeAtEnd;
68     private Drawable mBackground;
69     private boolean mEntireHeaderClickable;
70     private boolean mExpandOnlyOnButton;
71     private boolean mAcceptAllTouches;
72     private int mTotalWidth;
73 
74     ViewOutlineProvider mProvider = new ViewOutlineProvider() {
75         @Override
76         public void getOutline(View view, Outline outline) {
77             if (mBackground != null) {
78                 outline.setRect(0, 0, getWidth(), getHeight());
79                 outline.setAlpha(1f);
80             }
81         }
82     };
83 
NotificationHeaderView(Context context)84     public NotificationHeaderView(Context context) {
85         this(context, null);
86     }
87 
NotificationHeaderView(Context context, @Nullable AttributeSet attrs)88     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) {
89         this(context, attrs, 0);
90     }
91 
NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)92     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
93         this(context, attrs, defStyleAttr, 0);
94     }
95 
NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)96     public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
97         super(context, attrs, defStyleAttr, defStyleRes);
98         Resources res = getResources();
99         mChildMinWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_min_width);
100         mContentEndMargin = res.getDimensionPixelSize(R.dimen.notification_content_margin_end);
101         mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand);
102 
103         int[] attrIds = { android.R.attr.gravity };
104         TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes);
105         mGravity = ta.getInt(0, 0);
106         ta.recycle();
107     }
108 
109     @Override
onFinishInflate()110     protected void onFinishInflate() {
111         super.onFinishInflate();
112         mAppName = findViewById(com.android.internal.R.id.app_name_text);
113         mHeaderText = findViewById(com.android.internal.R.id.header_text);
114         mSecondaryHeaderText = findViewById(com.android.internal.R.id.header_text_secondary);
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         mCameraIcon = findViewById(com.android.internal.R.id.camera);
119         mMicIcon = findViewById(com.android.internal.R.id.mic);
120         mOverlayIcon = findViewById(com.android.internal.R.id.overlay);
121         mAppOps = findViewById(com.android.internal.R.id.app_ops);
122     }
123 
124     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)125     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
126         final int givenWidth = MeasureSpec.getSize(widthMeasureSpec);
127         final int givenHeight = MeasureSpec.getSize(heightMeasureSpec);
128         int wrapContentWidthSpec = MeasureSpec.makeMeasureSpec(givenWidth,
129                 MeasureSpec.AT_MOST);
130         int wrapContentHeightSpec = MeasureSpec.makeMeasureSpec(givenHeight,
131                 MeasureSpec.AT_MOST);
132         int totalWidth = getPaddingStart() + getPaddingEnd();
133         for (int i = 0; i < getChildCount(); i++) {
134             final View child = getChildAt(i);
135             if (child.getVisibility() == GONE) {
136                 // We'll give it the rest of the space in the end
137                 continue;
138             }
139             final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
140             int childWidthSpec = getChildMeasureSpec(wrapContentWidthSpec,
141                     lp.leftMargin + lp.rightMargin, lp.width);
142             int childHeightSpec = getChildMeasureSpec(wrapContentHeightSpec,
143                     lp.topMargin + lp.bottomMargin, lp.height);
144             child.measure(childWidthSpec, childHeightSpec);
145             totalWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
146         }
147         if (totalWidth > givenWidth) {
148             int overFlow = totalWidth - givenWidth;
149             // We are overflowing, lets shrink the app name first
150             overFlow = shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mAppName,
151                     mChildMinWidth);
152 
153             // still overflowing, we shrink the header text
154             overFlow = shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mHeaderText, 0);
155 
156             // still overflowing, finally we shrink the secondary header text
157             shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mSecondaryHeaderText,
158                     0);
159         }
160         mTotalWidth = Math.min(totalWidth, givenWidth);
161         setMeasuredDimension(givenWidth, givenHeight);
162     }
163 
shrinkViewForOverflow(int heightSpec, int overFlow, View targetView, int minimumWidth)164     private int shrinkViewForOverflow(int heightSpec, int overFlow, View targetView,
165             int minimumWidth) {
166         final int oldWidth = targetView.getMeasuredWidth();
167         if (overFlow > 0 && targetView.getVisibility() != GONE && oldWidth > minimumWidth) {
168             // we're still too big
169             int newSize = Math.max(minimumWidth, oldWidth - overFlow);
170             int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
171             targetView.measure(childWidthSpec, heightSpec);
172             overFlow -= oldWidth - newSize;
173         }
174         return overFlow;
175     }
176 
177     @Override
onLayout(boolean changed, int l, int t, int r, int b)178     protected void onLayout(boolean changed, int l, int t, int r, int b) {
179         int left = getPaddingStart();
180         int end = getMeasuredWidth();
181         final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
182         if (centerAligned) {
183             left += getMeasuredWidth() / 2 - mTotalWidth / 2;
184         }
185         int childCount = getChildCount();
186         int ownHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
187         for (int i = 0; i < childCount; i++) {
188             View child = getChildAt(i);
189             if (child.getVisibility() == GONE) {
190                 continue;
191             }
192             int childHeight = child.getMeasuredHeight();
193             MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
194             left += params.getMarginStart();
195             int right = left + child.getMeasuredWidth();
196             int top = (int) (getPaddingTop() + (ownHeight - childHeight) / 2.0f);
197             int bottom = top + childHeight;
198             int layoutLeft = left;
199             int layoutRight = right;
200             if (child == mExpandButton && mShowExpandButtonAtEnd) {
201                 layoutRight = end - mContentEndMargin;
202                 end = layoutLeft = layoutRight - child.getMeasuredWidth();
203             }
204             if (child == mProfileBadge) {
205                 int paddingEnd = getPaddingEnd();
206                 if (mShowWorkBadgeAtEnd) {
207                     paddingEnd = mContentEndMargin;
208                 }
209                 layoutRight = end - paddingEnd;
210                 end = layoutLeft = layoutRight - child.getMeasuredWidth();
211             }
212             if (child == mAppOps) {
213                 int paddingEnd = mContentEndMargin;
214                 layoutRight = end - paddingEnd;
215                 end = layoutLeft = layoutRight - child.getMeasuredWidth();
216             }
217             if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
218                 int ltrLeft = layoutLeft;
219                 layoutLeft = getWidth() - layoutRight;
220                 layoutRight = getWidth() - ltrLeft;
221             }
222             child.layout(layoutLeft, top, layoutRight, bottom);
223             left = right + params.getMarginEnd();
224         }
225         updateTouchListener();
226     }
227 
228     @Override
generateLayoutParams(AttributeSet attrs)229     public LayoutParams generateLayoutParams(AttributeSet attrs) {
230         return new ViewGroup.MarginLayoutParams(getContext(), attrs);
231     }
232 
233     /**
234      * Set a {@link Drawable} to be displayed as a background on the header.
235      */
setHeaderBackgroundDrawable(Drawable drawable)236     public void setHeaderBackgroundDrawable(Drawable drawable) {
237         if (drawable != null) {
238             setWillNotDraw(false);
239             mBackground = drawable;
240             mBackground.setCallback(this);
241             setOutlineProvider(mProvider);
242         } else {
243             setWillNotDraw(true);
244             mBackground = null;
245             setOutlineProvider(null);
246         }
247         invalidate();
248     }
249 
250     @Override
onDraw(Canvas canvas)251     protected void onDraw(Canvas canvas) {
252         if (mBackground != null) {
253             mBackground.setBounds(0, 0, getWidth(), getHeight());
254             mBackground.draw(canvas);
255         }
256     }
257 
258     @Override
verifyDrawable(Drawable who)259     protected boolean verifyDrawable(Drawable who) {
260         return super.verifyDrawable(who) || who == mBackground;
261     }
262 
263     @Override
drawableStateChanged()264     protected void drawableStateChanged() {
265         if (mBackground != null && mBackground.isStateful()) {
266             mBackground.setState(getDrawableState());
267         }
268     }
269 
updateTouchListener()270     private void updateTouchListener() {
271         if (mExpandClickListener == null && mAppOpsListener == null) {
272             setOnTouchListener(null);
273             return;
274         }
275         setOnTouchListener(mTouchListener);
276         mTouchListener.bindTouchRects();
277     }
278 
279     /**
280      * Sets onclick listener for app ops icons.
281      */
setAppOpsOnClickListener(OnClickListener l)282     public void setAppOpsOnClickListener(OnClickListener l) {
283         mAppOpsListener = l;
284         mAppOps.setOnClickListener(mAppOpsListener);
285         mCameraIcon.setOnClickListener(mAppOpsListener);
286         mMicIcon.setOnClickListener(mAppOpsListener);
287         mOverlayIcon.setOnClickListener(mAppOpsListener);
288         updateTouchListener();
289     }
290 
291     @Override
setOnClickListener(@ullable OnClickListener l)292     public void setOnClickListener(@Nullable OnClickListener l) {
293         mExpandClickListener = l;
294         mExpandButton.setOnClickListener(mExpandClickListener);
295         updateTouchListener();
296     }
297 
298     @RemotableViewMethod
setOriginalIconColor(int color)299     public void setOriginalIconColor(int color) {
300         mIconColor = color;
301     }
302 
getOriginalIconColor()303     public int getOriginalIconColor() {
304         return mIconColor;
305     }
306 
307     @RemotableViewMethod
setOriginalNotificationColor(int color)308     public void setOriginalNotificationColor(int color) {
309         mOriginalNotificationColor = color;
310     }
311 
getOriginalNotificationColor()312     public int getOriginalNotificationColor() {
313         return mOriginalNotificationColor;
314     }
315 
316     @RemotableViewMethod
setExpanded(boolean expanded)317     public void setExpanded(boolean expanded) {
318         mExpanded = expanded;
319         updateExpandButton();
320     }
321 
322     /**
323      * Shows or hides 'app op in use' icons based on app usage.
324      */
showAppOpsIcons(ArraySet<Integer> appOps)325     public void showAppOpsIcons(ArraySet<Integer> appOps) {
326         if (mOverlayIcon == null || mCameraIcon == null || mMicIcon == null || appOps == null) {
327             return;
328         }
329 
330         mOverlayIcon.setVisibility(appOps.contains(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
331                 ? View.VISIBLE : View.GONE);
332         mCameraIcon.setVisibility(appOps.contains(AppOpsManager.OP_CAMERA)
333                 ? View.VISIBLE : View.GONE);
334         mMicIcon.setVisibility(appOps.contains(AppOpsManager.OP_RECORD_AUDIO)
335                 ? View.VISIBLE : View.GONE);
336     }
337 
updateExpandButton()338     private void updateExpandButton() {
339         int drawableId;
340         int contentDescriptionId;
341         if (mExpanded) {
342             drawableId = R.drawable.ic_collapse_notification;
343             contentDescriptionId = R.string.expand_button_content_description_expanded;
344         } else {
345             drawableId = R.drawable.ic_expand_notification;
346             contentDescriptionId = R.string.expand_button_content_description_collapsed;
347         }
348         mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
349         mExpandButton.setColorFilter(mOriginalNotificationColor);
350         mExpandButton.setContentDescription(mContext.getText(contentDescriptionId));
351     }
352 
setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd)353     public void setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd) {
354         if (showWorkBadgeAtEnd != mShowWorkBadgeAtEnd) {
355             setClipToPadding(!showWorkBadgeAtEnd);
356             mShowWorkBadgeAtEnd = showWorkBadgeAtEnd;
357         }
358     }
359 
360     /**
361      * Sets whether or not the expand button appears at the end of the NotificationHeaderView. If
362      * both this and {@link #setShowWorkBadgeAtEnd(boolean)} have been set to true, then the
363      * expand button will appear closer to the end than the work badge.
364      */
setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd)365     public void setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd) {
366         if (showExpandButtonAtEnd != mShowExpandButtonAtEnd) {
367             setClipToPadding(!showExpandButtonAtEnd);
368             mShowExpandButtonAtEnd = showExpandButtonAtEnd;
369         }
370     }
371 
getWorkProfileIcon()372     public View getWorkProfileIcon() {
373         return mProfileBadge;
374     }
375 
getIcon()376     public CachingIconView getIcon() {
377         return mIcon;
378     }
379 
380     public class HeaderTouchListener implements View.OnTouchListener {
381 
382         private final ArrayList<Rect> mTouchRects = new ArrayList<>();
383         private Rect mExpandButtonRect;
384         private Rect mAppOpsRect;
385         private int mTouchSlop;
386         private boolean mTrackGesture;
387         private float mDownX;
388         private float mDownY;
389 
HeaderTouchListener()390         public HeaderTouchListener() {
391         }
392 
bindTouchRects()393         public void bindTouchRects() {
394             mTouchRects.clear();
395             addRectAroundView(mIcon);
396             mExpandButtonRect = addRectAroundView(mExpandButton);
397             mAppOpsRect = addRectAroundView(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