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.AppOpsManager;
21import android.app.Notification;
22import android.content.Context;
23import android.content.res.Resources;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Outline;
27import android.graphics.Rect;
28import android.graphics.drawable.Drawable;
29import android.util.ArraySet;
30import android.util.AttributeSet;
31import android.widget.ImageView;
32import android.widget.RemoteViews;
33
34import com.android.internal.R;
35import com.android.internal.widget.CachingIconView;
36
37import java.util.ArrayList;
38
39/**
40 * A header of a notification view
41 *
42 * @hide
43 */
44@RemoteViews.RemoteView
45public 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
84    public NotificationHeaderView(Context context) {
85        this(context, null);
86    }
87
88    public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) {
89        this(context, attrs, 0);
90    }
91
92    public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
93        this(context, attrs, defStyleAttr, 0);
94    }
95
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
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
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
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
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
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     */
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
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
259    protected boolean verifyDrawable(Drawable who) {
260        return super.verifyDrawable(who) || who == mBackground;
261    }
262
263    @Override
264    protected void drawableStateChanged() {
265        if (mBackground != null && mBackground.isStateful()) {
266            mBackground.setState(getDrawableState());
267        }
268    }
269
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     */
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
292    public void setOnClickListener(@Nullable OnClickListener l) {
293        mExpandClickListener = l;
294        mExpandButton.setOnClickListener(mExpandClickListener);
295        updateTouchListener();
296    }
297
298    @RemotableViewMethod
299    public void setOriginalIconColor(int color) {
300        mIconColor = color;
301    }
302
303    public int getOriginalIconColor() {
304        return mIconColor;
305    }
306
307    @RemotableViewMethod
308    public void setOriginalNotificationColor(int color) {
309        mOriginalNotificationColor = color;
310    }
311
312    public int getOriginalNotificationColor() {
313        return mOriginalNotificationColor;
314    }
315
316    @RemotableViewMethod
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     */
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
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
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     */
365    public void setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd) {
366        if (showExpandButtonAtEnd != mShowExpandButtonAtEnd) {
367            setClipToPadding(!showExpandButtonAtEnd);
368            mShowExpandButtonAtEnd = showExpandButtonAtEnd;
369        }
370    }
371
372    public View getWorkProfileIcon() {
373        return mProfileBadge;
374    }
375
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
390        public HeaderTouchListener() {
391        }
392
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
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
411        private Rect addRectAroundView(View view) {
412            final Rect r = getRectAroundView(view);
413            mTouchRects.add(r);
414            return r;
415        }
416
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
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
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
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
497    public ImageView getExpandButton() {
498        return mExpandButton;
499    }
500
501    @Override
502    public boolean hasOverlappingRendering() {
503        return false;
504    }
505
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
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
527    public void setExpandOnlyOnButton(boolean expandOnlyOnButton) {
528        mExpandOnlyOnButton = expandOnlyOnButton;
529    }
530}
531