Home | History | Annotate | Download | only in statusbar
      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 
     63         public void setGutsParent(NotificationGuts listener);
     64 
     65         /**
     66          * Return the view to be shown in the notification guts.
     67          */
     68         public View getContentView();
     69 
     70         /**
     71          * Return the actual height of the content.
     72          */
     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          */
     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          */
     88         public boolean willBeRemoved();
     89 
     90         /**
     91          * Return whether these guts are a leavebehind (e.g. {@link NotificationSnooze}).
     92          */
     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          */
    100         boolean shouldBeSaved();
    101     }
    102 
    103     public interface OnGutsClosedListener {
    104         public void onGutsClosed(NotificationGuts guts);
    105     }
    106 
    107     public interface OnHeightChangedListener {
    108         public void onHeightChanged(NotificationGuts guts);
    109     }
    110 
    111     interface OnSettingsClickListener {
    112         void onClick(View v, int appUid);
    113     }
    114 
    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 
    132     public NotificationGuts(Context context) {
    133         this(context, null);
    134     }
    135 
    136     public void setGutsContent(GutsContent content) {
    137         mGutsContent = content;
    138         removeAllViews();
    139         addView(mGutsContent.getContentView());
    140     }
    141 
    142     public GutsContent getGutsContent() {
    143         return mGutsContent;
    144     }
    145 
    146     public void resetFalsingCheck() {
    147         mHandler.removeCallbacks(mFalsingCheck);
    148         if (mNeedsFalsingProtection && mExposed) {
    149             mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY);
    150         }
    151     }
    152 
    153     @Override
    154     protected void onDraw(Canvas canvas) {
    155         draw(canvas, mBackground);
    156     }
    157 
    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
    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
    177     protected boolean verifyDrawable(Drawable who) {
    178         return super.verifyDrawable(who) || who == mBackground;
    179     }
    180 
    181     @Override
    182     protected void drawableStateChanged() {
    183         drawableStateChanged(mBackground);
    184     }
    185 
    186     private void drawableStateChanged(Drawable d) {
    187         if (d != null && d.isStateful()) {
    188             d.setState(getDrawableState());
    189         }
    190     }
    191 
    192     @Override
    193     public void drawableHotspotChanged(float x, float y) {
    194         if (mBackground != null) {
    195             mBackground.setHotspot(x, y);
    196         }
    197     }
    198 
    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      */
    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      */
    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. */
    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
    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 
    323     public void setActualHeight(int actualHeight) {
    324         mActualHeight = actualHeight;
    325         invalidate();
    326     }
    327 
    328     public int getActualHeight() {
    329         return mActualHeight;
    330     }
    331 
    332     public int getIntrinsicHeight() {
    333         return mGutsContent != null && mExposed ? mGutsContent.getActualHeight() : getHeight();
    334     }
    335 
    336     public void setClipTopAmount(int clipTopAmount) {
    337         mClipTopAmount = clipTopAmount;
    338         invalidate();
    339     }
    340 
    341     public void setClipBottomAmount(int clipBottomAmount) {
    342         mClipBottomAmount = clipBottomAmount;
    343         invalidate();
    344     }
    345 
    346     @Override
    347     public boolean hasOverlappingRendering() {
    348         // Prevents this view from creating a layer when alpha is animating.
    349         return false;
    350     }
    351 
    352     public void setClosedListener(OnGutsClosedListener listener) {
    353         mClosedListener = listener;
    354     }
    355 
    356     public void setHeightChangedListener(OnHeightChangedListener listener) {
    357         mHeightListener = listener;
    358     }
    359 
    360     protected void onHeightChanged() {
    361         if (mHeightListener != null) {
    362             mHeightListener.onHeightChanged(this);
    363         }
    364     }
    365 
    366     @VisibleForTesting
    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 
    385     public boolean willBeRemoved() {
    386         return mGutsContent != null ? mGutsContent.willBeRemoved() : false;
    387     }
    388 
    389     public boolean isExposed() {
    390         return mExposed;
    391     }
    392 
    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 
    401         private AnimateOpenListener(Runnable onAnimationEnd) {
    402             mOnAnimationEnd = onAnimationEnd;
    403         }
    404 
    405         @Override
    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 
    418         private AnimateCloseListener(View view) {
    419             mView = view;
    420         }
    421 
    422         @Override
    423         public void onAnimationEnd(Animator animation) {
    424             super.onAnimationEnd(animation);
    425             mView.setVisibility(View.GONE);
    426         }
    427     }
    428 }
    429