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;
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.support.annotation.Nullable;
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.widget.FrameLayout;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.systemui.Dependency;
36 import com.android.systemui.Interpolators;
37 import com.android.systemui.R;
38 import com.android.systemui.statusbar.stack.StackStateAnimator;
39 
40 /**
41  * The guts of a notification revealed when performing a long press.
42  */
43 public class NotificationGuts extends FrameLayout {
44     private static final String TAG = "NotificationGuts";
45     private static final long CLOSE_GUTS_DELAY = 8000;
46 
47     private Drawable mBackground;
48     private int mClipTopAmount;
49     private int mClipBottomAmount;
50     private int mActualHeight;
51     private boolean mExposed;
52 
53     private Handler mHandler;
54     private Runnable mFalsingCheck;
55     private boolean mNeedsFalsingProtection;
56     private OnGutsClosedListener mClosedListener;
57     private OnHeightChangedListener mHeightListener;
58 
59     private GutsContent mGutsContent;
60 
61     public interface GutsContent {
62 
setGutsParent(NotificationGuts listener)63         public void setGutsParent(NotificationGuts listener);
64 
65         /**
66          * Return the view to be shown in the notification guts.
67          */
getContentView()68         public View getContentView();
69 
70         /**
71          * Return the actual height of the content.
72          */
getActualHeight()73         public int getActualHeight();
74 
75         /**
76          * Called when the guts view have been told to close, typically after an outside
77          * 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.
82          */
handleCloseControls(boolean save, boolean force)83         public boolean handleCloseControls(boolean save, boolean force);
84 
85         /**
86          * Return whether the notification associated with these guts is set to be removed.
87          */
willBeRemoved()88         public boolean willBeRemoved();
89 
90         /**
91          * Return whether these guts are a leavebehind (e.g. {@link NotificationSnooze}).
92          */
isLeavebehind()93         public default boolean isLeavebehind() {
94             return false;
95         }
96 
97         /**
98          * Return whether something changed and needs to be saved, possibly requiring a bouncer.
99          */
shouldBeSaved()100         boolean shouldBeSaved();
101     }
102 
103     public interface OnGutsClosedListener {
onGutsClosed(NotificationGuts guts)104         public void onGutsClosed(NotificationGuts guts);
105     }
106 
107     public interface OnHeightChangedListener {
onHeightChanged(NotificationGuts guts)108         public void onHeightChanged(NotificationGuts guts);
109     }
110 
111     interface OnSettingsClickListener {
onClick(View v, int appUid)112         void onClick(View v, int appUid);
113     }
114 
NotificationGuts(Context context, AttributeSet attrs)115     public NotificationGuts(Context context, AttributeSet attrs) {
116         super(context, attrs);
117         setWillNotDraw(false);
118         mHandler = new Handler();
119         mFalsingCheck = new Runnable() {
120             @Override
121             public void run() {
122                 if (mNeedsFalsingProtection && mExposed) {
123                     closeControls(-1 /* x */, -1 /* y */, false /* save */, false /* force */);
124                 }
125             }
126         };
127         final TypedArray ta = context.obtainStyledAttributes(attrs,
128                 com.android.internal.R.styleable.Theme, 0, 0);
129         ta.recycle();
130     }
131 
NotificationGuts(Context context)132     public NotificationGuts(Context context) {
133         this(context, null);
134     }
135 
setGutsContent(GutsContent content)136     public void setGutsContent(GutsContent content) {
137         mGutsContent = content;
138         removeAllViews();
139         addView(mGutsContent.getContentView());
140     }
141 
getGutsContent()142     public GutsContent getGutsContent() {
143         return mGutsContent;
144     }
145 
resetFalsingCheck()146     public void resetFalsingCheck() {
147         mHandler.removeCallbacks(mFalsingCheck);
148         if (mNeedsFalsingProtection && mExposed) {
149             mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY);
150         }
151     }
152 
153     @Override
onDraw(Canvas canvas)154     protected void onDraw(Canvas canvas) {
155         draw(canvas, mBackground);
156     }
157 
draw(Canvas canvas, Drawable drawable)158     private void draw(Canvas canvas, Drawable drawable) {
159         int top = mClipTopAmount;
160         int bottom = mActualHeight - mClipBottomAmount;
161         if (drawable != null && top < bottom) {
162             drawable.setBounds(0, top, getWidth(), bottom);
163             drawable.draw(canvas);
164         }
165     }
166 
167     @Override
onFinishInflate()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
verifyDrawable(Drawable who)177     protected boolean verifyDrawable(Drawable who) {
178         return super.verifyDrawable(who) || who == mBackground;
179     }
180 
181     @Override
drawableStateChanged()182     protected void drawableStateChanged() {
183         drawableStateChanged(mBackground);
184     }
185 
drawableStateChanged(Drawable d)186     private void drawableStateChanged(Drawable d) {
187         if (d != null && d.isStateful()) {
188             d.setState(getDrawableState());
189         }
190     }
191 
192     @Override
drawableHotspotChanged(float x, float y)193     public void drawableHotspotChanged(float x, float y) {
194         if (mBackground != null) {
195             mBackground.setHotspot(x, y);
196         }
197     }
198 
openControls( boolean shouldDoCircularReveal, int x, int y, boolean needsFalsingProtection, @Nullable Runnable onAnimationEnd)199     public void openControls(
200             boolean shouldDoCircularReveal,
201             int x,
202             int y,
203             boolean needsFalsingProtection,
204             @Nullable Runnable onAnimationEnd) {
205         animateOpen(shouldDoCircularReveal, x, y, onAnimationEnd);
206         setExposed(true /* exposed */, needsFalsingProtection);
207     }
208 
209     /**
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      */
closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force)217     public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) {
218         if (mGutsContent != null) {
219             if ((mGutsContent.isLeavebehind() && leavebehinds)
220                     || (!mGutsContent.isLeavebehind() && controls)) {
221                 closeControls(x, y, mGutsContent.shouldBeSaved(), force);
222             }
223         }
224     }
225 
226     /**
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      */
closeControls(int x, int y, boolean save, boolean force)234     public void closeControls(int x, int y, boolean save, boolean force) {
235         // First try to dismiss any blocking helper.
236         boolean wasBlockingHelperDismissed =
237                 Dependency.get(NotificationBlockingHelperManager.class)
238                         .dismissCurrentBlockingHelper();
239 
240         if (getWindowToken() == null) {
241             if (mClosedListener != null) {
242                 mClosedListener.onGutsClosed(this);
243             }
244             return;
245         }
246 
247         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 
253             setExposed(false, mNeedsFalsingProtection);
254             if (mClosedListener != null) {
255                 mClosedListener.onGutsClosed(this);
256             }
257         }
258     }
259 
260     /** Animates in the guts view via either a fade or a circular reveal. */
animateOpen( boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd)261     private void animateOpen(
262             boolean shouldDoCircularReveal, int x, int y, @Nullable Runnable onAnimationEnd) {
263         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             }
284         } else {
285             Log.w(TAG, "Failed to animate guts open");
286         }
287     }
288 
289 
290     /** Animates out the guts view via either a fade or a circular reveal. */
291     @VisibleForTesting
animateClose(int x, int y, boolean shouldDoCircularReveal)292     void animateClose(int x, int y, boolean shouldDoCircularReveal) {
293         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();
317             }
318         } else {
319             Log.w(TAG, "Failed to animate guts close");
320         }
321     }
322 
setActualHeight(int actualHeight)323     public void setActualHeight(int actualHeight) {
324         mActualHeight = actualHeight;
325         invalidate();
326     }
327 
getActualHeight()328     public int getActualHeight() {
329         return mActualHeight;
330     }
331 
getIntrinsicHeight()332     public int getIntrinsicHeight() {
333         return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight();
334     }
335 
setClipTopAmount(int clipTopAmount)336     public void setClipTopAmount(int clipTopAmount) {
337         mClipTopAmount = clipTopAmount;
338         invalidate();
339     }
340 
setClipBottomAmount(int clipBottomAmount)341     public void setClipBottomAmount(int clipBottomAmount) {
342         mClipBottomAmount = clipBottomAmount;
343         invalidate();
344     }
345 
346     @Override
hasOverlappingRendering()347     public boolean hasOverlappingRendering() {
348         // Prevents this view from creating a layer when alpha is animating.
349         return false;
350     }
351 
setClosedListener(OnGutsClosedListener listener)352     public void setClosedListener(OnGutsClosedListener listener) {
353         mClosedListener = listener;
354     }
355 
setHeightChangedListener(OnHeightChangedListener listener)356     public void setHeightChangedListener(OnHeightChangedListener listener) {
357         mHeightListener = listener;
358     }
359 
onHeightChanged()360     protected void onHeightChanged() {
361         if (mHeightListener != null) {
362             mHeightListener.onHeightChanged(this);
363         }
364     }
365 
366     @VisibleForTesting
setExposed(boolean exposed, boolean needsFalsingProtection)367     void setExposed(boolean exposed, boolean needsFalsingProtection) {
368         final boolean wasExposed = mExposed;
369         mExposed = exposed;
370         mNeedsFalsingProtection = needsFalsingProtection;
371         if (mExposed && mNeedsFalsingProtection) {
372             resetFalsingCheck();
373         } else {
374             mHandler.removeCallbacks(mFalsingCheck);
375         }
376         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         }
383     }
384 
willBeRemoved()385     public boolean willBeRemoved() {
386         return mGutsContent != null ? mGutsContent.willBeRemoved() : false;
387     }
388 
isExposed()389     public boolean isExposed() {
390         return mExposed;
391     }
392 
isLeavebehind()393     public boolean isLeavebehind() {
394         return mGutsContent != null && mGutsContent.isLeavebehind();
395     }
396 
397     /** Listener for animations executed in {@link #animateOpen(boolean, int, int, Runnable)}. */
398     private static class AnimateOpenListener extends AnimatorListenerAdapter {
399         final Runnable mOnAnimationEnd;
400 
AnimateOpenListener(Runnable onAnimationEnd)401         private AnimateOpenListener(Runnable onAnimationEnd) {
402             mOnAnimationEnd = onAnimationEnd;
403         }
404 
405         @Override
onAnimationEnd(Animator animation)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 
AnimateCloseListener(View view)418         private AnimateCloseListener(View view) {
419             mView = view;
420         }
421 
422         @Override
onAnimationEnd(Animator animation)423         public void onAnimationEnd(Animator animation) {
424             super.onAnimationEnd(animation);
425             mView.setVisibility(View.GONE);
426         }
427     }
428 }
429