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