blob: fbe9c5d40beb553ccb5b7f30f72eb598d9d947e6 [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
Mady Mellor95d743c2017-01-10 12:05:27 -0800117 interface OnSettingsClickListener {
118 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);
274 // Circular reveal originating at (x, y)
275 Animator a = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r);
276 a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
277 a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
278 a.addListener(new AnimateOpenListener(onAnimationEnd));
279 a.start();
280 } else {
281 // Fade in content
282 this.setAlpha(0f);
283 this.animate()
284 .alpha(1f)
285 .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE)
286 .setInterpolator(Interpolators.ALPHA_IN)
287 .setListener(new AnimateOpenListener(onAnimationEnd))
288 .start();
289 }
Rohan Shah524cf7b2018-03-15 14:40:02 -0700290 } else {
Rohan Shah08f58582018-05-04 16:31:43 -0700291 Log.w(TAG, "Failed to animate guts open");
Rohan Shah524cf7b2018-03-15 14:40:02 -0700292 }
yoshiki iguchia85c2a02018-01-12 11:28:06 +0900293 }
294
Rohan Shah524cf7b2018-03-15 14:40:02 -0700295
296 /** Animates out the guts view via either a fade or a circular reveal. */
Rohan Shahda5dcdd2018-04-27 17:21:50 -0700297 @VisibleForTesting
298 void animateClose(int x, int y, boolean shouldDoCircularReveal) {
Rohan Shah08f58582018-05-04 16:31:43 -0700299 if (isAttachedToWindow()) {
300 if (shouldDoCircularReveal) {
301 // Circular reveal originating at (x, y)
302 if (x == -1 || y == -1) {
303 x = (getLeft() + getRight()) / 2;
304 y = (getTop() + getHeight() / 2);
305 }
306 double horz = Math.max(getWidth() - x, x);
307 double vert = Math.max(getHeight() - y, y);
308 float r = (float) Math.hypot(horz, vert);
309 Animator a = ViewAnimationUtils.createCircularReveal(this,
310 x, y, r, 0);
311 a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
312 a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
Gus Prevas9abc5062018-10-31 16:11:04 -0400313 a.addListener(new AnimateCloseListener(this /* view */, mGutsContent));
Rohan Shah08f58582018-05-04 16:31:43 -0700314 a.start();
315 } else {
316 // Fade in the blocking helper.
317 this.animate()
318 .alpha(0f)
319 .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE)
320 .setInterpolator(Interpolators.ALPHA_OUT)
Gus Prevas9abc5062018-10-31 16:11:04 -0400321 .setListener(new AnimateCloseListener(this, /* view */mGutsContent))
Rohan Shah08f58582018-05-04 16:31:43 -0700322 .start();
Mady Mellor97c8df42016-03-22 18:09:39 -0700323 }
Rohan Shah524cf7b2018-03-15 14:40:02 -0700324 } else {
Rohan Shah08f58582018-05-04 16:31:43 -0700325 Log.w(TAG, "Failed to animate guts close");
Gus Prevas9abc5062018-10-31 16:11:04 -0400326 mGutsContent.onFinishedClosing();
Rohan Shah524cf7b2018-03-15 14:40:02 -0700327 }
Mady Mellor97c8df42016-03-22 18:09:39 -0700328 }
329
Selim Cinek024ca592014-09-01 15:11:28 +0200330 public void setActualHeight(int actualHeight) {
331 mActualHeight = actualHeight;
332 invalidate();
333 }
334
335 public int getActualHeight() {
336 return mActualHeight;
337 }
338
Mady Mellore09fb702017-03-30 13:23:29 -0700339 public int getIntrinsicHeight() {
340 return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight();
341 }
342
Selim Cinek024ca592014-09-01 15:11:28 +0200343 public void setClipTopAmount(int clipTopAmount) {
344 mClipTopAmount = clipTopAmount;
345 invalidate();
346 }
347
Selim Cineka686b2c2016-10-26 13:58:27 -0700348 public void setClipBottomAmount(int clipBottomAmount) {
349 mClipBottomAmount = clipBottomAmount;
350 invalidate();
351 }
352
Selim Cinek024ca592014-09-01 15:11:28 +0200353 @Override
354 public boolean hasOverlappingRendering() {
Selim Cinek024ca592014-09-01 15:11:28 +0200355 // Prevents this view from creating a layer when alpha is animating.
356 return false;
357 }
Selim Cinekd84a5932015-12-15 11:45:36 -0800358
Mady Mellor97c8df42016-03-22 18:09:39 -0700359 public void setClosedListener(OnGutsClosedListener listener) {
Mady Mellore09fb702017-03-30 13:23:29 -0700360 mClosedListener = listener;
361 }
362
363 public void setHeightChangedListener(OnHeightChangedListener listener) {
364 mHeightListener = listener;
365 }
366
367 protected void onHeightChanged() {
368 if (mHeightListener != null) {
369 mHeightListener.onHeightChanged(this);
370 }
Mady Mellor97c8df42016-03-22 18:09:39 -0700371 }
372
Rohan Shahda5dcdd2018-04-27 17:21:50 -0700373 @VisibleForTesting
374 void setExposed(boolean exposed, boolean needsFalsingProtection) {
Geoffrey Pitschd94e7882017-04-06 09:52:11 -0400375 final boolean wasExposed = mExposed;
Selim Cinekd84a5932015-12-15 11:45:36 -0800376 mExposed = exposed;
Mady Mellor97c8df42016-03-22 18:09:39 -0700377 mNeedsFalsingProtection = needsFalsingProtection;
378 if (mExposed && mNeedsFalsingProtection) {
379 resetFalsingCheck();
380 } else {
381 mHandler.removeCallbacks(mFalsingCheck);
382 }
Geoffrey Pitschd94e7882017-04-06 09:52:11 -0400383 if (wasExposed != mExposed && mGutsContent != null) {
384 final View contentView = mGutsContent.getContentView();
385 contentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
386 if (mExposed) {
387 contentView.requestAccessibilityFocus();
388 }
389 }
Selim Cinekd84a5932015-12-15 11:45:36 -0800390 }
391
Mady Mellor434180c2017-02-13 11:29:42 -0800392 public boolean willBeRemoved() {
393 return mGutsContent != null ? mGutsContent.willBeRemoved() : false;
394 }
395
Geoffrey Pitsch4dd50062016-12-06 16:41:22 -0500396 public boolean isExposed() {
Selim Cinekd84a5932015-12-15 11:45:36 -0800397 return mExposed;
398 }
Mady Mellor32343e62017-07-19 10:52:47 -0700399
400 public boolean isLeavebehind() {
401 return mGutsContent != null && mGutsContent.isLeavebehind();
402 }
Rohan Shah524cf7b2018-03-15 14:40:02 -0700403
404 /** Listener for animations executed in {@link #animateOpen(boolean, int, int, Runnable)}. */
405 private static class AnimateOpenListener extends AnimatorListenerAdapter {
406 final Runnable mOnAnimationEnd;
407
408 private AnimateOpenListener(Runnable onAnimationEnd) {
409 mOnAnimationEnd = onAnimationEnd;
410 }
411
412 @Override
413 public void onAnimationEnd(Animator animation) {
414 super.onAnimationEnd(animation);
415 if (mOnAnimationEnd != null) {
416 mOnAnimationEnd.run();
417 }
418 }
419 }
420
421 /** Listener for animations executed in {@link #animateClose(int, int, boolean)}. */
422 private static class AnimateCloseListener extends AnimatorListenerAdapter {
423 final View mView;
Gus Prevas9abc5062018-10-31 16:11:04 -0400424 private final GutsContent mGutsContent;
Rohan Shah524cf7b2018-03-15 14:40:02 -0700425
Gus Prevas9abc5062018-10-31 16:11:04 -0400426 private AnimateCloseListener(View view, GutsContent gutsContent) {
Rohan Shah524cf7b2018-03-15 14:40:02 -0700427 mView = view;
Gus Prevas9abc5062018-10-31 16:11:04 -0400428 mGutsContent = gutsContent;
Rohan Shah524cf7b2018-03-15 14:40:02 -0700429 }
430
431 @Override
432 public void onAnimationEnd(Animator animation) {
433 super.onAnimationEnd(animation);
434 mView.setVisibility(View.GONE);
Gus Prevas9abc5062018-10-31 16:11:04 -0400435 mGutsContent.onFinishedClosing();
Rohan Shah524cf7b2018-03-15 14:40:02 -0700436 }
437 }
Selim Cinek024ca592014-09-01 15:11:28 +0200438}