blob: 4700baae8fabcd9f4fe3d133d80d4853f4affc1a [file] [log] [blame]
Selim Cinek024ca592014-09-01 15:11:28 +02001/*
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
Rohan Shah20790b82018-07-02 17:21:04 -070017package com.android.systemui.statusbar.notification.row;
Selim Cinek024ca592014-09-01 15:11:28 +020018
Mady Mellor97c8df42016-03-22 18:09:39 -070019import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
Selim Cinek024ca592014-09-01 15:11:28 +020021import android.content.Context;
Julia Reynolds3aa969f2016-05-26 11:07:49 -040022import android.content.res.TypedArray;
Selim Cinek024ca592014-09-01 15:11:28 +020023import android.graphics.Canvas;
24import android.graphics.drawable.Drawable;
Mady Mellor97c8df42016-03-22 18:09:39 -070025import android.os.Handler;
Selim Cinek024ca592014-09-01 15:11:28 +020026import android.util.AttributeSet;
Rohan Shah08f58582018-05-04 16:31:43 -070027import android.util.Log;
Julia Reynoldsa07af882015-12-17 08:32:48 -050028import android.view.View;
Mady Mellor97c8df42016-03-22 18:09:39 -070029import android.view.ViewAnimationUtils;
Geoffrey Pitschd94e7882017-04-06 09:52:11 -040030import android.view.accessibility.AccessibilityEvent;
Mady Mellor87d79452017-01-10 11:52:52 -080031import android.widget.FrameLayout;
Julia Reynoldsead00aa2015-12-07 08:23:48 -050032
Gus Prevas9abc5062018-10-31 16:11:04 -040033import androidx.annotation.Nullable;
34
Rohan Shahda5dcdd2018-04-27 17:21:50 -070035import com.android.internal.annotations.VisibleForTesting;
Rohan Shah524cf7b2018-03-15 14:40:02 -070036import com.android.systemui.Dependency;
Mady Mellor97c8df42016-03-22 18:09:39 -070037import com.android.systemui.Interpolators;
Selim Cinek024ca592014-09-01 15:11:28 +020038import com.android.systemui.R;
Rohan Shah20790b82018-07-02 17:21:04 -070039import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
Selim Cinek024ca592014-09-01 15:11:28 +020040
41/**
42 * The guts of a notification revealed when performing a long press.
43 */
Mady Mellor95d743c2017-01-10 12:05:27 -080044public class NotificationGuts extends FrameLayout {
Geoffrey Pitsch4dd50062016-12-06 16:41:22 -050045 private static final String TAG = "NotificationGuts";
Mady Mellor97c8df42016-03-22 18:09:39 -070046 private static final long CLOSE_GUTS_DELAY = 8000;
47
Selim Cinek024ca592014-09-01 15:11:28 +020048 private Drawable mBackground;
49 private int mClipTopAmount;
Selim Cineka686b2c2016-10-26 13:58:27 -070050 private int mClipBottomAmount;
Selim Cinek024ca592014-09-01 15:11:28 +020051 private int mActualHeight;
Selim Cinekd84a5932015-12-15 11:45:36 -080052 private boolean mExposed;
Selim Cinek024ca592014-09-01 15:11:28 +020053
Mady Mellor97c8df42016-03-22 18:09:39 -070054 private Handler mHandler;
55 private Runnable mFalsingCheck;
56 private boolean mNeedsFalsingProtection;
Mady Mellore09fb702017-03-30 13:23:29 -070057 private OnGutsClosedListener mClosedListener;
58 private OnHeightChangedListener mHeightListener;
Mady Mellor97c8df42016-03-22 18:09:39 -070059
Mady Mellor87d79452017-01-10 11:52:52 -080060 private GutsContent mGutsContent;
61
Mady Mellor95d743c2017-01-10 12:05:27 -080062 public interface GutsContent {
63
64 public void setGutsParent(NotificationGuts listener);
65
66 /**
Lucas Dupin9b08c012018-05-16 19:53:32 -070067 * Return the view to be shown in the notification guts.
Mady Mellor95d743c2017-01-10 12:05:27 -080068 */
69 public View getContentView();
70
71 /**
Lucas Dupin9b08c012018-05-16 19:53:32 -070072 * Return the actual height of the content.
Mady Mellore09fb702017-03-30 13:23:29 -070073 */
74 public int getActualHeight();
75
76 /**
Mady Mellor95d743c2017-01-10 12:05:27 -080077 * Called when the guts view have been told to close, typically after an outside
Mady Mellorc2dbe492017-03-30 13:22:03 -070078 * 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.
Mady Mellor95d743c2017-01-10 12:05:27 -080083 */
Mady Mellorc2dbe492017-03-30 13:22:03 -070084 public boolean handleCloseControls(boolean save, boolean force);
Mady Mellor95d743c2017-01-10 12:05:27 -080085
86 /**
Lucas Dupin9b08c012018-05-16 19:53:32 -070087 * Return whether the notification associated with these guts is set to be removed.
Mady Mellor95d743c2017-01-10 12:05:27 -080088 */
89 public boolean willBeRemoved();
Mady Mellorc2dbe492017-03-30 13:22:03 -070090
91 /**
Lucas Dupin9b08c012018-05-16 19:53:32 -070092 * Return whether these guts are a leavebehind (e.g. {@link NotificationSnooze}).
Mady Mellorc2dbe492017-03-30 13:22:03 -070093 */
94 public default boolean isLeavebehind() {
95 return false;
96 }
Lucas Dupin9b08c012018-05-16 19:53:32 -070097
98 /**
99 * Return whether something changed and needs to be saved, possibly requiring a bouncer.
100 */
101 boolean shouldBeSaved();
Gus Prevas9abc5062018-10-31 16:11:04 -0400102
103 /**
104 * Called when the guts view has finished its close animation.
105 */
106 default void onFinishedClosing() {}
Mady Mellor95d743c2017-01-10 12:05:27 -0800107 }
108
Mady Mellor97c8df42016-03-22 18:09:39 -0700109 public interface OnGutsClosedListener {
110 public void onGutsClosed(NotificationGuts guts);
111 }
112
Mady Mellore09fb702017-03-30 13:23:29 -0700113 public interface OnHeightChangedListener {
114 public void onHeightChanged(NotificationGuts guts);
115 }
116
Evan Lairde55c6012019-03-13 12:54:37 -0400117 private interface OnSettingsClickListener {
Mady Mellor95d743c2017-01-10 12:05:27 -0800118 void onClick(View v, int appUid);
119 }
120
Selim Cinek024ca592014-09-01 15:11:28 +0200121 public NotificationGuts(Context context, AttributeSet attrs) {
122 super(context, attrs);
123 setWillNotDraw(false);
Mady Mellor97c8df42016-03-22 18:09:39 -0700124 mHandler = new Handler();
125 mFalsingCheck = new Runnable() {
126 @Override
127 public void run() {
128 if (mNeedsFalsingProtection && mExposed) {
Mady Mellorc2dbe492017-03-30 13:22:03 -0700129 closeControls(-1 /* x */, -1 /* y */, false /* save */, false /* force */);
Mady Mellor97c8df42016-03-22 18:09:39 -0700130 }
131 }
132 };
Mady Mellor87d79452017-01-10 11:52:52 -0800133 final TypedArray ta = context.obtainStyledAttributes(attrs,
134 com.android.internal.R.styleable.Theme, 0, 0);
Julia Reynolds3aa969f2016-05-26 11:07:49 -0400135 ta.recycle();
Mady Mellor97c8df42016-03-22 18:09:39 -0700136 }
137
Mady Mellor87d79452017-01-10 11:52:52 -0800138 public NotificationGuts(Context context) {
139 this(context, null);
Mady Mellor87d79452017-01-10 11:52:52 -0800140 }
141
142 public void setGutsContent(GutsContent content) {
143 mGutsContent = content;
144 removeAllViews();
145 addView(mGutsContent.getContentView());
146 }
147
Mady Mellorc2dbe492017-03-30 13:22:03 -0700148 public GutsContent getGutsContent() {
149 return mGutsContent;
150 }
151
Mady Mellor97c8df42016-03-22 18:09:39 -0700152 public void resetFalsingCheck() {
153 mHandler.removeCallbacks(mFalsingCheck);
154 if (mNeedsFalsingProtection && mExposed) {
155 mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY);
156 }
Selim Cinek024ca592014-09-01 15:11:28 +0200157 }
158
159 @Override
160 protected void onDraw(Canvas canvas) {
161 draw(canvas, mBackground);
162 }
163
164 private void draw(Canvas canvas, Drawable drawable) {
Selim Cineka686b2c2016-10-26 13:58:27 -0700165 int top = mClipTopAmount;
166 int bottom = mActualHeight - mClipBottomAmount;
167 if (drawable != null && top < bottom) {
168 drawable.setBounds(0, top, getWidth(), bottom);
Selim Cinek024ca592014-09-01 15:11:28 +0200169 drawable.draw(canvas);
170 }
171 }
172
173 @Override
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
183 protected boolean verifyDrawable(Drawable who) {
184 return super.verifyDrawable(who) || who == mBackground;
185 }
186
187 @Override
188 protected void drawableStateChanged() {
189 drawableStateChanged(mBackground);
190 }
191
192 private void drawableStateChanged(Drawable d) {
193 if (d != null && d.isStateful()) {
194 d.setState(getDrawableState());
195 }
196 }
197
198 @Override
199 public void drawableHotspotChanged(float x, float y) {
200 if (mBackground != null) {
201 mBackground.setHotspot(x, y);
202 }
203 }
204
yoshiki iguchia85c2a02018-01-12 11:28:06 +0900205 public void openControls(
Rohan Shah524cf7b2018-03-15 14:40:02 -0700206 boolean shouldDoCircularReveal,
207 int x,
208 int y,
209 boolean needsFalsingProtection,
210 @Nullable Runnable onAnimationEnd) {
211 animateOpen(shouldDoCircularReveal, x, y, onAnimationEnd);
yoshiki iguchia85c2a02018-01-12 11:28:06 +0900212 setExposed(true /* exposed */, needsFalsingProtection);
213 }
214
Lucas Dupin9b08c012018-05-16 19:53:32 -0700215 /**
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 */
Mady Mellorc2dbe492017-03-30 13:22:03 -0700223 public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) {
224 if (mGutsContent != null) {
Lucas Dupin9b08c012018-05-16 19:53:32 -0700225 if ((mGutsContent.isLeavebehind() && leavebehinds)
226 || (!mGutsContent.isLeavebehind() && controls)) {
227 closeControls(x, y, mGutsContent.shouldBeSaved(), force);
Mady Mellorc2dbe492017-03-30 13:22:03 -0700228 }
229 }
230 }
231
Rohan Shah524cf7b2018-03-15 14:40:02 -0700232 /**
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 */
Mady Mellorc2dbe492017-03-30 13:22:03 -0700240 public void closeControls(int x, int y, boolean save, boolean force) {
Rohan Shah524cf7b2018-03-15 14:40:02 -0700241 // First try to dismiss any blocking helper.
242 boolean wasBlockingHelperDismissed =
243 Dependency.get(NotificationBlockingHelperManager.class)
244 .dismissCurrentBlockingHelper();
245
Mady Mellor97c8df42016-03-22 18:09:39 -0700246 if (getWindowToken() == null) {
Mady Mellore09fb702017-03-30 13:23:29 -0700247 if (mClosedListener != null) {
248 mClosedListener.onGutsClosed(this);
Mady Mellor97c8df42016-03-22 18:09:39 -0700249 }
250 return;
251 }
Mady Mellorc2dbe492017-03-30 13:22:03 -0700252
Rohan Shah524cf7b2018-03-15 14:40:02 -0700253 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
Mady Mellorc2dbe492017-03-30 13:22:03 -0700259 setExposed(false, mNeedsFalsingProtection);
260 if (mClosedListener != null) {
261 mClosedListener.onGutsClosed(this);
262 }
Mady Mellor87d79452017-01-10 11:52:52 -0800263 }
264 }
265
Rohan Shah524cf7b2018-03-15 14:40:02 -0700266 /** Animates in the guts view via either a fade or a circular reveal. */
267 private void animateOpen(
268 boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd) {
Rohan Shah08f58582018-05-04 16:31:43 -0700269 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);
Evan Lairde55c6012019-03-13 12:54:37 -0400274 // Make sure we'll be visible after the circular reveal
275 setAlpha(1f);
Rohan Shah08f58582018-05-04 16:31:43 -0700276 // 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 }
Rohan Shah524cf7b2018-03-15 14:40:02 -0700292 } else {
Rohan Shah08f58582018-05-04 16:31:43 -0700293 Log.w(TAG, "Failed to animate guts open");
Rohan Shah524cf7b2018-03-15 14:40:02 -0700294 }
yoshiki iguchia85c2a02018-01-12 11:28:06 +0900295 }
296
Rohan Shah524cf7b2018-03-15 14:40:02 -0700297
298 /** Animates out the guts view via either a fade or a circular reveal. */
Rohan Shahda5dcdd2018-04-27 17:21:50 -0700299 @VisibleForTesting
300 void animateClose(int x, int y, boolean shouldDoCircularReveal) {
Rohan Shah08f58582018-05-04 16:31:43 -0700301 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);
Gus Prevas9abc5062018-10-31 16:11:04 -0400315 a.addListener(new AnimateCloseListener(this /* view */, mGutsContent));
Rohan Shah08f58582018-05-04 16:31:43 -0700316 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)
Gus Prevas9abc5062018-10-31 16:11:04 -0400323 .setListener(new AnimateCloseListener(this, /* view */mGutsContent))
Rohan Shah08f58582018-05-04 16:31:43 -0700324 .start();
Mady Mellor97c8df42016-03-22 18:09:39 -0700325 }
Rohan Shah524cf7b2018-03-15 14:40:02 -0700326 } else {
Rohan Shah08f58582018-05-04 16:31:43 -0700327 Log.w(TAG, "Failed to animate guts close");
Gus Prevas9abc5062018-10-31 16:11:04 -0400328 mGutsContent.onFinishedClosing();
Rohan Shah524cf7b2018-03-15 14:40:02 -0700329 }
Mady Mellor97c8df42016-03-22 18:09:39 -0700330 }
331
Selim Cinek024ca592014-09-01 15:11:28 +0200332 public void setActualHeight(int actualHeight) {
333 mActualHeight = actualHeight;
334 invalidate();
335 }
336
337 public int getActualHeight() {
338 return mActualHeight;
339 }
340
Mady Mellore09fb702017-03-30 13:23:29 -0700341 public int getIntrinsicHeight() {
342 return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight();
343 }
344
Selim Cinek024ca592014-09-01 15:11:28 +0200345 public void setClipTopAmount(int clipTopAmount) {
346 mClipTopAmount = clipTopAmount;
347 invalidate();
348 }
349
Selim Cineka686b2c2016-10-26 13:58:27 -0700350 public void setClipBottomAmount(int clipBottomAmount) {
351 mClipBottomAmount = clipBottomAmount;
352 invalidate();
353 }
354
Selim Cinek024ca592014-09-01 15:11:28 +0200355 @Override
356 public boolean hasOverlappingRendering() {
Selim Cinek024ca592014-09-01 15:11:28 +0200357 // Prevents this view from creating a layer when alpha is animating.
358 return false;
359 }
Selim Cinekd84a5932015-12-15 11:45:36 -0800360
Mady Mellor97c8df42016-03-22 18:09:39 -0700361 public void setClosedListener(OnGutsClosedListener listener) {
Mady Mellore09fb702017-03-30 13:23:29 -0700362 mClosedListener = listener;
363 }
364
365 public void setHeightChangedListener(OnHeightChangedListener listener) {
366 mHeightListener = listener;
367 }
368
369 protected void onHeightChanged() {
370 if (mHeightListener != null) {
371 mHeightListener.onHeightChanged(this);
372 }
Mady Mellor97c8df42016-03-22 18:09:39 -0700373 }
374
Rohan Shahda5dcdd2018-04-27 17:21:50 -0700375 @VisibleForTesting
376 void setExposed(boolean exposed, boolean needsFalsingProtection) {
Geoffrey Pitschd94e7882017-04-06 09:52:11 -0400377 final boolean wasExposed = mExposed;
Selim Cinekd84a5932015-12-15 11:45:36 -0800378 mExposed = exposed;
Mady Mellor97c8df42016-03-22 18:09:39 -0700379 mNeedsFalsingProtection = needsFalsingProtection;
380 if (mExposed && mNeedsFalsingProtection) {
381 resetFalsingCheck();
382 } else {
383 mHandler.removeCallbacks(mFalsingCheck);
384 }
Geoffrey Pitschd94e7882017-04-06 09:52:11 -0400385 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 }
Selim Cinekd84a5932015-12-15 11:45:36 -0800392 }
393
Mady Mellor434180c2017-02-13 11:29:42 -0800394 public boolean willBeRemoved() {
395 return mGutsContent != null ? mGutsContent.willBeRemoved() : false;
396 }
397
Geoffrey Pitsch4dd50062016-12-06 16:41:22 -0500398 public boolean isExposed() {
Selim Cinekd84a5932015-12-15 11:45:36 -0800399 return mExposed;
400 }
Mady Mellor32343e62017-07-19 10:52:47 -0700401
402 public boolean isLeavebehind() {
403 return mGutsContent != null && mGutsContent.isLeavebehind();
404 }
Rohan Shah524cf7b2018-03-15 14:40:02 -0700405
406 /** Listener for animations executed in {@link #animateOpen(boolean, int, int, Runnable)}. */
407 private static class AnimateOpenListener extends AnimatorListenerAdapter {
408 final Runnable mOnAnimationEnd;
409
410 private AnimateOpenListener(Runnable onAnimationEnd) {
411 mOnAnimationEnd = onAnimationEnd;
412 }
413
414 @Override
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 static class AnimateCloseListener extends AnimatorListenerAdapter {
425 final View mView;
Gus Prevas9abc5062018-10-31 16:11:04 -0400426 private final GutsContent mGutsContent;
Rohan Shah524cf7b2018-03-15 14:40:02 -0700427
Gus Prevas9abc5062018-10-31 16:11:04 -0400428 private AnimateCloseListener(View view, GutsContent gutsContent) {
Rohan Shah524cf7b2018-03-15 14:40:02 -0700429 mView = view;
Gus Prevas9abc5062018-10-31 16:11:04 -0400430 mGutsContent = gutsContent;
Rohan Shah524cf7b2018-03-15 14:40:02 -0700431 }
432
433 @Override
434 public void onAnimationEnd(Animator animation) {
435 super.onAnimationEnd(animation);
436 mView.setVisibility(View.GONE);
Gus Prevas9abc5062018-10-31 16:11:04 -0400437 mGutsContent.onFinishedClosing();
Rohan Shah524cf7b2018-03-15 14:40:02 -0700438 }
439 }
Selim Cinek024ca592014-09-01 15:11:28 +0200440}