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.app.AppOpsManager;
     21 import android.app.Notification;
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.content.res.TypedArray;
     25 import android.graphics.Canvas;
     26 import android.graphics.Outline;
     27 import android.graphics.Rect;
     28 import android.graphics.drawable.Drawable;
     29 import android.util.ArraySet;
     30 import android.util.AttributeSet;
     31 import android.widget.ImageView;
     32 import android.widget.RemoteViews;
     33 
     34 import com.android.internal.R;
     35 import com.android.internal.widget.CachingIconView;
     36 
     37 import java.util.ArrayList;
     38 
     39 /**
     40  * A header of a notification view
     41  *
     42  * @hide
     43  */
     44 @RemoteViews.RemoteView
     45 public 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