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