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.compat.annotation.UnsupportedAppUsage; 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 NotificationHeaderView(Context context)87 public NotificationHeaderView(Context context) { 88 this(context, null); 89 } 90 91 @UnsupportedAppUsage NotificationHeaderView(Context context, @Nullable AttributeSet attrs)92 public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) { 93 this(context, attrs, 0); 94 } 95 NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)96 public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 97 this(context, attrs, defStyleAttr, 0); 98 } 99 NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 onFinishInflate()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 onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 shrinkViewForOverflow(int heightSpec, int overFlow, View targetView, int minimumWidth)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 onLayout(boolean changed, int l, int t, int r, int b)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 generateLayoutParams(AttributeSet attrs)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 */ setHeaderBackgroundDrawable(Drawable drawable)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 onDraw(Canvas canvas)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 verifyDrawable(Drawable who)272 protected boolean verifyDrawable(Drawable who) { 273 return super.verifyDrawable(who) || who == mBackground; 274 } 275 276 @Override drawableStateChanged()277 protected void drawableStateChanged() { 278 if (mBackground != null && mBackground.isStateful()) { 279 mBackground.setState(getDrawableState()); 280 } 281 } 282 updateTouchListener()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 */ setAppOpsOnClickListener(OnClickListener l)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 setOnClickListener(@ullable OnClickListener l)305 public void setOnClickListener(@Nullable OnClickListener l) { 306 mExpandClickListener = l; 307 mExpandButton.setOnClickListener(mExpandClickListener); 308 updateTouchListener(); 309 } 310 311 @RemotableViewMethod setOriginalIconColor(int color)312 public void setOriginalIconColor(int color) { 313 mIconColor = color; 314 } 315 getOriginalIconColor()316 public int getOriginalIconColor() { 317 return mIconColor; 318 } 319 320 @RemotableViewMethod setOriginalNotificationColor(int color)321 public void setOriginalNotificationColor(int color) { 322 mOriginalNotificationColor = color; 323 } 324 getOriginalNotificationColor()325 public int getOriginalNotificationColor() { 326 return mOriginalNotificationColor; 327 } 328 329 @RemotableViewMethod setExpanded(boolean expanded)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 */ showAppOpsIcons(ArraySet<Integer> appOps)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. */ setRecentlyAudiblyAlerted(boolean audiblyAlerted)352 public void setRecentlyAudiblyAlerted(boolean audiblyAlerted) { 353 mAudiblyAlertedIcon.setVisibility(audiblyAlerted ? View.VISIBLE : View.GONE); 354 } 355 updateExpandButton()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 setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd)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 */ setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd)383 public void setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd) { 384 if (showExpandButtonAtEnd != mShowExpandButtonAtEnd) { 385 setClipToPadding(!showExpandButtonAtEnd); 386 mShowExpandButtonAtEnd = showExpandButtonAtEnd; 387 } 388 } 389 getWorkProfileIcon()390 public View getWorkProfileIcon() { 391 return mProfileBadge; 392 } 393 getIcon()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 setHeaderTextMarginEnd(int headerTextMarginEnd)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 */ getHeaderTextMarginEnd()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 HeaderTouchListener()428 public HeaderTouchListener() { 429 } 430 bindTouchRects()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 addWidthRect()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 addRectAroundView(View view)449 private Rect addRectAroundView(View view) { 450 final Rect r = getRectAroundView(view); 451 mTouchRects.add(r); 452 return r; 453 } 454 getRectAroundView(View view)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 onTouch(View v, MotionEvent event)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 isInside(float x, float y)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 getFirstChildNotGone()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 getExpandButton()535 public ImageView getExpandButton() { 536 return mExpandButton; 537 } 538 539 @Override hasOverlappingRendering()540 public boolean hasOverlappingRendering() { 541 return false; 542 } 543 isInTouchRect(float x, float y)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 setAcceptAllTouches(boolean acceptAllTouches)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 setExpandOnlyOnButton(boolean expandOnlyOnButton)565 public void setExpandOnlyOnButton(boolean expandOnlyOnButton) { 566 mExpandOnlyOnButton = expandOnlyOnButton; 567 } 568 } 569