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.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.widget.FrameLayout;
32 
33 import androidx.annotation.Nullable;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.systemui.Dependency;
37 import com.android.systemui.Interpolators;
38 import com.android.systemui.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     public interface GutsContent {
63 
setGutsParent(NotificationGuts listener)64         public void setGutsParent(NotificationGuts listener);
65 
66         /**
67          * Return the view to be shown in the notification guts.
68          */
getContentView()69         public View getContentView();
70 
71         /**
72          * Return the actual height of the content.
73          */
getActualHeight()74         public int getActualHeight();
75 
76         /**
77          * Called when the guts view have been told to close, typically after an outside
78          * 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.
83          */
handleCloseControls(boolean save, boolean force)84         public boolean handleCloseControls(boolean save, boolean force);
85 
86         /**
87          * Return whether the notification associated with these guts is set to be removed.
88          */
willBeRemoved()89         public boolean willBeRemoved();
90 
91         /**
92          * Return whether these guts are a leavebehind (e.g. {@link NotificationSnooze}).
93          */
isLeavebehind()94         public default boolean isLeavebehind() {
95             return false;
96         }
97 
98         /**
99          * Return whether something changed and needs to be saved, possibly requiring a bouncer.
100          */
shouldBeSaved()101         boolean shouldBeSaved();
102 
103         /**
104          * Called when the guts view has finished its close animation.
105          */
onFinishedClosing()106         default void onFinishedClosing() {}
107     }
108 
109     public interface OnGutsClosedListener {
onGutsClosed(NotificationGuts guts)110         public void onGutsClosed(NotificationGuts guts);
111     }
112 
113     public interface OnHeightChangedListener {
onHeightChanged(NotificationGuts guts)114         public void onHeightChanged(NotificationGuts guts);
115     }
116 
117     private interface OnSettingsClickListener {
onClick(View v, int appUid)118         void onClick(View v, int appUid);
119     }
120 
NotificationGuts(Context context, AttributeSet attrs)121     public NotificationGuts(Context context, AttributeSet attrs) {
122         super(context, attrs);
123         setWillNotDraw(false);
124         mHandler = new Handler();
125         mFalsingCheck = new Runnable() {
126             @Override
127             public void run() {
128                 if (mNeedsFalsingProtection && mExposed) {
129                     closeControls(-1 /* x */, -1 /* y */, false /* save */, false /* force */);
130                 }
131             }
132         };
133         final TypedArray ta = context.obtainStyledAttributes(attrs,
134                 com.android.internal.R.styleable.Theme, 0, 0);
135         ta.recycle();
136     }
137 
NotificationGuts(Context context)138     public NotificationGuts(Context context) {
139         this(context, null);
140     }
141 
setGutsContent(GutsContent content)142     public void setGutsContent(GutsContent content) {
143         mGutsContent = content;
144         removeAllViews();
145         addView(mGutsContent.getContentView());
146     }
147 
getGutsContent()148     public GutsContent getGutsContent() {
149         return mGutsContent;
150     }
151 
resetFalsingCheck()152     public void resetFalsingCheck() {
153         mHandler.removeCallbacks(mFalsingCheck);
154         if (mNeedsFalsingProtection && mExposed) {
155             mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY);
156         }
157     }
158 
159     @Override
onDraw(Canvas canvas)160     protected void onDraw(Canvas canvas) {
161         draw(canvas, mBackground);
162     }
163 
draw(Canvas canvas, Drawable drawable)164     private void draw(Canvas canvas, Drawable drawable) {
165         int top = mClipTopAmount;
166         int bottom = mActualHeight - mClipBottomAmount;
167         if (drawable != null && top < bottom) {
168             drawable.setBounds(0, top, getWidth(), bottom);
169             drawable.draw(canvas);
170         }
171     }
172 
173     @Override
onFinishInflate()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
verifyDrawable(Drawable who)183     protected boolean verifyDrawable(Drawable who) {
184         return super.verifyDrawable(who) || who == mBackground;
185     }
186 
187     @Override
drawableStateChanged()188     protected void drawableStateChanged() {
189         drawableStateChanged(mBackground);
190     }
191 
drawableStateChanged(Drawable d)192     private void drawableStateChanged(Drawable d) {
193         if (d != null && d.isStateful()) {
194             d.setState(getDrawableState());
195         }
196     }
197 
198     @Override
drawableHotspotChanged(float x, float y)199     public void drawableHotspotChanged(float x, float y) {
200         if (mBackground != null) {
201             mBackground.setHotspot(x, y);
202         }
203     }
204 
openControls( boolean shouldDoCircularReveal, int x, int y, boolean needsFalsingProtection, @Nullable Runnable onAnimationEnd)205     public void openControls(
206             boolean shouldDoCircularReveal,
207             int x,
208             int y,
209             boolean needsFalsingProtection,
210             @Nullable Runnable onAnimationEnd) {
211         animateOpen(shouldDoCircularReveal, x, y, onAnimationEnd);
212         setExposed(true /* exposed */, needsFalsingProtection);
213     }
214 
215     /**
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      */
closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force)223     public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) {
224         if (mGutsContent != null) {
225             if ((mGutsContent.isLeavebehind() && leavebehinds)
226                     || (!mGutsContent.isLeavebehind() && controls)) {
227                 closeControls(x, y, mGutsContent.shouldBeSaved(), force);
228             }
229         }
230     }
231 
232     /**
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      */
closeControls(int x, int y, boolean save, boolean force)240     public void closeControls(int x, int y, boolean save, boolean force) {
241         // First try to dismiss any blocking helper.
242         boolean wasBlockingHelperDismissed =
243                 Dependency.get(NotificationBlockingHelperManager.class)
244                         .dismissCurrentBlockingHelper();
245 
246         if (getWindowToken() == null) {
247             if (mClosedListener != null) {
248                 mClosedListener.onGutsClosed(this);
249             }
250             return;
251         }
252 
253         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 
259             setExposed(false, mNeedsFalsingProtection);
260             if (mClosedListener != null) {
261                 mClosedListener.onGutsClosed(this);
262             }
263         }
264     }
265 
266     /** Animates in the guts view via either a fade or a circular reveal. */
animateOpen( boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd)267     private void animateOpen(
268             boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd) {
269         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                 // Make sure we'll be visible after the circular reveal
275                 setAlpha(1f);
276                 // Circular reveal originating at (x, y)
277                 Animator a = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r);
278                 a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
279                 a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
280                 a.addListener(new AnimateOpenListener(onAnimationEnd));
281                 a.start();
282             } else {
283                 // Fade in content
284                 this.setAlpha(0f);
285                 this.animate()
286                         .alpha(1f)
287                         .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE)
288                         .setInterpolator(Interpolators.ALPHA_IN)
289                         .setListener(new AnimateOpenListener(onAnimationEnd))
290                         .start();
291             }
292         } else {
293             Log.w(TAG, "Failed to animate guts open");
294         }
295     }
296 
297 
298     /** Animates out the guts view via either a fade or a circular reveal. */
299     @VisibleForTesting
animateClose(int x, int y, boolean shouldDoCircularReveal)300     void animateClose(int x, int y, boolean shouldDoCircularReveal) {
301         if (isAttachedToWindow()) {
302             if (shouldDoCircularReveal) {
303                 // Circular reveal originating at (x, y)
304                 if (x == -1 || y == -1) {
305                     x = (getLeft() + getRight()) / 2;
306                     y = (getTop() + getHeight() / 2);
307                 }
308                 double horz = Math.max(getWidth() - x, x);
309                 double vert = Math.max(getHeight() - y, y);
310                 float r = (float) Math.hypot(horz, vert);
311                 Animator a = ViewAnimationUtils.createCircularReveal(this,
312                         x, y, r, 0);
313                 a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
314                 a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
315                 a.addListener(new AnimateCloseListener(this /* view */, mGutsContent));
316                 a.start();
317             } else {
318                 // Fade in the blocking helper.
319                 this.animate()
320                         .alpha(0f)
321                         .setDuration(StackStateAnimator.ANIMATION_DURATION_BLOCKING_HELPER_FADE)
322                         .setInterpolator(Interpolators.ALPHA_OUT)
323                         .setListener(new AnimateCloseListener(this, /* view */mGutsContent))
324                         .start();
325             }
326         } else {
327             Log.w(TAG, "Failed to animate guts close");
328             mGutsContent.onFinishedClosing();
329         }
330     }
331 
setActualHeight(int actualHeight)332     public void setActualHeight(int actualHeight) {
333         mActualHeight = actualHeight;
334         invalidate();
335     }
336 
getActualHeight()337     public int getActualHeight() {
338         return mActualHeight;
339     }
340 
getIntrinsicHeight()341     public int getIntrinsicHeight() {
342         return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight();
343     }
344 
setClipTopAmount(int clipTopAmount)345     public void setClipTopAmount(int clipTopAmount) {
346         mClipTopAmount = clipTopAmount;
347         invalidate();
348     }
349 
setClipBottomAmount(int clipBottomAmount)350     public void setClipBottomAmount(int clipBottomAmount) {
351         mClipBottomAmount = clipBottomAmount;
352         invalidate();
353     }
354 
355     @Override
hasOverlappingRendering()356     public boolean hasOverlappingRendering() {
357         // Prevents this view from creating a layer when alpha is animating.
358         return false;
359     }
360 
setClosedListener(OnGutsClosedListener listener)361     public void setClosedListener(OnGutsClosedListener listener) {
362         mClosedListener = listener;
363     }
364 
setHeightChangedListener(OnHeightChangedListener listener)365     public void setHeightChangedListener(OnHeightChangedListener listener) {
366         mHeightListener = listener;
367     }
368 
onHeightChanged()369     protected void onHeightChanged() {
370         if (mHeightListener != null) {
371             mHeightListener.onHeightChanged(this);
372         }
373     }
374 
375     @VisibleForTesting
setExposed(boolean exposed, boolean needsFalsingProtection)376     void setExposed(boolean exposed, boolean needsFalsingProtection) {
377         final boolean wasExposed = mExposed;
378         mExposed = exposed;
379         mNeedsFalsingProtection = needsFalsingProtection;
380         if (mExposed && mNeedsFalsingProtection) {
381             resetFalsingCheck();
382         } else {
383             mHandler.removeCallbacks(mFalsingCheck);
384         }
385         if (wasExposed != mExposed && mGutsContent != null) {
386             final View contentView = mGutsContent.getContentView();
387             contentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
388             if (mExposed) {
389                 contentView.requestAccessibilityFocus();
390             }
391         }
392     }
393 
willBeRemoved()394     public boolean willBeRemoved() {
395         return mGutsContent != null ? mGutsContent.willBeRemoved() : false;
396     }
397 
isExposed()398     public boolean isExposed() {
399         return mExposed;
400     }
401 
isLeavebehind()402     public boolean isLeavebehind() {
403         return mGutsContent != null && mGutsContent.isLeavebehind();
404     }
405 
406     /** Listener for animations executed in {@link #animateOpen(boolean, int, int, Runnable)}. */
407     private static class AnimateOpenListener extends AnimatorListenerAdapter {
408         final Runnable mOnAnimationEnd;
409 
AnimateOpenListener(Runnable onAnimationEnd)410         private AnimateOpenListener(Runnable onAnimationEnd) {
411             mOnAnimationEnd = onAnimationEnd;
412         }
413 
414         @Override
onAnimationEnd(Animator animation)415         public void onAnimationEnd(Animator animation) {
416             super.onAnimationEnd(animation);
417             if (mOnAnimationEnd != null) {
418                 mOnAnimationEnd.run();
419             }
420         }
421     }
422 
423     /** Listener for animations executed in {@link #animateClose(int, int, boolean)}. */
424     private class AnimateCloseListener extends AnimatorListenerAdapter {
425         final View mView;
426         private final GutsContent mGutsContent;
427 
AnimateCloseListener(View view, GutsContent gutsContent)428         private AnimateCloseListener(View view, GutsContent gutsContent) {
429             mView = view;
430             mGutsContent = gutsContent;
431         }
432 
433         @Override
onAnimationEnd(Animator animation)434         public void onAnimationEnd(Animator animation) {
435             super.onAnimationEnd(animation);
436             if (!isExposed()) {
437                 mView.setVisibility(View.GONE);
438                 mGutsContent.onFinishedClosing();
439             }
440         }
441     }
442 }
443