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