blob: cfd5d83c87b9e59a0e80729426990bf565a8c084 [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
17package com.android.systemui.statusbar;
18
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;
yoshiki iguchia85c2a02018-01-12 11:28:06 +090026import android.support.annotation.Nullable;
Selim Cinek024ca592014-09-01 15:11:28 +020027import android.util.AttributeSet;
Rohan Shah08f58582018-05-04 16:31:43 -070028import android.util.Log;
Julia Reynoldsa07af882015-12-17 08:32:48 -050029import android.view.View;
Mady Mellor97c8df42016-03-22 18:09:39 -070030import android.view.ViewAnimationUtils;
Geoffrey Pitschd94e7882017-04-06 09:52:11 -040031import android.view.accessibility.AccessibilityEvent;
Mady Mellor87d79452017-01-10 11:52:52 -080032import android.widget.FrameLayout;
Julia Reynoldsead00aa2015-12-07 08:23:48 -050033
Rohan Shahda5dcdd2018-04-27 17:21:50 -070034import com.android.internal.annotations.VisibleForTesting;
Rohan Shah524cf7b2018-03-15 14:40:02 -070035import com.android.systemui.Dependency;
Mady Mellor97c8df42016-03-22 18:09:39 -070036import com.android.systemui.Interpolators;
Selim Cinek024ca592014-09-01 15:11:28 +020037import com.android.systemui.R;
Mady Mellor97c8df42016-03-22 18:09:39 -070038import com.android.systemui.statusbar.stack.StackStateAnimator;
Selim Cinek024ca592014-09-01 15:11:28 +020039
40/**
41 * The guts of a notification revealed when performing a long press.
42 */
Mady Mellor95d743c2017-01-10 12:05:27 -080043public class NotificationGuts extends FrameLayout {
Geoffrey Pitsch4dd50062016-12-06 16:41:22 -050044 private static final String TAG = "NotificationGuts";
Mady Mellor97c8df42016-03-22 18:09:39 -070045 private static final long CLOSE_GUTS_DELAY = 8000;
46
Selim Cinek024ca592014-09-01 15:11:28 +020047 private Drawable mBackground;
48 private int mClipTopAmount;
Selim Cineka686b2c2016-10-26 13:58:27 -070049 private int mClipBottomAmount;
Selim Cinek024ca592014-09-01 15:11:28 +020050 private int mActualHeight;
Selim Cinekd84a5932015-12-15 11:45:36 -080051 private boolean mExposed;
Selim Cinek024ca592014-09-01 15:11:28 +020052
Mady Mellor97c8df42016-03-22 18:09:39 -070053 private Handler mHandler;
54 private Runnable mFalsingCheck;
55 private boolean mNeedsFalsingProtection;
Mady Mellore09fb702017-03-30 13:23:29 -070056 private OnGutsClosedListener mClosedListener;
57 private OnHeightChangedListener mHeightListener;
Mady Mellor97c8df42016-03-22 18:09:39 -070058
Mady Mellor87d79452017-01-10 11:52:52 -080059 private GutsContent mGutsContent;
60
Mady Mellor95d743c2017-01-10 12:05:27 -080061 public interface GutsContent {
62
63 public void setGutsParent(NotificationGuts listener);
64
65 /**
Lucas Dupin9b08c012018-05-16 19:53:32 -070066 * Return the view to be shown in the notification guts.
Mady Mellor95d743c2017-01-10 12:05:27 -080067 */
68 public View getContentView();
69
70 /**
Lucas Dupin9b08c012018-05-16 19:53:32 -070071 * Return the actual height of the content.
Mady Mellore09fb702017-03-30 13:23:29 -070072 */
73 public int getActualHeight();
74
75 /**
Mady Mellor95d743c2017-01-10 12:05:27 -080076 * Called when the guts view have been told to close, typically after an outside
Mady Mellorc2dbe492017-03-30 13:22:03 -070077 * 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.
Mady Mellor95d743c2017-01-10 12:05:27 -080082 */
Mady Mellorc2dbe492017-03-30 13:22:03 -070083 public boolean handleCloseControls(boolean save, boolean force);
Mady Mellor95d743c2017-01-10 12:05:27 -080084
85 /**
Lucas Dupin9b08c012018-05-16 19:53:32 -070086 * Return whether the notification associated with these guts is set to be removed.
Mady Mellor95d743c2017-01-10 12:05:27 -080087 */
88 public boolean willBeRemoved();
Mady Mellorc2dbe492017-03-30 13:22:03 -070089
90 /**
Lucas Dupin9b08c012018-05-16 19:53:32 -070091 * Return whether these guts are a leavebehind (e.g. {@link NotificationSnooze}).
Mady Mellorc2dbe492017-03-30 13:22:03 -070092 */
93 public default boolean isLeavebehind() {
94 return false;
95 }
Lucas Dupin9b08c012018-05-16 19:53:32 -070096
97 /**
98 * Return whether something changed and needs to be saved, possibly requiring a bouncer.
99 */
100 boolean shouldBeSaved();
Mady Mellor95d743c2017-01-10 12:05:27 -0800101 }
102
Mady Mellor97c8df42016-03-22 18:09:39 -0700103 public interface OnGutsClosedListener {
104 public void onGutsClosed(NotificationGuts guts);
105 }
106
Mady Mellore09fb702017-03-30 13:23:29 -0700107 public interface OnHeightChangedListener {
108 public void onHeightChanged(NotificationGuts guts);
109 }
110
Mady Mellor95d743c2017-01-10 12:05:27 -0800111 interface OnSettingsClickListener {
112 void onClick(View v, int appUid);
113 }
114
Selim Cinek024ca592014-09-01 15:11:28 +0200115 public NotificationGuts(Context context, AttributeSet attrs) {
116 super(context, attrs);
117 setWillNotDraw(false);
Mady Mellor97c8df42016-03-22 18:09:39 -0700118 mHandler = new Handler();
119 mFalsingCheck = new Runnable() {
120 @Override
121 public void run() {
122 if (mNeedsFalsingProtection && mExposed) {
Mady Mellorc2dbe492017-03-30 13:22:03 -0700123 closeControls(-1 /* x */, -1 /* y */, false /* save */, false /* force */);
Mady Mellor97c8df42016-03-22 18:09:39 -0700124 }
125 }
126 };
Mady Mellor87d79452017-01-10 11:52:52 -0800127 final TypedArray ta = context.obtainStyledAttributes(attrs,
128 com.android.internal.R.styleable.Theme, 0, 0);
Julia Reynolds3aa969f2016-05-26 11:07:49 -0400129 ta.recycle();
Mady Mellor97c8df42016-03-22 18:09:39 -0700130 }
131
Mady Mellor87d79452017-01-10 11:52:52 -0800132 public NotificationGuts(Context context) {
133 this(context, null);
Mady Mellor87d79452017-01-10 11:52:52 -0800134 }
135
136 public void setGutsContent(GutsContent content) {
137 mGutsContent = content;
138 removeAllViews();
139 addView(mGutsContent.getContentView());
140 }
141
Mady Mellorc2dbe492017-03-30 13:22:03 -0700142 public GutsContent getGutsContent() {
143 return mGutsContent;
144 }
145
Mady Mellor97c8df42016-03-22 18:09:39 -0700146 public void resetFalsingCheck() {
147 mHandler.removeCallbacks(mFalsingCheck);
148 if (mNeedsFalsingProtection && mExposed) {
149 mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY);
150 }
Selim Cinek024ca592014-09-01 15:11:28 +0200151 }
152
153 @Override
154 protected void onDraw(Canvas canvas) {
155 draw(canvas, mBackground);
156 }
157
158 private void draw(Canvas canvas, Drawable drawable) {
Selim Cineka686b2c2016-10-26 13:58:27 -0700159 int top = mClipTopAmount;
160 int bottom = mActualHeight - mClipBottomAmount;
161 if (drawable != null && top < bottom) {
162 drawable.setBounds(0, top, getWidth(), bottom);
Selim Cinek024ca592014-09-01 15:11:28 +0200163 drawable.draw(canvas);
164 }
165 }
166
167 @Override
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
177 protected boolean verifyDrawable(Drawable who) {
178 return super.verifyDrawable(who) || who == mBackground;
179 }
180
181 @Override
182 protected void drawableStateChanged() {
183 drawableStateChanged(mBackground);
184 }
185
186 private void drawableStateChanged(Drawable d) {
187 if (d != null && d.isStateful()) {
188 d.setState(getDrawableState());
189 }
190 }
191
192 @Override
193 public void drawableHotspotChanged(float x, float y) {
194 if (mBackground != null) {
195 mBackground.setHotspot(x, y);
196 }
197 }
198
yoshiki iguchia85c2a02018-01-12 11:28:06 +0900199 public void openControls(
Rohan Shah524cf7b2018-03-15 14:40:02 -0700200 boolean shouldDoCircularReveal,
201 int x,
202 int y,
203 boolean needsFalsingProtection,
204 @Nullable Runnable onAnimationEnd) {
205 animateOpen(shouldDoCircularReveal, x, y, onAnimationEnd);
yoshiki iguchia85c2a02018-01-12 11:28:06 +0900206 setExposed(true /* exposed */, needsFalsingProtection);
207 }
208
Lucas Dupin9b08c012018-05-16 19:53:32 -0700209 /**
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 */
Mady Mellorc2dbe492017-03-30 13:22:03 -0700217 public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) {
218 if (mGutsContent != null) {
Lucas Dupin9b08c012018-05-16 19:53:32 -0700219 if ((mGutsContent.isLeavebehind() && leavebehinds)
220 || (!mGutsContent.isLeavebehind() && controls)) {
221 closeControls(x, y, mGutsContent.shouldBeSaved(), force);
Mady Mellorc2dbe492017-03-30 13:22:03 -0700222 }
223 }
224 }
225
Rohan Shah524cf7b2018-03-15 14:40:02 -0700226 /**
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 */
Mady Mellorc2dbe492017-03-30 13:22:03 -0700234 public void closeControls(int x, int y, boolean save, boolean force) {
Rohan Shah524cf7b2018-03-15 14:40:02 -0700235 // First try to dismiss any blocking helper.
236 boolean wasBlockingHelperDismissed =
237 Dependency.get(NotificationBlockingHelperManager.class)
238 .dismissCurrentBlockingHelper();
239
Mady Mellor97c8df42016-03-22 18:09:39 -0700240 if (getWindowToken() == null) {
Mady Mellore09fb702017-03-30 13:23:29 -0700241 if (mClosedListener != null) {
242 mClosedListener.onGutsClosed(this);
Mady Mellor97c8df42016-03-22 18:09:39 -0700243 }
244 return;
245 }
Mady Mellorc2dbe492017-03-30 13:22:03 -0700246
Rohan Shah524cf7b2018-03-15 14:40:02 -0700247 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
Mady Mellorc2dbe492017-03-30 13:22:03 -0700253 setExposed(false, mNeedsFalsingProtection);
254 if (mClosedListener != null) {
255 mClosedListener.onGutsClosed(this);
256 }
Mady Mellor87d79452017-01-10 11:52:52 -0800257 }
258 }
259
Rohan Shah524cf7b2018-03-15 14:40:02 -0700260 /** Animates in the guts view via either a fade or a circular reveal. */
261 private void animateOpen(
262 boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd) {
Rohan Shah08f58582018-05-04 16:31:43 -0700263 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 }
Rohan Shah524cf7b2018-03-15 14:40:02 -0700284 } else {
Rohan Shah08f58582018-05-04 16:31:43 -0700285 Log.w(TAG, "Failed to animate guts open");
Rohan Shah524cf7b2018-03-15 14:40:02 -0700286 }
yoshiki iguchia85c2a02018-01-12 11:28:06 +0900287 }
288
Rohan Shah524cf7b2018-03-15 14:40:02 -0700289
290 /** Animates out the guts view via either a fade or a circular reveal. */
Rohan Shahda5dcdd2018-04-27 17:21:50 -0700291 @VisibleForTesting
292 void animateClose(int x, int y, boolean shouldDoCircularReveal) {
Rohan Shah08f58582018-05-04 16:31:43 -0700293 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();
Mady Mellor97c8df42016-03-22 18:09:39 -0700317 }
Rohan Shah524cf7b2018-03-15 14:40:02 -0700318 } else {
Rohan Shah08f58582018-05-04 16:31:43 -0700319 Log.w(TAG, "Failed to animate guts close");
Rohan Shah524cf7b2018-03-15 14:40:02 -0700320 }
Mady Mellor97c8df42016-03-22 18:09:39 -0700321 }
322
Selim Cinek024ca592014-09-01 15:11:28 +0200323 public void setActualHeight(int actualHeight) {
324 mActualHeight = actualHeight;
325 invalidate();
326 }
327
328 public int getActualHeight() {
329 return mActualHeight;
330 }
331
Mady Mellore09fb702017-03-30 13:23:29 -0700332 public int getIntrinsicHeight() {
333 return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight();
334 }
335
Selim Cinek024ca592014-09-01 15:11:28 +0200336 public void setClipTopAmount(int clipTopAmount) {
337 mClipTopAmount = clipTopAmount;
338 invalidate();
339 }
340
Selim Cineka686b2c2016-10-26 13:58:27 -0700341 public void setClipBottomAmount(int clipBottomAmount) {
342 mClipBottomAmount = clipBottomAmount;
343 invalidate();
344 }
345
Selim Cinek024ca592014-09-01 15:11:28 +0200346 @Override
347 public boolean hasOverlappingRendering() {
Selim Cinek024ca592014-09-01 15:11:28 +0200348 // Prevents this view from creating a layer when alpha is animating.
349 return false;
350 }
Selim Cinekd84a5932015-12-15 11:45:36 -0800351
Mady Mellor97c8df42016-03-22 18:09:39 -0700352 public void setClosedListener(OnGutsClosedListener listener) {
Mady Mellore09fb702017-03-30 13:23:29 -0700353 mClosedListener = listener;
354 }
355
356 public void setHeightChangedListener(OnHeightChangedListener listener) {
357 mHeightListener = listener;
358 }
359
360 protected void onHeightChanged() {
361 if (mHeightListener != null) {
362 mHeightListener.onHeightChanged(this);
363 }
Mady Mellor97c8df42016-03-22 18:09:39 -0700364 }
365
Rohan Shahda5dcdd2018-04-27 17:21:50 -0700366 @VisibleForTesting
367 void setExposed(boolean exposed, boolean needsFalsingProtection) {
Geoffrey Pitschd94e7882017-04-06 09:52:11 -0400368 final boolean wasExposed = mExposed;
Selim Cinekd84a5932015-12-15 11:45:36 -0800369 mExposed = exposed;
Mady Mellor97c8df42016-03-22 18:09:39 -0700370 mNeedsFalsingProtection = needsFalsingProtection;
371 if (mExposed && mNeedsFalsingProtection) {
372 resetFalsingCheck();
373 } else {
374 mHandler.removeCallbacks(mFalsingCheck);
375 }
Geoffrey Pitschd94e7882017-04-06 09:52:11 -0400376 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 }
Selim Cinekd84a5932015-12-15 11:45:36 -0800383 }
384
Mady Mellor434180c2017-02-13 11:29:42 -0800385 public boolean willBeRemoved() {
386 return mGutsContent != null ? mGutsContent.willBeRemoved() : false;
387 }
388
Geoffrey Pitsch4dd50062016-12-06 16:41:22 -0500389 public boolean isExposed() {
Selim Cinekd84a5932015-12-15 11:45:36 -0800390 return mExposed;
391 }
Mady Mellor32343e62017-07-19 10:52:47 -0700392
393 public boolean isLeavebehind() {
394 return mGutsContent != null && mGutsContent.isLeavebehind();
395 }
Rohan Shah524cf7b2018-03-15 14:40:02 -0700396
397 /** Listener for animations executed in {@link #animateOpen(boolean, int, int, Runnable)}. */
398 private static class AnimateOpenListener extends AnimatorListenerAdapter {
399 final Runnable mOnAnimationEnd;
400
401 private AnimateOpenListener(Runnable onAnimationEnd) {
402 mOnAnimationEnd = onAnimationEnd;
403 }
404
405 @Override
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
418 private AnimateCloseListener(View view) {
419 mView = view;
420 }
421
422 @Override
423 public void onAnimationEnd(Animator animation) {
424 super.onAnimationEnd(animation);
425 mView.setVisibility(View.GONE);
426 }
427 }
Selim Cinek024ca592014-09-01 15:11:28 +0200428}