• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.internal.annotations.VisibleForTesting;
37 import com.android.systemui.R;
38 import com.android.systemui.animation.Interpolators;
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, 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          */
shouldBeSaved()126         boolean shouldBeSaved();
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( boolean shouldDoCircularReveal, int x, int y, boolean needsFalsingProtection, @Nullable Runnable onAnimationEnd)240     public void openControls(
241             boolean shouldDoCircularReveal,
242             int x,
243             int y,
244             boolean needsFalsingProtection,
245             @Nullable Runnable onAnimationEnd) {
246         animateOpen(shouldDoCircularReveal, x, y, onAnimationEnd);
247         setExposed(true /* exposed */, needsFalsingProtection);
248     }
249 
250     /**
251      * Hide controls if they are visible
252      * @param leavebehinds true if leavebehinds should be closed
253      * @param controls true if controls should be closed
254      * @param x x coordinate to animate the close circular reveal with
255      * @param y y coordinate to animate the close circular reveal with
256      * @param force whether the guts should be force-closed regardless of state.
257      */
closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force)258     public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) {
259         if (mGutsContent != null) {
260             if ((mGutsContent.isLeavebehind() && leavebehinds)
261                     || (!mGutsContent.isLeavebehind() && controls)) {
262                 closeControls(x, y, mGutsContent.shouldBeSaved(), force);
263             }
264         }
265     }
266 
267     /**
268      * Closes any exposed guts/views.
269      */
closeControls(View eventSource, boolean save)270     public void closeControls(View eventSource, boolean save) {
271         int[] parentLoc = new int[2];
272         int[] targetLoc = new int[2];
273         getLocationOnScreen(parentLoc);
274         eventSource.getLocationOnScreen(targetLoc);
275         final int centerX = eventSource.getWidth() / 2;
276         final int centerY = eventSource.getHeight() / 2;
277         final int x = targetLoc[0] - parentLoc[0] + centerX;
278         final int y = targetLoc[1] - parentLoc[1] + centerY;
279 
280         closeControls(x, y, save, false);
281     }
282 
283     /**
284      * Closes any exposed guts/views.
285      *
286      * @param x x coordinate to animate the close circular reveal with
287      * @param y y coordinate to animate the close circular reveal with
288      * @param save whether the state should be saved
289      * @param force whether the guts should be force-closed regardless of state.
290      */
closeControls(int x, int y, boolean save, boolean force)291     private void closeControls(int x, int y, boolean save, boolean force) {
292         // First try to dismiss any blocking helper.
293         if (getWindowToken() == null) {
294             if (mClosedListener != null) {
295                 mClosedListener.onGutsClosed(this);
296             }
297             return;
298         }
299 
300         if (mGutsContent == null
301                 || !mGutsContent.handleCloseControls(save, force)) {
302             // We only want to do a circular reveal if we're not showing the blocking helper.
303             animateClose(x, y, true /* shouldDoCircularReveal */);
304 
305             setExposed(false, mNeedsFalsingProtection);
306             if (mClosedListener != null) {
307                 mClosedListener.onGutsClosed(this);
308             }
309         }
310     }
311 
312     /** Animates in the guts view via either a fade or a circular reveal. */
animateOpen( boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd)313     private void animateOpen(
314             boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd) {
315         if (isAttachedToWindow()) {
316             if (shouldDoCircularReveal) {
317                 double horz = Math.max(getWidth() - x, x);
318                 double vert = Math.max(getHeight() - y, y);
319                 float r = (float) Math.hypot(horz, vert);
320                 // Make sure we'll be visible after the circular reveal
321                 setAlpha(1f);
322                 // Circular reveal originating at (x, y)
323                 Animator a = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r);
324                 a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
325                 a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
326                 a.addListener(new AnimateOpenListener(onAnimationEnd));
327                 a.start();
328             } else {
329                 // Fade in content
330                 this.setAlpha(0f);
331                 this.animate()
332                         .alpha(1f)
333                         .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE)
334                         .setInterpolator(Interpolators.ALPHA_IN)
335                         .setListener(new AnimateOpenListener(onAnimationEnd))
336                         .start();
337             }
338         } else {
339             Log.w(TAG, "Failed to animate guts open");
340         }
341     }
342 
343 
344     /** Animates out the guts view via either a fade or a circular reveal. */
345     @VisibleForTesting
animateClose(int x, int y, boolean shouldDoCircularReveal)346     void animateClose(int x, int y, boolean shouldDoCircularReveal) {
347         if (isAttachedToWindow()) {
348             if (shouldDoCircularReveal) {
349                 // Circular reveal originating at (x, y)
350                 if (x == -1 || y == -1) {
351                     x = (getLeft() + getRight()) / 2;
352                     y = (getTop() + getHeight() / 2);
353                 }
354                 double horz = Math.max(getWidth() - x, x);
355                 double vert = Math.max(getHeight() - y, y);
356                 float r = (float) Math.hypot(horz, vert);
357                 Animator a = ViewAnimationUtils.createCircularReveal(this,
358                         x, y, r, 0);
359                 a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
360                 a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
361                 a.addListener(new AnimateCloseListener(this /* view */, mGutsContent));
362                 a.start();
363             } else {
364                 // Fade in the blocking helper.
365                 this.animate()
366                         .alpha(0f)
367                         .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE)
368                         .setInterpolator(Interpolators.ALPHA_OUT)
369                         .setListener(new AnimateCloseListener(this, /* view */mGutsContent))
370                         .start();
371             }
372         } else {
373             Log.w(TAG, "Failed to animate guts close");
374             mGutsContent.onFinishedClosing();
375         }
376     }
377 
setActualHeight(int actualHeight)378     public void setActualHeight(int actualHeight) {
379         mActualHeight = actualHeight;
380         invalidate();
381     }
382 
getActualHeight()383     public int getActualHeight() {
384         return mActualHeight;
385     }
386 
getIntrinsicHeight()387     public int getIntrinsicHeight() {
388         return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight();
389     }
390 
setClipTopAmount(int clipTopAmount)391     public void setClipTopAmount(int clipTopAmount) {
392         mClipTopAmount = clipTopAmount;
393         invalidate();
394     }
395 
setClipBottomAmount(int clipBottomAmount)396     public void setClipBottomAmount(int clipBottomAmount) {
397         mClipBottomAmount = clipBottomAmount;
398         invalidate();
399     }
400 
401     @Override
hasOverlappingRendering()402     public boolean hasOverlappingRendering() {
403         // Prevents this view from creating a layer when alpha is animating.
404         return false;
405     }
406 
setClosedListener(OnGutsClosedListener listener)407     public void setClosedListener(OnGutsClosedListener listener) {
408         mClosedListener = listener;
409     }
410 
setHeightChangedListener(OnHeightChangedListener listener)411     public void setHeightChangedListener(OnHeightChangedListener listener) {
412         mHeightListener = listener;
413     }
414 
onHeightChanged()415     protected void onHeightChanged() {
416         if (mHeightListener != null) {
417             mHeightListener.onHeightChanged(this);
418         }
419     }
420 
421     @VisibleForTesting
setExposed(boolean exposed, boolean needsFalsingProtection)422     void setExposed(boolean exposed, boolean needsFalsingProtection) {
423         final boolean wasExposed = mExposed;
424         mExposed = exposed;
425         mNeedsFalsingProtection = needsFalsingProtection;
426         if (mExposed && mNeedsFalsingProtection) {
427             resetFalsingCheck();
428         } else {
429             mHandler.removeCallbacks(mFalsingCheck);
430         }
431         if (wasExposed != mExposed && mGutsContent != null) {
432             final View contentView = mGutsContent.getContentView();
433             contentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
434             if (mExposed) {
435                 contentView.requestAccessibilityFocus();
436             }
437         }
438     }
439 
willBeRemoved()440     public boolean willBeRemoved() {
441         return mGutsContent != null ? mGutsContent.willBeRemoved() : false;
442     }
443 
isExposed()444     public boolean isExposed() {
445         return mExposed;
446     }
447 
isLeavebehind()448     public boolean isLeavebehind() {
449         return mGutsContent != null && mGutsContent.isLeavebehind();
450     }
451 
452     /** Listener for animations executed in {@link #animateOpen(boolean, int, int, Runnable)}. */
453     private static class AnimateOpenListener extends AnimatorListenerAdapter {
454         final Runnable mOnAnimationEnd;
455 
AnimateOpenListener(Runnable onAnimationEnd)456         private AnimateOpenListener(Runnable onAnimationEnd) {
457             mOnAnimationEnd = onAnimationEnd;
458         }
459 
460         @Override
onAnimationEnd(Animator animation)461         public void onAnimationEnd(Animator animation) {
462             super.onAnimationEnd(animation);
463             if (mOnAnimationEnd != null) {
464                 mOnAnimationEnd.run();
465             }
466         }
467     }
468 
469     /** Listener for animations executed in {@link #animateClose(int, int, boolean)}. */
470     private class AnimateCloseListener extends AnimatorListenerAdapter {
471         final View mView;
472         private final GutsContent mGutsContent;
473 
AnimateCloseListener(View view, GutsContent gutsContent)474         private AnimateCloseListener(View view, GutsContent gutsContent) {
475             mView = view;
476             mGutsContent = gutsContent;
477         }
478 
479         @Override
onAnimationEnd(Animator animation)480         public void onAnimationEnd(Animator animation) {
481             super.onAnimationEnd(animation);
482             if (!isExposed()) {
483                 mView.setVisibility(View.GONE);
484                 mGutsContent.onFinishedClosing();
485             }
486         }
487     }
488 }
489