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.content.Context;
22 import android.graphics.Canvas;
23 import android.graphics.Outline;
24 import android.graphics.Rect;
25 import android.graphics.drawable.Drawable;
26 import android.util.AttributeSet;
27 import android.widget.ImageView;
28 import android.widget.RemoteViews;
29 
30 import com.android.internal.widget.CachingIconView;
31 
32 import java.util.ArrayList;
33 
34 /**
35  * A header of a notification view
36  *
37  * @hide
38  */
39 @RemoteViews.RemoteView
40 public class NotificationHeaderView extends ViewGroup {
41     public static final int NO_COLOR = Notification.COLOR_INVALID;
42     private final int mChildMinWidth;
43     private final int mContentEndMargin;
44     private View mAppName;
45     private View mHeaderText;
46     private OnClickListener mExpandClickListener;
47     private HeaderTouchListener mTouchListener = new HeaderTouchListener();
48     private ImageView mExpandButton;
49     private CachingIconView mIcon;
50     private View mProfileBadge;
51     private View mInfo;
52     private int mIconColor;
53     private int mOriginalNotificationColor;
54     private boolean mExpanded;
55     private boolean mShowWorkBadgeAtEnd;
56     private Drawable mBackground;
57     private int mHeaderBackgroundHeight;
58 
59     ViewOutlineProvider mProvider = new ViewOutlineProvider() {
60         @Override
61         public void getOutline(View view, Outline outline) {
62             if (mBackground != null) {
63                 outline.setRect(0, 0, getWidth(), mHeaderBackgroundHeight);
64                 outline.setAlpha(1f);
65             }
66         }
67     };
68     private boolean mAcceptAllTouches;
69 
NotificationHeaderView(Context context)70     public NotificationHeaderView(Context context) {
71         this(context, null);
72     }
73 
NotificationHeaderView(Context context, @Nullable AttributeSet attrs)74     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) {
75         this(context, attrs, 0);
76     }
77 
NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)78     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
79         this(context, attrs, defStyleAttr, 0);
80     }
81 
NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)82     public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
83         super(context, attrs, defStyleAttr, defStyleRes);
84         mChildMinWidth = getResources().getDimensionPixelSize(
85                 com.android.internal.R.dimen.notification_header_shrink_min_width);
86         mContentEndMargin = getResources().getDimensionPixelSize(
87                 com.android.internal.R.dimen.notification_content_margin_end);
88         mHeaderBackgroundHeight = getResources().getDimensionPixelSize(
89                 com.android.internal.R.dimen.notification_header_background_height);
90     }
91 
92     @Override
onFinishInflate()93     protected void onFinishInflate() {
94         super.onFinishInflate();
95         mAppName = findViewById(com.android.internal.R.id.app_name_text);
96         mHeaderText = findViewById(com.android.internal.R.id.header_text);
97         mExpandButton = findViewById(com.android.internal.R.id.expand_button);
98         mIcon = findViewById(com.android.internal.R.id.icon);
99         mProfileBadge = findViewById(com.android.internal.R.id.profile_badge);
100     }
101 
102     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)103     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
104         final int givenWidth = MeasureSpec.getSize(widthMeasureSpec);
105         final int givenHeight = MeasureSpec.getSize(heightMeasureSpec);
106         int wrapContentWidthSpec = MeasureSpec.makeMeasureSpec(givenWidth,
107                 MeasureSpec.AT_MOST);
108         int wrapContentHeightSpec = MeasureSpec.makeMeasureSpec(givenHeight,
109                 MeasureSpec.AT_MOST);
110         int totalWidth = getPaddingStart() + getPaddingEnd();
111         for (int i = 0; i < getChildCount(); i++) {
112             final View child = getChildAt(i);
113             if (child.getVisibility() == GONE) {
114                 // We'll give it the rest of the space in the end
115                 continue;
116             }
117             final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
118             int childWidthSpec = getChildMeasureSpec(wrapContentWidthSpec,
119                     lp.leftMargin + lp.rightMargin, lp.width);
120             int childHeightSpec = getChildMeasureSpec(wrapContentHeightSpec,
121                     lp.topMargin + lp.bottomMargin, lp.height);
122             child.measure(childWidthSpec, childHeightSpec);
123             totalWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
124         }
125         if (totalWidth > givenWidth) {
126             int overFlow = totalWidth - givenWidth;
127             // We are overflowing, lets shrink the app name first
128             final int appWidth = mAppName.getMeasuredWidth();
129             if (overFlow > 0 && mAppName.getVisibility() != GONE && appWidth > mChildMinWidth) {
130                 int newSize = appWidth - Math.min(appWidth - mChildMinWidth, overFlow);
131                 int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
132                 mAppName.measure(childWidthSpec, wrapContentHeightSpec);
133                 overFlow -= appWidth - newSize;
134             }
135             // still overflowing, finaly we shrink the header text
136             if (overFlow > 0 && mHeaderText.getVisibility() != GONE) {
137                 // we're still too big
138                 final int textWidth = mHeaderText.getMeasuredWidth();
139                 int newSize = Math.max(0, textWidth - overFlow);
140                 int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
141                 mHeaderText.measure(childWidthSpec, wrapContentHeightSpec);
142             }
143         }
144         setMeasuredDimension(givenWidth, givenHeight);
145     }
146 
147     @Override
onLayout(boolean changed, int l, int t, int r, int b)148     protected void onLayout(boolean changed, int l, int t, int r, int b) {
149         int left = getPaddingStart();
150         int childCount = getChildCount();
151         int ownHeight = getHeight() - getPaddingTop() - getPaddingBottom();
152         for (int i = 0; i < childCount; i++) {
153             View child = getChildAt(i);
154             if (child.getVisibility() == GONE) {
155                 continue;
156             }
157             int childHeight = child.getMeasuredHeight();
158             MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
159             left += params.getMarginStart();
160             int right = left + child.getMeasuredWidth();
161             int top = (int) (getPaddingTop() + (ownHeight - childHeight) / 2.0f);
162             int bottom = top + childHeight;
163             int layoutLeft = left;
164             int layoutRight = right;
165             if (child == mProfileBadge) {
166                 int paddingEnd = getPaddingEnd();
167                 if (mShowWorkBadgeAtEnd) {
168                     paddingEnd = mContentEndMargin;
169                 }
170                 layoutRight = getWidth() - paddingEnd;
171                 layoutLeft = layoutRight - child.getMeasuredWidth();
172             }
173             if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
174                 int ltrLeft = layoutLeft;
175                 layoutLeft = getWidth() - layoutRight;
176                 layoutRight = getWidth() - ltrLeft;
177             }
178             child.layout(layoutLeft, top, layoutRight, bottom);
179             left = right + params.getMarginEnd();
180         }
181         updateTouchListener();
182     }
183 
184     @Override
generateLayoutParams(AttributeSet attrs)185     public LayoutParams generateLayoutParams(AttributeSet attrs) {
186         return new ViewGroup.MarginLayoutParams(getContext(), attrs);
187     }
188 
189     /**
190      * Set a {@link Drawable} to be displayed as a background on the header.
191      */
setHeaderBackgroundDrawable(Drawable drawable)192     public void setHeaderBackgroundDrawable(Drawable drawable) {
193         if (drawable != null) {
194             setWillNotDraw(false);
195             mBackground = drawable;
196             mBackground.setCallback(this);
197             setOutlineProvider(mProvider);
198         } else {
199             setWillNotDraw(true);
200             mBackground = null;
201             setOutlineProvider(null);
202         }
203         invalidate();
204     }
205 
206     @Override
onDraw(Canvas canvas)207     protected void onDraw(Canvas canvas) {
208         if (mBackground != null) {
209             mBackground.setBounds(0, 0, getWidth(), mHeaderBackgroundHeight);
210             mBackground.draw(canvas);
211         }
212     }
213 
214     @Override
verifyDrawable(Drawable who)215     protected boolean verifyDrawable(Drawable who) {
216         return super.verifyDrawable(who) || who == mBackground;
217     }
218 
219     @Override
drawableStateChanged()220     protected void drawableStateChanged() {
221         if (mBackground != null && mBackground.isStateful()) {
222             mBackground.setState(getDrawableState());
223         }
224     }
225 
updateTouchListener()226     private void updateTouchListener() {
227         if (mExpandClickListener != null) {
228             mTouchListener.bindTouchRects();
229         }
230     }
231 
232     @Override
setOnClickListener(@ullable OnClickListener l)233     public void setOnClickListener(@Nullable OnClickListener l) {
234         mExpandClickListener = l;
235         setOnTouchListener(mExpandClickListener != null ? mTouchListener : null);
236         mExpandButton.setOnClickListener(mExpandClickListener);
237         updateTouchListener();
238     }
239 
240     @RemotableViewMethod
setOriginalIconColor(int color)241     public void setOriginalIconColor(int color) {
242         mIconColor = color;
243     }
244 
getOriginalIconColor()245     public int getOriginalIconColor() {
246         return mIconColor;
247     }
248 
249     @RemotableViewMethod
setOriginalNotificationColor(int color)250     public void setOriginalNotificationColor(int color) {
251         mOriginalNotificationColor = color;
252     }
253 
getOriginalNotificationColor()254     public int getOriginalNotificationColor() {
255         return mOriginalNotificationColor;
256     }
257 
258     @RemotableViewMethod
setExpanded(boolean expanded)259     public void setExpanded(boolean expanded) {
260         mExpanded = expanded;
261         updateExpandButton();
262     }
263 
updateExpandButton()264     private void updateExpandButton() {
265         int drawableId;
266         int contentDescriptionId;
267         if (mExpanded) {
268             drawableId = com.android.internal.R.drawable.ic_collapse_notification;
269             contentDescriptionId
270                     = com.android.internal.R.string.expand_button_content_description_expanded;
271         } else {
272             drawableId = com.android.internal.R.drawable.ic_expand_notification;
273             contentDescriptionId
274                     = com.android.internal.R.string.expand_button_content_description_collapsed;
275         }
276         mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
277         mExpandButton.setColorFilter(mOriginalNotificationColor);
278         mExpandButton.setContentDescription(mContext.getText(contentDescriptionId));
279     }
280 
setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd)281     public void setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd) {
282         if (showWorkBadgeAtEnd != mShowWorkBadgeAtEnd) {
283             setClipToPadding(!showWorkBadgeAtEnd);
284             mShowWorkBadgeAtEnd = showWorkBadgeAtEnd;
285         }
286     }
287 
getWorkProfileIcon()288     public View getWorkProfileIcon() {
289         return mProfileBadge;
290     }
291 
getIcon()292     public CachingIconView getIcon() {
293         return mIcon;
294     }
295 
296     public class HeaderTouchListener implements View.OnTouchListener {
297 
298         private final ArrayList<Rect> mTouchRects = new ArrayList<>();
299         private int mTouchSlop;
300         private boolean mTrackGesture;
301         private float mDownX;
302         private float mDownY;
303 
HeaderTouchListener()304         public HeaderTouchListener() {
305         }
306 
bindTouchRects()307         public void bindTouchRects() {
308             mTouchRects.clear();
309             addRectAroundViewView(mIcon);
310             addRectAroundViewView(mExpandButton);
311             addWidthRect();
312             mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
313         }
314 
addWidthRect()315         private void addWidthRect() {
316             Rect r = new Rect();
317             r.top = 0;
318             r.bottom = (int) (32 * getResources().getDisplayMetrics().density);
319             r.left = 0;
320             r.right = getWidth();
321             mTouchRects.add(r);
322         }
323 
addRectAroundViewView(View view)324         private void addRectAroundViewView(View view) {
325             final Rect r = getRectAroundView(view);
326             mTouchRects.add(r);
327         }
328 
getRectAroundView(View view)329         private Rect getRectAroundView(View view) {
330             float size = 48 * getResources().getDisplayMetrics().density;
331             final Rect r = new Rect();
332             if (view.getVisibility() == GONE) {
333                 view = getFirstChildNotGone();
334                 r.left = (int) (view.getLeft() - size / 2.0f);
335             } else {
336                 r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - size / 2.0f);
337             }
338             r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - size / 2.0f);
339             r.bottom = (int) (r.top + size);
340             r.right = (int) (r.left + size);
341             return r;
342         }
343 
344         @Override
onTouch(View v, MotionEvent event)345         public boolean onTouch(View v, MotionEvent event) {
346             float x = event.getX();
347             float y = event.getY();
348             switch (event.getActionMasked() & MotionEvent.ACTION_MASK) {
349                 case MotionEvent.ACTION_DOWN:
350                     mTrackGesture = false;
351                     if (isInside(x, y)) {
352                         mDownX = x;
353                         mDownY = y;
354                         mTrackGesture = true;
355                         return true;
356                     }
357                     break;
358                 case MotionEvent.ACTION_MOVE:
359                     if (mTrackGesture) {
360                         if (Math.abs(mDownX - x) > mTouchSlop
361                                 || Math.abs(mDownY - y) > mTouchSlop) {
362                             mTrackGesture = false;
363                         }
364                     }
365                     break;
366                 case MotionEvent.ACTION_UP:
367                     if (mTrackGesture) {
368                         mExpandButton.performClick();
369                     }
370                     break;
371             }
372             return mTrackGesture;
373         }
374 
isInside(float x, float y)375         private boolean isInside(float x, float y) {
376             if (mAcceptAllTouches) {
377                 return true;
378             }
379             for (int i = 0; i < mTouchRects.size(); i++) {
380                 Rect r = mTouchRects.get(i);
381                 if (r.contains((int) x, (int) y)) {
382                     return true;
383                 }
384             }
385             return false;
386         }
387     }
388 
getFirstChildNotGone()389     private View getFirstChildNotGone() {
390         for (int i = 0; i < getChildCount(); i++) {
391             final View child = getChildAt(i);
392             if (child.getVisibility() != GONE) {
393                 return child;
394             }
395         }
396         return this;
397     }
398 
getExpandButton()399     public ImageView getExpandButton() {
400         return mExpandButton;
401     }
402 
403     @Override
hasOverlappingRendering()404     public boolean hasOverlappingRendering() {
405         return false;
406     }
407 
isInTouchRect(float x, float y)408     public boolean isInTouchRect(float x, float y) {
409         if (mExpandClickListener == null) {
410             return false;
411         }
412         return mTouchListener.isInside(x, y);
413     }
414 
415     @RemotableViewMethod
setAcceptAllTouches(boolean acceptAllTouches)416     public void setAcceptAllTouches(boolean acceptAllTouches) {
417         mAcceptAllTouches = acceptAllTouches;
418     }
419 }
420