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