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.app.animation.Interpolators; 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.systemui.res.R; 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, /* save= */ 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 */ shouldBeSavedOnClose()126 boolean shouldBeSavedOnClose(); 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( int x, int y, boolean needsFalsingProtection, @Nullable Runnable onAnimationEnd)240 public void openControls( 241 int x, 242 int y, 243 boolean needsFalsingProtection, 244 @Nullable Runnable onAnimationEnd) { 245 animateOpen(x, y, onAnimationEnd); 246 setExposed(true /* exposed */, needsFalsingProtection); 247 } 248 249 /** 250 * Hide controls if they are visible 251 * @param leavebehinds true if leavebehinds should be closed 252 * @param controls true if controls should be closed 253 * @param x x coordinate to animate the close circular reveal with 254 * @param y y coordinate to animate the close circular reveal with 255 * @param force whether the guts should be force-closed regardless of state. 256 */ closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force)257 public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) { 258 if (mGutsContent != null) { 259 if ((mGutsContent.isLeavebehind() && leavebehinds) 260 || (!mGutsContent.isLeavebehind() && controls)) { 261 closeControls(x, y, mGutsContent.shouldBeSavedOnClose(), force); 262 } 263 } 264 } 265 266 /** 267 * Closes any exposed guts/views. 268 */ closeControls(View eventSource, boolean save)269 public void closeControls(View eventSource, boolean save) { 270 int[] parentLoc = new int[2]; 271 int[] targetLoc = new int[2]; 272 getLocationOnScreen(parentLoc); 273 eventSource.getLocationOnScreen(targetLoc); 274 final int centerX = eventSource.getWidth() / 2; 275 final int centerY = eventSource.getHeight() / 2; 276 final int x = targetLoc[0] - parentLoc[0] + centerX; 277 final int y = targetLoc[1] - parentLoc[1] + centerY; 278 279 closeControls(x, y, save, false); 280 } 281 282 /** 283 * Closes any exposed guts/views. 284 * 285 * @param x x coordinate to animate the close circular reveal with 286 * @param y y coordinate to animate the close circular reveal with 287 * @param save whether the state should be saved 288 * @param force whether the guts should be force-closed regardless of state. 289 */ closeControls(int x, int y, boolean save, boolean force)290 private void closeControls(int x, int y, boolean save, boolean force) { 291 // First try to dismiss any blocking helper. 292 if (getWindowToken() == null) { 293 if (mClosedListener != null) { 294 mClosedListener.onGutsClosed(this); 295 } 296 return; 297 } 298 299 if (mGutsContent == null 300 || !mGutsContent.handleCloseControls(save, force)) { 301 // We only want to do a circular reveal if we're not showing the blocking helper. 302 animateClose(x, y); 303 304 setExposed(false, mNeedsFalsingProtection); 305 if (mClosedListener != null) { 306 mClosedListener.onGutsClosed(this); 307 } 308 } 309 } 310 311 /** Animates in the guts view with a circular reveal. */ animateOpen(int x, int y, @Nullable Runnable onAnimationEnd)312 private void animateOpen(int x, int y, @Nullable Runnable onAnimationEnd) { 313 if (isAttachedToWindow()) { 314 double horz = Math.max(getWidth() - x, x); 315 double vert = Math.max(getHeight() - y, y); 316 float r = (float) Math.hypot(horz, vert); 317 // Make sure we'll be visible after the circular reveal 318 setAlpha(1f); 319 // Circular reveal originating at (x, y) 320 Animator a = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r); 321 a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 322 a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 323 a.addListener(new AnimateOpenListener(onAnimationEnd)); 324 a.start(); 325 326 } else { 327 Log.w(TAG, "Failed to animate guts open"); 328 } 329 } 330 331 332 /** Animates out the guts view with a circular reveal. */ 333 @VisibleForTesting animateClose(int x, int y)334 void animateClose(int x, int y) { 335 if (isAttachedToWindow()) { 336 // Circular reveal originating at (x, y) 337 if (x == -1 || y == -1) { 338 x = (getLeft() + getRight()) / 2; 339 y = (getTop() + getHeight() / 2); 340 } 341 double horz = Math.max(getWidth() - x, x); 342 double vert = Math.max(getHeight() - y, y); 343 float r = (float) Math.hypot(horz, vert); 344 Animator a = ViewAnimationUtils.createCircularReveal(this, 345 x, y, r, 0); 346 a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 347 a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 348 a.addListener(new AnimateCloseListener(this /* view */, mGutsContent)); 349 a.start(); 350 } else { 351 Log.w(TAG, "Failed to animate guts close"); 352 mGutsContent.onFinishedClosing(); 353 } 354 } 355 setActualHeight(int actualHeight)356 public void setActualHeight(int actualHeight) { 357 mActualHeight = actualHeight; 358 invalidate(); 359 } 360 getActualHeight()361 public int getActualHeight() { 362 return mActualHeight; 363 } 364 getIntrinsicHeight()365 public int getIntrinsicHeight() { 366 return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight(); 367 } 368 setClipTopAmount(int clipTopAmount)369 public void setClipTopAmount(int clipTopAmount) { 370 mClipTopAmount = clipTopAmount; 371 invalidate(); 372 } 373 setClipBottomAmount(int clipBottomAmount)374 public void setClipBottomAmount(int clipBottomAmount) { 375 mClipBottomAmount = clipBottomAmount; 376 invalidate(); 377 } 378 379 @Override hasOverlappingRendering()380 public boolean hasOverlappingRendering() { 381 // Prevents this view from creating a layer when alpha is animating. 382 return false; 383 } 384 setClosedListener(OnGutsClosedListener listener)385 public void setClosedListener(OnGutsClosedListener listener) { 386 mClosedListener = listener; 387 } 388 setHeightChangedListener(OnHeightChangedListener listener)389 public void setHeightChangedListener(OnHeightChangedListener listener) { 390 mHeightListener = listener; 391 } 392 onHeightChanged()393 protected void onHeightChanged() { 394 if (mHeightListener != null) { 395 mHeightListener.onHeightChanged(this); 396 } 397 } 398 399 @VisibleForTesting setExposed(boolean exposed, boolean needsFalsingProtection)400 void setExposed(boolean exposed, boolean needsFalsingProtection) { 401 final boolean wasExposed = mExposed; 402 mExposed = exposed; 403 mNeedsFalsingProtection = needsFalsingProtection; 404 if (mExposed && mNeedsFalsingProtection) { 405 resetFalsingCheck(); 406 } else { 407 mHandler.removeCallbacks(mFalsingCheck); 408 } 409 if (wasExposed != mExposed && mGutsContent != null) { 410 final View contentView = mGutsContent.getContentView(); 411 contentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 412 if (mExposed) { 413 contentView.requestAccessibilityFocus(); 414 } 415 } 416 } 417 willBeRemoved()418 public boolean willBeRemoved() { 419 return mGutsContent != null ? mGutsContent.willBeRemoved() : false; 420 } 421 isExposed()422 public boolean isExposed() { 423 return mExposed; 424 } 425 isLeavebehind()426 public boolean isLeavebehind() { 427 return mGutsContent != null && mGutsContent.isLeavebehind(); 428 } 429 430 /** Listener for animations executed in {@link #animateOpen(int, int, Runnable)}. */ 431 private static class AnimateOpenListener extends AnimatorListenerAdapter { 432 final Runnable mOnAnimationEnd; 433 AnimateOpenListener(Runnable onAnimationEnd)434 private AnimateOpenListener(Runnable onAnimationEnd) { 435 mOnAnimationEnd = onAnimationEnd; 436 } 437 438 @Override onAnimationEnd(Animator animation)439 public void onAnimationEnd(Animator animation) { 440 super.onAnimationEnd(animation); 441 if (mOnAnimationEnd != null) { 442 mOnAnimationEnd.run(); 443 } 444 } 445 } 446 447 /** Listener for animations executed in {@link #animateClose(int, int, boolean)}. */ 448 private class AnimateCloseListener extends AnimatorListenerAdapter { 449 final View mView; 450 private final GutsContent mGutsContent; 451 AnimateCloseListener(View view, GutsContent gutsContent)452 private AnimateCloseListener(View view, GutsContent gutsContent) { 453 mView = view; 454 mGutsContent = gutsContent; 455 } 456 457 @Override onAnimationEnd(Animator animation)458 public void onAnimationEnd(Animator animation) { 459 super.onAnimationEnd(animation); 460 if (!isExposed()) { 461 mView.setVisibility(View.GONE); 462 mGutsContent.onFinishedClosing(); 463 } 464 } 465 } 466 } 467