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