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