1 /* 2 * Copyright (C) 2014 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 com.android.systemui.statusbar.notification.row; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.content.Context; 22 import android.content.res.TypedArray; 23 import android.graphics.Canvas; 24 import android.graphics.drawable.Drawable; 25 import android.os.Bundle; 26 import android.os.Handler; 27 import android.util.AttributeSet; 28 import android.util.Log; 29 import android.view.View; 30 import android.view.ViewAnimationUtils; 31 import android.view.accessibility.AccessibilityEvent; 32 import android.view.accessibility.AccessibilityNodeInfo; 33 import android.widget.FrameLayout; 34 35 import androidx.annotation.Nullable; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.systemui.Dependency; 39 import com.android.systemui.Interpolators; 40 import com.android.systemui.R; 41 import com.android.systemui.statusbar.notification.stack.StackStateAnimator; 42 43 /** 44 * The guts of a notification revealed when performing a long press. 45 */ 46 public class NotificationGuts extends FrameLayout { 47 private static final String TAG = "NotificationGuts"; 48 private static final long CLOSE_GUTS_DELAY = 8000; 49 50 private Drawable mBackground; 51 private int mClipTopAmount; 52 private int mClipBottomAmount; 53 private int mActualHeight; 54 private boolean mExposed; 55 56 private Handler mHandler; 57 private Runnable mFalsingCheck; 58 private boolean mNeedsFalsingProtection; 59 private OnGutsClosedListener mClosedListener; 60 private OnHeightChangedListener mHeightListener; 61 62 private GutsContent mGutsContent; 63 64 private View.AccessibilityDelegate mGutsContentAccessibilityDelegate = 65 new View.AccessibilityDelegate() { 66 @Override 67 public void onInitializeAccessibilityNodeInfo( 68 View host, AccessibilityNodeInfo info) { 69 super.onInitializeAccessibilityNodeInfo(host, info); 70 info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK); 71 } 72 73 @Override 74 public boolean performAccessibilityAction(View host, int action, Bundle args) { 75 if (super.performAccessibilityAction(host, action, args)) { 76 return true; 77 } 78 79 switch (action) { 80 case AccessibilityNodeInfo.ACTION_LONG_CLICK: 81 closeControls(host, false); 82 return true; 83 } 84 85 return false; 86 } 87 }; 88 89 public interface GutsContent { 90 setGutsParent(NotificationGuts listener)91 public void setGutsParent(NotificationGuts listener); 92 93 /** 94 * Return the view to be shown in the notification guts. 95 */ getContentView()96 public View getContentView(); 97 98 /** 99 * Return the actual height of the content. 100 */ getActualHeight()101 public int getActualHeight(); 102 103 /** 104 * Called when the guts view have been told to close, typically after an outside 105 * interaction. 106 * 107 * @param save whether the state should be saved. 108 * @param force whether the guts view should be forced closed regardless of state. 109 * @return if closing the view has been handled. 110 */ handleCloseControls(boolean save, boolean force)111 public boolean handleCloseControls(boolean save, boolean force); 112 113 /** 114 * Return whether the notification associated with these guts is set to be removed. 115 */ willBeRemoved()116 public boolean willBeRemoved(); 117 118 /** 119 * Return whether these guts are a leavebehind (e.g. {@link NotificationSnooze}). 120 */ isLeavebehind()121 public default boolean isLeavebehind() { 122 return false; 123 } 124 125 /** 126 * Return whether something changed and needs to be saved, possibly requiring a bouncer. 127 */ shouldBeSaved()128 boolean shouldBeSaved(); 129 130 /** 131 * Called when the guts view has finished its close animation. 132 */ onFinishedClosing()133 default void onFinishedClosing() {} 134 135 /** 136 * Returns whether falsing protection is needed before showing the contents of this 137 * view on the lockscreen 138 */ needsFalsingProtection()139 boolean needsFalsingProtection(); 140 141 /** 142 * Equivalent to {@link View#setAccessibilityDelegate(AccessibilityDelegate)} 143 */ setAccessibilityDelegate(AccessibilityDelegate gutsContentAccessibilityDelegate)144 void setAccessibilityDelegate(AccessibilityDelegate gutsContentAccessibilityDelegate); 145 } 146 147 public interface OnGutsClosedListener { onGutsClosed(NotificationGuts guts)148 public void onGutsClosed(NotificationGuts guts); 149 } 150 151 public interface OnHeightChangedListener { onHeightChanged(NotificationGuts guts)152 public void onHeightChanged(NotificationGuts guts); 153 } 154 155 private interface OnSettingsClickListener { onClick(View v, int appUid)156 void onClick(View v, int appUid); 157 } 158 NotificationGuts(Context context, AttributeSet attrs)159 public NotificationGuts(Context context, AttributeSet attrs) { 160 super(context, attrs); 161 setWillNotDraw(false); 162 mHandler = new Handler(); 163 mFalsingCheck = new Runnable() { 164 @Override 165 public void run() { 166 if (mNeedsFalsingProtection && mExposed) { 167 closeControls(-1 /* x */, -1 /* y */, false /* save */, false /* force */); 168 } 169 } 170 }; 171 final TypedArray ta = context.obtainStyledAttributes(attrs, 172 com.android.internal.R.styleable.Theme, 0, 0); 173 ta.recycle(); 174 } 175 NotificationGuts(Context context)176 public NotificationGuts(Context context) { 177 this(context, null); 178 } 179 setGutsContent(GutsContent content)180 public void setGutsContent(GutsContent content) { 181 content.setGutsParent(this); 182 content.setAccessibilityDelegate(mGutsContentAccessibilityDelegate); 183 mGutsContent = content; 184 removeAllViews(); 185 addView(mGutsContent.getContentView()); 186 } 187 getGutsContent()188 public GutsContent getGutsContent() { 189 return mGutsContent; 190 } 191 resetFalsingCheck()192 public void resetFalsingCheck() { 193 mHandler.removeCallbacks(mFalsingCheck); 194 if (mNeedsFalsingProtection && mExposed) { 195 mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY); 196 } 197 } 198 199 @Override onDraw(Canvas canvas)200 protected void onDraw(Canvas canvas) { 201 draw(canvas, mBackground); 202 } 203 draw(Canvas canvas, Drawable drawable)204 private void draw(Canvas canvas, Drawable drawable) { 205 int top = mClipTopAmount; 206 int bottom = mActualHeight - mClipBottomAmount; 207 if (drawable != null && top < bottom) { 208 drawable.setBounds(0, top, getWidth(), bottom); 209 drawable.draw(canvas); 210 } 211 } 212 213 @Override onFinishInflate()214 protected void onFinishInflate() { 215 super.onFinishInflate(); 216 mBackground = mContext.getDrawable(R.drawable.notification_guts_bg); 217 if (mBackground != null) { 218 mBackground.setCallback(this); 219 } 220 } 221 222 @Override verifyDrawable(Drawable who)223 protected boolean verifyDrawable(Drawable who) { 224 return super.verifyDrawable(who) || who == mBackground; 225 } 226 227 @Override drawableStateChanged()228 protected void drawableStateChanged() { 229 drawableStateChanged(mBackground); 230 } 231 drawableStateChanged(Drawable d)232 private void drawableStateChanged(Drawable d) { 233 if (d != null && d.isStateful()) { 234 d.setState(getDrawableState()); 235 } 236 } 237 238 @Override drawableHotspotChanged(float x, float y)239 public void drawableHotspotChanged(float x, float y) { 240 if (mBackground != null) { 241 mBackground.setHotspot(x, y); 242 } 243 } 244 openControls( boolean shouldDoCircularReveal, int x, int y, boolean needsFalsingProtection, @Nullable Runnable onAnimationEnd)245 public void openControls( 246 boolean shouldDoCircularReveal, 247 int x, 248 int y, 249 boolean needsFalsingProtection, 250 @Nullable Runnable onAnimationEnd) { 251 animateOpen(shouldDoCircularReveal, x, y, onAnimationEnd); 252 setExposed(true /* exposed */, needsFalsingProtection); 253 } 254 255 /** 256 * Hide controls if they are visible 257 * @param leavebehinds true if leavebehinds should be closed 258 * @param controls true if controls should be closed 259 * @param x x coordinate to animate the close circular reveal with 260 * @param y y coordinate to animate the close circular reveal with 261 * @param force whether the guts should be force-closed regardless of state. 262 */ closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force)263 public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) { 264 if (mGutsContent != null) { 265 if ((mGutsContent.isLeavebehind() && leavebehinds) 266 || (!mGutsContent.isLeavebehind() && controls)) { 267 closeControls(x, y, mGutsContent.shouldBeSaved(), force); 268 } 269 } 270 } 271 272 /** 273 * Closes any exposed guts/views. 274 */ closeControls(View eventSource, boolean save)275 public void closeControls(View eventSource, boolean save) { 276 int[] parentLoc = new int[2]; 277 int[] targetLoc = new int[2]; 278 getLocationOnScreen(parentLoc); 279 eventSource.getLocationOnScreen(targetLoc); 280 final int centerX = eventSource.getWidth() / 2; 281 final int centerY = eventSource.getHeight() / 2; 282 final int x = targetLoc[0] - parentLoc[0] + centerX; 283 final int y = targetLoc[1] - parentLoc[1] + centerY; 284 285 closeControls(x, y, save, false); 286 } 287 288 /** 289 * Closes any exposed guts/views. 290 * 291 * @param x x coordinate to animate the close circular reveal with 292 * @param y y coordinate to animate the close circular reveal with 293 * @param save whether the state should be saved 294 * @param force whether the guts should be force-closed regardless of state. 295 */ closeControls(int x, int y, boolean save, boolean force)296 private void closeControls(int x, int y, boolean save, boolean force) { 297 // First try to dismiss any blocking helper. 298 boolean wasBlockingHelperDismissed = 299 Dependency.get(NotificationBlockingHelperManager.class) 300 .dismissCurrentBlockingHelper(); 301 302 if (getWindowToken() == null) { 303 if (mClosedListener != null) { 304 mClosedListener.onGutsClosed(this); 305 } 306 return; 307 } 308 309 if (mGutsContent == null 310 || !mGutsContent.handleCloseControls(save, force) 311 || wasBlockingHelperDismissed) { 312 // We only want to do a circular reveal if we're not showing the blocking helper. 313 animateClose(x, y, !wasBlockingHelperDismissed /* shouldDoCircularReveal */); 314 315 setExposed(false, mNeedsFalsingProtection); 316 if (mClosedListener != null) { 317 mClosedListener.onGutsClosed(this); 318 } 319 } 320 } 321 322 /** Animates in the guts view via either a fade or a circular reveal. */ animateOpen( boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd)323 private void animateOpen( 324 boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd) { 325 if (isAttachedToWindow()) { 326 if (shouldDoCircularReveal) { 327 double horz = Math.max(getWidth() - x, x); 328 double vert = Math.max(getHeight() - y, y); 329 float r = (float) Math.hypot(horz, vert); 330 // Make sure we'll be visible after the circular reveal 331 setAlpha(1f); 332 // Circular reveal originating at (x, y) 333 Animator a = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r); 334 a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 335 a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 336 a.addListener(new AnimateOpenListener(onAnimationEnd)); 337 a.start(); 338 } else { 339 // Fade in content 340 this.setAlpha(0f); 341 this.animate() 342 .alpha(1f) 343 .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE) 344 .setInterpolator(Interpolators.ALPHA_IN) 345 .setListener(new AnimateOpenListener(onAnimationEnd)) 346 .start(); 347 } 348 } else { 349 Log.w(TAG, "Failed to animate guts open"); 350 } 351 } 352 353 354 /** Animates out the guts view via either a fade or a circular reveal. */ 355 @VisibleForTesting animateClose(int x, int y, boolean shouldDoCircularReveal)356 void animateClose(int x, int y, boolean shouldDoCircularReveal) { 357 if (isAttachedToWindow()) { 358 if (shouldDoCircularReveal) { 359 // Circular reveal originating at (x, y) 360 if (x == -1 || y == -1) { 361 x = (getLeft() + getRight()) / 2; 362 y = (getTop() + getHeight() / 2); 363 } 364 double horz = Math.max(getWidth() - x, x); 365 double vert = Math.max(getHeight() - y, y); 366 float r = (float) Math.hypot(horz, vert); 367 Animator a = ViewAnimationUtils.createCircularReveal(this, 368 x, y, r, 0); 369 a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 370 a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 371 a.addListener(new AnimateCloseListener(this /* view */, mGutsContent)); 372 a.start(); 373 } else { 374 // Fade in the blocking helper. 375 this.animate() 376 .alpha(0f) 377 .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE) 378 .setInterpolator(Interpolators.ALPHA_OUT) 379 .setListener(new AnimateCloseListener(this, /* view */mGutsContent)) 380 .start(); 381 } 382 } else { 383 Log.w(TAG, "Failed to animate guts close"); 384 mGutsContent.onFinishedClosing(); 385 } 386 } 387 setActualHeight(int actualHeight)388 public void setActualHeight(int actualHeight) { 389 mActualHeight = actualHeight; 390 invalidate(); 391 } 392 getActualHeight()393 public int getActualHeight() { 394 return mActualHeight; 395 } 396 getIntrinsicHeight()397 public int getIntrinsicHeight() { 398 return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight(); 399 } 400 setClipTopAmount(int clipTopAmount)401 public void setClipTopAmount(int clipTopAmount) { 402 mClipTopAmount = clipTopAmount; 403 invalidate(); 404 } 405 setClipBottomAmount(int clipBottomAmount)406 public void setClipBottomAmount(int clipBottomAmount) { 407 mClipBottomAmount = clipBottomAmount; 408 invalidate(); 409 } 410 411 @Override hasOverlappingRendering()412 public boolean hasOverlappingRendering() { 413 // Prevents this view from creating a layer when alpha is animating. 414 return false; 415 } 416 setClosedListener(OnGutsClosedListener listener)417 public void setClosedListener(OnGutsClosedListener listener) { 418 mClosedListener = listener; 419 } 420 setHeightChangedListener(OnHeightChangedListener listener)421 public void setHeightChangedListener(OnHeightChangedListener listener) { 422 mHeightListener = listener; 423 } 424 onHeightChanged()425 protected void onHeightChanged() { 426 if (mHeightListener != null) { 427 mHeightListener.onHeightChanged(this); 428 } 429 } 430 431 @VisibleForTesting setExposed(boolean exposed, boolean needsFalsingProtection)432 void setExposed(boolean exposed, boolean needsFalsingProtection) { 433 final boolean wasExposed = mExposed; 434 mExposed = exposed; 435 mNeedsFalsingProtection = needsFalsingProtection; 436 if (mExposed && mNeedsFalsingProtection) { 437 resetFalsingCheck(); 438 } else { 439 mHandler.removeCallbacks(mFalsingCheck); 440 } 441 if (wasExposed != mExposed && mGutsContent != null) { 442 final View contentView = mGutsContent.getContentView(); 443 contentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 444 if (mExposed) { 445 contentView.requestAccessibilityFocus(); 446 } 447 } 448 } 449 willBeRemoved()450 public boolean willBeRemoved() { 451 return mGutsContent != null ? mGutsContent.willBeRemoved() : false; 452 } 453 isExposed()454 public boolean isExposed() { 455 return mExposed; 456 } 457 isLeavebehind()458 public boolean isLeavebehind() { 459 return mGutsContent != null && mGutsContent.isLeavebehind(); 460 } 461 462 /** Listener for animations executed in {@link #animateOpen(boolean, int, int, Runnable)}. */ 463 private static class AnimateOpenListener extends AnimatorListenerAdapter { 464 final Runnable mOnAnimationEnd; 465 AnimateOpenListener(Runnable onAnimationEnd)466 private AnimateOpenListener(Runnable onAnimationEnd) { 467 mOnAnimationEnd = onAnimationEnd; 468 } 469 470 @Override onAnimationEnd(Animator animation)471 public void onAnimationEnd(Animator animation) { 472 super.onAnimationEnd(animation); 473 if (mOnAnimationEnd != null) { 474 mOnAnimationEnd.run(); 475 } 476 } 477 } 478 479 /** Listener for animations executed in {@link #animateClose(int, int, boolean)}. */ 480 private class AnimateCloseListener extends AnimatorListenerAdapter { 481 final View mView; 482 private final GutsContent mGutsContent; 483 AnimateCloseListener(View view, GutsContent gutsContent)484 private AnimateCloseListener(View view, GutsContent gutsContent) { 485 mView = view; 486 mGutsContent = gutsContent; 487 } 488 489 @Override onAnimationEnd(Animator animation)490 public void onAnimationEnd(Animator animation) { 491 super.onAnimationEnd(animation); 492 if (!isExposed()) { 493 mView.setVisibility(View.GONE); 494 mGutsContent.onFinishedClosing(); 495 } 496 } 497 } 498 } 499