Home | History | Annotate | Download | only in view
      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.annotation.UnsupportedAppUsage;
     21 import android.app.AppOpsManager;
     22 import android.app.Notification;
     23 import android.content.Context;
     24 import android.content.res.Resources;
     25 import android.content.res.TypedArray;
     26 import android.graphics.Canvas;
     27 import android.graphics.Outline;
     28 import android.graphics.Rect;
     29 import android.graphics.drawable.Drawable;
     30 import android.util.ArraySet;
     31 import android.util.AttributeSet;
     32 import android.widget.ImageView;
     33 import android.widget.RemoteViews;
     34 
     35 import com.android.internal.R;
     36 import com.android.internal.widget.CachingIconView;
     37 
     38 import java.util.ArrayList;
     39 
     40 /**
     41  * A header of a notification view
     42  *
     43  * @hide
     44  */
     45 @RemoteViews.RemoteView
     46 public class NotificationHeaderView extends ViewGroup {
     47     public static final int NO_COLOR = Notification.COLOR_INVALID;
     48     private final int mChildMinWidth;
     49     private final int mContentEndMargin;
     50     private final int mGravity;
     51     private View mAppName;
     52     private View mHeaderText;
     53     private View mSecondaryHeaderText;
     54     private OnClickListener mExpandClickListener;
     55     private OnClickListener mAppOpsListener;
     56     private HeaderTouchListener mTouchListener = new HeaderTouchListener();
     57     private ImageView mExpandButton;
     58     private CachingIconView mIcon;
     59     private View mProfileBadge;
     60     private View mOverlayIcon;
     61     private View mCameraIcon;
     62     private View mMicIcon;
     63     private View mAppOps;
     64     private View mAudiblyAlertedIcon;
     65     private int mIconColor;
     66     private int mOriginalNotificationColor;
     67     private boolean mExpanded;
     68     private boolean mShowExpandButtonAtEnd;
     69     private boolean mShowWorkBadgeAtEnd;
     70     private int mHeaderTextMarginEnd;
     71     private Drawable mBackground;
     72     private boolean mEntireHeaderClickable;
     73     private boolean mExpandOnlyOnButton;
     74     private boolean mAcceptAllTouches;
     75     private int mTotalWidth;
     76 
     77     ViewOutlineProvider mProvider = new ViewOutlineProvider() {
     78         @Override
     79         public void getOutline(View view, Outline outline) {
     80             if (mBackground != null) {
     81                 outline.setRect(0, 0, getWidth(), getHeight());
     82                 outline.setAlpha(1f);
     83             }
     84         }
     85     };
     86 
     87     public NotificationHeaderView(Context context) {
     88         this(context, null);
     89     }
     90 
     91     @UnsupportedAppUsage
     92     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) {
     93         this(context, attrs, 0);
     94     }
     95 
     96     public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
     97         this(context, attrs, defStyleAttr, 0);
     98     }
     99 
    100     public NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    101         super(context, attrs, defStyleAttr, defStyleRes);
    102         Resources res = getResources();
    103         mChildMinWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_min_width);
    104         mContentEndMargin = res.getDimensionPixelSize(R.dimen.notification_content_margin_end);
    105         mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand);
    106 
    107         int[] attrIds = { android.R.attr.gravity };
    108         TypedArray ta = context.obtainStyledAttributes(attrs, attrIds, defStyleAttr, defStyleRes);
    109         mGravity = ta.getInt(0, 0);
    110         ta.recycle();
    111     }
    112 
    113     @Override
    114     protected void onFinishInflate() {
    115         super.onFinishInflate();
    116         mAppName = findViewById(com.android.internal.R.id.app_name_text);
    117         mHeaderText = findViewById(com.android.internal.R.id.header_text);
    118         mSecondaryHeaderText = findViewById(com.android.internal.R.id.header_text_secondary);
    119         mExpandButton = findViewById(com.android.internal.R.id.expand_button);
    120         mIcon = findViewById(com.android.internal.R.id.icon);
    121         mProfileBadge = findViewById(com.android.internal.R.id.profile_badge);
    122         mCameraIcon = findViewById(com.android.internal.R.id.camera);
    123         mMicIcon = findViewById(com.android.internal.R.id.mic);
    124         mOverlayIcon = findViewById(com.android.internal.R.id.overlay);
    125         mAppOps = findViewById(com.android.internal.R.id.app_ops);
    126         mAudiblyAlertedIcon = findViewById(com.android.internal.R.id.alerted_icon);
    127     }
    128 
    129     @Override
    130     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    131         final int givenWidth = MeasureSpec.getSize(widthMeasureSpec);
    132         final int givenHeight = MeasureSpec.getSize(heightMeasureSpec);
    133         int wrapContentWidthSpec = MeasureSpec.makeMeasureSpec(givenWidth,
    134                 MeasureSpec.AT_MOST);
    135         int wrapContentHeightSpec = MeasureSpec.makeMeasureSpec(givenHeight,
    136                 MeasureSpec.AT_MOST);
    137         int totalWidth = getPaddingStart();
    138         int iconWidth = 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             if ((child == mExpandButton && mShowExpandButtonAtEnd)
    152                     || child == mProfileBadge
    153                     || child == mAppOps) {
    154                 iconWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
    155             } else {
    156                 totalWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
    157             }
    158         }
    159 
    160         // Ensure that there is at least enough space for the icons
    161         int endMargin = Math.max(mHeaderTextMarginEnd, iconWidth);
    162         if (totalWidth > givenWidth - endMargin) {
    163             int overFlow = totalWidth - givenWidth + endMargin;
    164             // We are overflowing, lets shrink the app name first
    165             overFlow = shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mAppName,
    166                     mChildMinWidth);
    167 
    168             // still overflowing, we shrink the header text
    169             overFlow = shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mHeaderText, 0);
    170 
    171             // still overflowing, finally we shrink the secondary header text
    172             shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mSecondaryHeaderText,
    173                     0);
    174         }
    175         totalWidth += getPaddingEnd();
    176         mTotalWidth = Math.min(totalWidth, givenWidth);
    177         setMeasuredDimension(givenWidth, givenHeight);
    178     }
    179 
    180     private int shrinkViewForOverflow(int heightSpec, int overFlow, View targetView,
    181             int minimumWidth) {
    182         final int oldWidth = targetView.getMeasuredWidth();
    183         if (overFlow > 0 && targetView.getVisibility() != GONE && oldWidth > minimumWidth) {
    184             // we're still too big
    185             int newSize = Math.max(minimumWidth, oldWidth - overFlow);
    186             int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
    187             targetView.measure(childWidthSpec, heightSpec);
    188             overFlow -= oldWidth - newSize;
    189         }
    190         return overFlow;
    191     }
    192 
    193     @Override
    194     protected void onLayout(boolean changed, int l, int t, int r, int b) {
    195         int left = getPaddingStart();
    196         int end = getMeasuredWidth();
    197         final boolean centerAligned = (mGravity & Gravity.CENTER_HORIZONTAL) != 0;
    198         if (centerAligned) {
    199             left += getMeasuredWidth() / 2 - mTotalWidth / 2;
    200         }
    201         int childCount = getChildCount();
    202         int ownHeight = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();
    203         for (int i = 0; i < childCount; i++) {
    204             View child = getChildAt(i);
    205             if (child.getVisibility() == GONE) {
    206                 continue;
    207             }
    208             int childHeight = child.getMeasuredHeight();
    209             MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
    210             int layoutLeft;
    211             int layoutRight;
    212             int top = (int) (getPaddingTop() + (ownHeight - childHeight) / 2.0f);
    213             int bottom = top + childHeight;
    214             if ((child == mExpandButton && mShowExpandButtonAtEnd)
    215                     || child == mProfileBadge
    216                     || child == mAppOps) {
    217                 if (end == getMeasuredWidth()) {
    218                     layoutRight = end - mContentEndMargin;
    219                 } else {
    220                     layoutRight = end - params.getMarginEnd();
    221                 }
    222                 layoutLeft = layoutRight - child.getMeasuredWidth();
    223                 end = layoutLeft - params.getMarginStart();
    224             } else {
    225                 left += params.getMarginStart();
    226                 int right = left + child.getMeasuredWidth();
    227                 layoutLeft = left;
    228                 layoutRight = right;
    229                 left = right + params.getMarginEnd();
    230             }
    231             if (getLayoutDirection() == LAYOUT_DIRECTION_RTL) {
    232                 int ltrLeft = layoutLeft;
    233                 layoutLeft = getWidth() - layoutRight;
    234                 layoutRight = getWidth() - ltrLeft;
    235             }
    236             child.layout(layoutLeft, top, layoutRight, bottom);
    237         }
    238         updateTouchListener();
    239     }
    240 
    241     @Override
    242     public LayoutParams generateLayoutParams(AttributeSet attrs) {
    243         return new ViewGroup.MarginLayoutParams(getContext(), attrs);
    244     }
    245 
    246     /**
    247      * Set a {@link Drawable} to be displayed as a background on the header.
    248      */
    249     public void setHeaderBackgroundDrawable(Drawable drawable) {
    250         if (drawable != null) {
    251             setWillNotDraw(false);
    252             mBackground = drawable;
    253             mBackground.setCallback(this);
    254             setOutlineProvider(mProvider);
    255         } else {
    256             setWillNotDraw(true);
    257             mBackground = null;
    258             setOutlineProvider(null);
    259         }
    260         invalidate();
    261     }
    262 
    263     @Override
    264     protected void onDraw(Canvas canvas) {
    265         if (mBackground != null) {
    266             mBackground.setBounds(0, 0, getWidth(), getHeight());
    267             mBackground.draw(canvas);
    268         }
    269     }
    270 
    271     @Override
    272     protected boolean verifyDrawable(Drawable who) {
    273         return super.verifyDrawable(who) || who == mBackground;
    274     }
    275 
    276     @Override
    277     protected void drawableStateChanged() {
    278         if (mBackground != null && mBackground.isStateful()) {
    279             mBackground.setState(getDrawableState());
    280         }
    281     }
    282 
    283     private void updateTouchListener() {
    284         if (mExpandClickListener == null && mAppOpsListener == null) {
    285             setOnTouchListener(null);
    286             return;
    287         }
    288         setOnTouchListener(mTouchListener);
    289         mTouchListener.bindTouchRects();
    290     }
    291 
    292     /**
    293      * Sets onclick listener for app ops icons.
    294      */
    295     public void setAppOpsOnClickListener(OnClickListener l) {
    296         mAppOpsListener = l;
    297         mAppOps.setOnClickListener(mAppOpsListener);
    298         mCameraIcon.setOnClickListener(mAppOpsListener);
    299         mMicIcon.setOnClickListener(mAppOpsListener);
    300         mOverlayIcon.setOnClickListener(mAppOpsListener);
    301         updateTouchListener();
    302     }
    303 
    304     @Override
    305     public void setOnClickListener(@Nullable OnClickListener l) {
    306         mExpandClickListener = l;
    307         mExpandButton.setOnClickListener(mExpandClickListener);
    308         updateTouchListener();
    309     }
    310 
    311     @RemotableViewMethod
    312     public void setOriginalIconColor(int color) {
    313         mIconColor = color;
    314     }
    315 
    316     public int getOriginalIconColor() {
    317         return mIconColor;
    318     }
    319 
    320     @RemotableViewMethod
    321     public void setOriginalNotificationColor(int color) {
    322         mOriginalNotificationColor = color;
    323     }
    324 
    325     public int getOriginalNotificationColor() {
    326         return mOriginalNotificationColor;
    327     }
    328 
    329     @RemotableViewMethod
    330     public void setExpanded(boolean expanded) {
    331         mExpanded = expanded;
    332         updateExpandButton();
    333     }
    334 
    335     /**
    336      * Shows or hides 'app op in use' icons based on app usage.
    337      */
    338     public void showAppOpsIcons(ArraySet<Integer> appOps) {
    339         if (mOverlayIcon == null || mCameraIcon == null || mMicIcon == null || appOps == null) {
    340             return;
    341         }
    342 
    343         mOverlayIcon.setVisibility(appOps.contains(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
    344                 ? View.VISIBLE : View.GONE);
    345         mCameraIcon.setVisibility(appOps.contains(AppOpsManager.OP_CAMERA)
    346                 ? View.VISIBLE : View.GONE);
    347         mMicIcon.setVisibility(appOps.contains(AppOpsManager.OP_RECORD_AUDIO)
    348                 ? View.VISIBLE : View.GONE);
    349     }
    350 
    351     /** Updates icon visibility based on the noisiness of the notification. */
    352     public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) {
    353         mAudiblyAlertedIcon.setVisibility(audiblyAlerted ? View.VISIBLE : View.GONE);
    354     }
    355 
    356     private void updateExpandButton() {
    357         int drawableId;
    358         int contentDescriptionId;
    359         if (mExpanded) {
    360             drawableId = R.drawable.ic_collapse_notification;
    361             contentDescriptionId = R.string.expand_button_content_description_expanded;
    362         } else {
    363             drawableId = R.drawable.ic_expand_notification;
    364             contentDescriptionId = R.string.expand_button_content_description_collapsed;
    365         }
    366         mExpandButton.setImageDrawable(getContext().getDrawable(drawableId));
    367         mExpandButton.setColorFilter(mOriginalNotificationColor);
    368         mExpandButton.setContentDescription(mContext.getText(contentDescriptionId));
    369     }
    370 
    371     public void setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd) {
    372         if (showWorkBadgeAtEnd != mShowWorkBadgeAtEnd) {
    373             setClipToPadding(!showWorkBadgeAtEnd);
    374             mShowWorkBadgeAtEnd = showWorkBadgeAtEnd;
    375         }
    376     }
    377 
    378     /**
    379      * Sets whether or not the expand button appears at the end of the NotificationHeaderView. If
    380      * both this and {@link #setShowWorkBadgeAtEnd(boolean)} have been set to true, then the
    381      * expand button will appear closer to the end than the work badge.
    382      */
    383     public void setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd) {
    384         if (showExpandButtonAtEnd != mShowExpandButtonAtEnd) {
    385             setClipToPadding(!showExpandButtonAtEnd);
    386             mShowExpandButtonAtEnd = showExpandButtonAtEnd;
    387         }
    388     }
    389 
    390     public View getWorkProfileIcon() {
    391         return mProfileBadge;
    392     }
    393 
    394     public CachingIconView getIcon() {
    395         return mIcon;
    396     }
    397 
    398     /**
    399      * Sets the margin end for the text portion of the header, excluding right-aligned elements
    400      * @param headerTextMarginEnd margin size
    401      */
    402     @RemotableViewMethod
    403     public void setHeaderTextMarginEnd(int headerTextMarginEnd) {
    404         if (mHeaderTextMarginEnd != headerTextMarginEnd) {
    405             mHeaderTextMarginEnd = headerTextMarginEnd;
    406             requestLayout();
    407         }
    408     }
    409 
    410     /**
    411      * Get the current margin end value for the header text
    412      * @return margin size
    413      */
    414     public int getHeaderTextMarginEnd() {
    415         return mHeaderTextMarginEnd;
    416     }
    417 
    418     public class HeaderTouchListener implements View.OnTouchListener {
    419 
    420         private final ArrayList<Rect> mTouchRects = new ArrayList<>();
    421         private Rect mExpandButtonRect;
    422         private Rect mAppOpsRect;
    423         private int mTouchSlop;
    424         private boolean mTrackGesture;
    425         private float mDownX;
    426         private float mDownY;
    427 
    428         public HeaderTouchListener() {
    429         }
    430 
    431         public void bindTouchRects() {
    432             mTouchRects.clear();
    433             addRectAroundView(mIcon);
    434             mExpandButtonRect = addRectAroundView(mExpandButton);
    435             mAppOpsRect = addRectAroundView(mAppOps);
    436             addWidthRect();
    437             mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    438         }
    439 
    440         private void addWidthRect() {
    441             Rect r = new Rect();
    442             r.top = 0;
    443             r.bottom = (int) (32 * getResources().getDisplayMetrics().density);
    444             r.left = 0;
    445             r.right = getWidth();
    446             mTouchRects.add(r);
    447         }
    448 
    449         private Rect addRectAroundView(View view) {
    450             final Rect r = getRectAroundView(view);
    451             mTouchRects.add(r);
    452             return r;
    453         }
    454 
    455         private Rect getRectAroundView(View view) {
    456             float size = 48 * getResources().getDisplayMetrics().density;
    457             float width = Math.max(size, view.getWidth());
    458             float height = Math.max(size, view.getHeight());
    459             final Rect r = new Rect();
    460             if (view.getVisibility() == GONE) {
    461                 view = getFirstChildNotGone();
    462                 r.left = (int) (view.getLeft() - width / 2.0f);
    463             } else {
    464                 r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - width / 2.0f);
    465             }
    466             r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - height / 2.0f);
    467             r.bottom = (int) (r.top + height);
    468             r.right = (int) (r.left + width);
    469             return r;
    470         }
    471 
    472         @Override
    473         public boolean onTouch(View v, MotionEvent event) {
    474             float x = event.getX();
    475             float y = event.getY();
    476             switch (event.getActionMasked() & MotionEvent.ACTION_MASK) {
    477                 case MotionEvent.ACTION_DOWN:
    478                     mTrackGesture = false;
    479                     if (isInside(x, y)) {
    480                         mDownX = x;
    481                         mDownY = y;
    482                         mTrackGesture = true;
    483                         return true;
    484                     }
    485                     break;
    486                 case MotionEvent.ACTION_MOVE:
    487                     if (mTrackGesture) {
    488                         if (Math.abs(mDownX - x) > mTouchSlop
    489                                 || Math.abs(mDownY - y) > mTouchSlop) {
    490                             mTrackGesture = false;
    491                         }
    492                     }
    493                     break;
    494                 case MotionEvent.ACTION_UP:
    495                     if (mTrackGesture) {
    496                         if (mAppOps.isVisibleToUser() && (mAppOpsRect.contains((int) x, (int) y)
    497                                 || mAppOpsRect.contains((int) mDownX, (int) mDownY))) {
    498                             mAppOps.performClick();
    499                             return true;
    500                         }
    501                         mExpandButton.performClick();
    502                     }
    503                     break;
    504             }
    505             return mTrackGesture;
    506         }
    507 
    508         private boolean isInside(float x, float y) {
    509             if (mAcceptAllTouches) {
    510                 return true;
    511             }
    512             if (mExpandOnlyOnButton) {
    513                 return mExpandButtonRect.contains((int) x, (int) y);
    514             }
    515             for (int i = 0; i < mTouchRects.size(); i++) {
    516                 Rect r = mTouchRects.get(i);
    517                 if (r.contains((int) x, (int) y)) {
    518                     return true;
    519                 }
    520             }
    521             return false;
    522         }
    523     }
    524 
    525     private View getFirstChildNotGone() {
    526         for (int i = 0; i < getChildCount(); i++) {
    527             final View child = getChildAt(i);
    528             if (child.getVisibility() != GONE) {
    529                 return child;
    530             }
    531         }
    532         return this;
    533     }
    534 
    535     public ImageView getExpandButton() {
    536         return mExpandButton;
    537     }
    538 
    539     @Override
    540     public boolean hasOverlappingRendering() {
    541         return false;
    542     }
    543 
    544     public boolean isInTouchRect(float x, float y) {
    545         if (mExpandClickListener == null) {
    546             return false;
    547         }
    548         return mTouchListener.isInside(x, y);
    549     }
    550 
    551     /**
    552      * Sets whether or not all touches to this header view will register as a click. Note that
    553      * if the config value for {@code config_notificationHeaderClickableForExpand} is {@code true},
    554      * then calling this method with {@code false} will not override that configuration.
    555      */
    556     @RemotableViewMethod
    557     public void setAcceptAllTouches(boolean acceptAllTouches) {
    558         mAcceptAllTouches = mEntireHeaderClickable || acceptAllTouches;
    559     }
    560 
    561     /**
    562      * Sets whether only the expand icon itself should serve as the expand target.
    563      */
    564     @RemotableViewMethod
    565     public void setExpandOnlyOnButton(boolean expandOnlyOnButton) {
    566         mExpandOnlyOnButton = expandOnlyOnButton;
    567     }
    568 }
    569