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.graphics.Canvas;
23import android.graphics.Outline;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
26import android.util.AttributeSet;
27import android.widget.ImageView;
28import android.widget.RemoteViews;
29
30import com.android.internal.widget.CachingIconView;
31
32import java.util.ArrayList;
33
34/**
35 * A header of a notification view
36 *
37 * @hide
38 */
39@RemoteViews.RemoteView
40public 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
70    public NotificationHeaderView(Context context) {
71        this(context, null);
72    }
73
74    public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) {
75        this(context, attrs, 0);
76    }
77
78    public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
79        this(context, attrs, defStyleAttr, 0);
80    }
81
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
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
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
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
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     */
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
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
215    protected boolean verifyDrawable(Drawable who) {
216        return super.verifyDrawable(who) || who == mBackground;
217    }
218
219    @Override
220    protected void drawableStateChanged() {
221        if (mBackground != null && mBackground.isStateful()) {
222            mBackground.setState(getDrawableState());
223        }
224    }
225
226    private void updateTouchListener() {
227        if (mExpandClickListener != null) {
228            mTouchListener.bindTouchRects();
229        }
230    }
231
232    @Override
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
241    public void setOriginalIconColor(int color) {
242        mIconColor = color;
243    }
244
245    public int getOriginalIconColor() {
246        return mIconColor;
247    }
248
249    @RemotableViewMethod
250    public void setOriginalNotificationColor(int color) {
251        mOriginalNotificationColor = color;
252    }
253
254    public int getOriginalNotificationColor() {
255        return mOriginalNotificationColor;
256    }
257
258    @RemotableViewMethod
259    public void setExpanded(boolean expanded) {
260        mExpanded = expanded;
261        updateExpandButton();
262    }
263
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
281    public void setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd) {
282        if (showWorkBadgeAtEnd != mShowWorkBadgeAtEnd) {
283            setClipToPadding(!showWorkBadgeAtEnd);
284            mShowWorkBadgeAtEnd = showWorkBadgeAtEnd;
285        }
286    }
287
288    public View getWorkProfileIcon() {
289        return mProfileBadge;
290    }
291
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
304        public HeaderTouchListener() {
305        }
306
307        public void bindTouchRects() {
308            mTouchRects.clear();
309            addRectAroundViewView(mIcon);
310            addRectAroundViewView(mExpandButton);
311            addWidthRect();
312            mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
313        }
314
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
324        private void addRectAroundViewView(View view) {
325            final Rect r = getRectAroundView(view);
326            mTouchRects.add(r);
327        }
328
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
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
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
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
399    public ImageView getExpandButton() {
400        return mExpandButton;
401    }
402
403    @Override
404    public boolean hasOverlappingRendering() {
405        return false;
406    }
407
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
416    public void setAcceptAllTouches(boolean acceptAllTouches) {
417        mAcceptAllTouches = acceptAllTouches;
418    }
419}
420