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 NotificationHeaderView(Context context)84 public NotificationHeaderView(Context context) { 85 this(context, null); 86 } 87 NotificationHeaderView(Context context, @Nullable AttributeSet attrs)88 public NotificationHeaderView(Context context, @Nullable AttributeSet attrs) { 89 this(context, attrs, 0); 90 } 91 NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)92 public NotificationHeaderView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 93 this(context, attrs, defStyleAttr, 0); 94 } 95 NotificationHeaderView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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 onFinishInflate()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 onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 shrinkViewForOverflow(int heightSpec, int overFlow, View targetView, int minimumWidth)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 onLayout(boolean changed, int l, int t, int r, int b)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 generateLayoutParams(AttributeSet attrs)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 */ setHeaderBackgroundDrawable(Drawable drawable)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 onDraw(Canvas canvas)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 verifyDrawable(Drawable who)259 protected boolean verifyDrawable(Drawable who) { 260 return super.verifyDrawable(who) || who == mBackground; 261 } 262 263 @Override drawableStateChanged()264 protected void drawableStateChanged() { 265 if (mBackground != null && mBackground.isStateful()) { 266 mBackground.setState(getDrawableState()); 267 } 268 } 269 updateTouchListener()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 */ setAppOpsOnClickListener(OnClickListener l)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 setOnClickListener(@ullable OnClickListener l)292 public void setOnClickListener(@Nullable OnClickListener l) { 293 mExpandClickListener = l; 294 mExpandButton.setOnClickListener(mExpandClickListener); 295 updateTouchListener(); 296 } 297 298 @RemotableViewMethod setOriginalIconColor(int color)299 public void setOriginalIconColor(int color) { 300 mIconColor = color; 301 } 302 getOriginalIconColor()303 public int getOriginalIconColor() { 304 return mIconColor; 305 } 306 307 @RemotableViewMethod setOriginalNotificationColor(int color)308 public void setOriginalNotificationColor(int color) { 309 mOriginalNotificationColor = color; 310 } 311 getOriginalNotificationColor()312 public int getOriginalNotificationColor() { 313 return mOriginalNotificationColor; 314 } 315 316 @RemotableViewMethod setExpanded(boolean expanded)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 */ showAppOpsIcons(ArraySet<Integer> appOps)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 updateExpandButton()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 setShowWorkBadgeAtEnd(boolean showWorkBadgeAtEnd)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 */ setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd)365 public void setShowExpandButtonAtEnd(boolean showExpandButtonAtEnd) { 366 if (showExpandButtonAtEnd != mShowExpandButtonAtEnd) { 367 setClipToPadding(!showExpandButtonAtEnd); 368 mShowExpandButtonAtEnd = showExpandButtonAtEnd; 369 } 370 } 371 getWorkProfileIcon()372 public View getWorkProfileIcon() { 373 return mProfileBadge; 374 } 375 getIcon()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 HeaderTouchListener()390 public HeaderTouchListener() { 391 } 392 bindTouchRects()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 addWidthRect()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 addRectAroundView(View view)411 private Rect addRectAroundView(View view) { 412 final Rect r = getRectAroundView(view); 413 mTouchRects.add(r); 414 return r; 415 } 416 getRectAroundView(View view)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 onTouch(View v, MotionEvent event)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 isInside(float x, float y)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 getFirstChildNotGone()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 getExpandButton()497 public ImageView getExpandButton() { 498 return mExpandButton; 499 } 500 501 @Override hasOverlappingRendering()502 public boolean hasOverlappingRendering() { 503 return false; 504 } 505 isInTouchRect(float x, float y)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 setAcceptAllTouches(boolean acceptAllTouches)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 setExpandOnlyOnButton(boolean expandOnlyOnButton)527 public void setExpandOnlyOnButton(boolean expandOnlyOnButton) { 528 mExpandOnlyOnButton = expandOnlyOnButton; 529 } 530 } 531