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