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.app.INotificationManager;
     22 import android.content.Context;
     23 import android.content.pm.PackageInfo;
     24 import android.content.pm.PackageManager;
     25 import android.content.res.ColorStateList;
     26 import android.content.res.TypedArray;
     27 import android.graphics.Canvas;
     28 import android.graphics.drawable.Drawable;
     29 import android.os.Handler;
     30 import android.os.RemoteException;
     31 import android.os.ServiceManager;
     32 import android.service.notification.NotificationListenerService;
     33 import android.service.notification.NotificationListenerService.Ranking;
     34 import android.service.notification.StatusBarNotification;
     35 import android.util.AttributeSet;
     36 import android.view.View;
     37 import android.view.ViewAnimationUtils;
     38 import android.widget.ImageView;
     39 import android.widget.LinearLayout;
     40 import android.widget.RadioButton;
     41 import android.widget.RadioGroup;
     42 import android.widget.SeekBar;
     43 import android.widget.TextView;
     44 
     45 import com.android.internal.logging.MetricsLogger;
     46 import com.android.internal.logging.MetricsProto.MetricsEvent;
     47 import com.android.settingslib.Utils;
     48 import com.android.systemui.Interpolators;
     49 import com.android.systemui.R;
     50 import com.android.systemui.statusbar.stack.StackStateAnimator;
     51 import com.android.systemui.tuner.TunerService;
     52 
     53 import java.util.Set;
     54 
     55 /**
     56  * The guts of a notification revealed when performing a long press.
     57  */
     58 public class NotificationGuts extends LinearLayout implements TunerService.Tunable {
     59     public static final String SHOW_SLIDER = "show_importance_slider";
     60 
     61     private static final long CLOSE_GUTS_DELAY = 8000;
     62 
     63     private Drawable mBackground;
     64     private int mClipTopAmount;
     65     private int mActualHeight;
     66     private boolean mExposed;
     67     private INotificationManager mINotificationManager;
     68     private int mStartingUserImportance;
     69     private int mNotificationImportance;
     70     private boolean mShowSlider;
     71 
     72     private SeekBar mSeekBar;
     73     private ImageView mAutoButton;
     74     private ColorStateList mActiveSliderTint;
     75     private ColorStateList mInactiveSliderTint;
     76     private float mActiveSliderAlpha = 1.0f;
     77     private float mInactiveSliderAlpha;
     78     private TextView mImportanceSummary;
     79     private TextView mImportanceTitle;
     80     private boolean mAuto;
     81 
     82     private RadioButton mBlock;
     83     private RadioButton mSilent;
     84     private RadioButton mReset;
     85 
     86     private Handler mHandler;
     87     private Runnable mFalsingCheck;
     88     private boolean mNeedsFalsingProtection;
     89     private OnGutsClosedListener mListener;
     90 
     91     public interface OnGutsClosedListener {
     92         public void onGutsClosed(NotificationGuts guts);
     93     }
     94 
     95     public NotificationGuts(Context context, AttributeSet attrs) {
     96         super(context, attrs);
     97         setWillNotDraw(false);
     98         mHandler = new Handler();
     99         mFalsingCheck = new Runnable() {
    100             @Override
    101             public void run() {
    102                 if (mNeedsFalsingProtection && mExposed) {
    103                     closeControls(-1 /* x */, -1 /* y */, true /* notify */);
    104                 }
    105             }
    106         };
    107         final TypedArray ta =
    108                 context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Theme, 0, 0);
    109         mInactiveSliderAlpha =
    110                 ta.getFloat(com.android.internal.R.styleable.Theme_disabledAlpha, 0.5f);
    111         ta.recycle();
    112     }
    113 
    114     @Override
    115     protected void onAttachedToWindow() {
    116         super.onAttachedToWindow();
    117         TunerService.get(mContext).addTunable(this, SHOW_SLIDER);
    118     }
    119 
    120     @Override
    121     protected void onDetachedFromWindow() {
    122         TunerService.get(mContext).removeTunable(this);
    123         super.onDetachedFromWindow();
    124     }
    125 
    126     public void resetFalsingCheck() {
    127         mHandler.removeCallbacks(mFalsingCheck);
    128         if (mNeedsFalsingProtection && mExposed) {
    129             mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY);
    130         }
    131     }
    132 
    133     @Override
    134     protected void onDraw(Canvas canvas) {
    135         draw(canvas, mBackground);
    136     }
    137 
    138     private void draw(Canvas canvas, Drawable drawable) {
    139         if (drawable != null) {
    140             drawable.setBounds(0, mClipTopAmount, getWidth(), mActualHeight);
    141             drawable.draw(canvas);
    142         }
    143     }
    144 
    145     @Override
    146     protected void onFinishInflate() {
    147         super.onFinishInflate();
    148         mBackground = mContext.getDrawable(R.drawable.notification_guts_bg);
    149         if (mBackground != null) {
    150             mBackground.setCallback(this);
    151         }
    152     }
    153 
    154     @Override
    155     protected boolean verifyDrawable(Drawable who) {
    156         return super.verifyDrawable(who) || who == mBackground;
    157     }
    158 
    159     @Override
    160     protected void drawableStateChanged() {
    161         drawableStateChanged(mBackground);
    162     }
    163 
    164     private void drawableStateChanged(Drawable d) {
    165         if (d != null && d.isStateful()) {
    166             d.setState(getDrawableState());
    167         }
    168     }
    169 
    170     @Override
    171     public void drawableHotspotChanged(float x, float y) {
    172         if (mBackground != null) {
    173             mBackground.setHotspot(x, y);
    174         }
    175     }
    176 
    177     void bindImportance(final PackageManager pm, final StatusBarNotification sbn,
    178             final Set<String> nonBlockablePkgs, final int importance) {
    179         mINotificationManager = INotificationManager.Stub.asInterface(
    180                 ServiceManager.getService(Context.NOTIFICATION_SERVICE));
    181         mStartingUserImportance = NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED;
    182         try {
    183             mStartingUserImportance =
    184                     mINotificationManager.getImportance(sbn.getPackageName(), sbn.getUid());
    185         } catch (RemoteException e) {}
    186         mNotificationImportance = importance;
    187 
    188         final View importanceSlider = findViewById(R.id.importance_slider);
    189         final View importanceButtons = findViewById(R.id.importance_buttons);
    190         final View cantTouchThis = findViewById(R.id.cant_silence_or_block);
    191 
    192         final boolean essentialPackage =
    193                 (nonBlockablePkgs != null && nonBlockablePkgs.contains(sbn.getPackageName()));
    194         if (essentialPackage) {
    195             importanceButtons.setVisibility(View.GONE);
    196             importanceSlider.setVisibility(View.GONE);
    197             cantTouchThis.setVisibility(View.VISIBLE);
    198         } else {
    199             cantTouchThis.setVisibility(View.GONE);
    200 
    201             boolean nonBlockable = false;
    202             try {
    203                 final PackageInfo info =
    204                         pm.getPackageInfo(sbn.getPackageName(), PackageManager.GET_SIGNATURES);
    205                 nonBlockable = Utils.isSystemPackage(getResources(), pm, info);
    206             } catch (PackageManager.NameNotFoundException e) {
    207                 // unlikely.
    208             }
    209 
    210             if (mShowSlider) {
    211                 bindSlider(importanceSlider, nonBlockable);
    212                 importanceSlider.setVisibility(View.VISIBLE);
    213                 importanceButtons.setVisibility(View.GONE);
    214             } else {
    215                 bindToggles(importanceButtons, mStartingUserImportance, nonBlockable);
    216                 importanceButtons.setVisibility(View.VISIBLE);
    217                 importanceSlider.setVisibility(View.GONE);
    218             }
    219         }
    220     }
    221 
    222     public boolean hasImportanceChanged() {
    223         return mStartingUserImportance != getSelectedImportance();
    224     }
    225 
    226     void saveImportance(final StatusBarNotification sbn) {
    227         int progress = getSelectedImportance();
    228         MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
    229                 progress - mStartingUserImportance);
    230         try {
    231             mINotificationManager.setImportance(sbn.getPackageName(), sbn.getUid(), progress);
    232         } catch (RemoteException e) {
    233             // :(
    234         }
    235     }
    236 
    237     private int getSelectedImportance() {
    238         if (mSeekBar!= null && mSeekBar.isShown()) {
    239             if (mSeekBar.isEnabled()) {
    240                 return mSeekBar.getProgress();
    241             } else {
    242                 return Ranking.IMPORTANCE_UNSPECIFIED;
    243             }
    244         } else {
    245             if (mBlock.isChecked()) {
    246                 return Ranking.IMPORTANCE_NONE;
    247             } else if (mSilent.isChecked()) {
    248                 return Ranking.IMPORTANCE_LOW;
    249             } else {
    250                 return Ranking.IMPORTANCE_UNSPECIFIED;
    251             }
    252         }
    253     }
    254 
    255     private void bindToggles(final View importanceButtons, final int importance,
    256             final boolean nonBlockable) {
    257         ((RadioGroup) importanceButtons).setOnCheckedChangeListener(
    258                 new RadioGroup.OnCheckedChangeListener() {
    259                     @Override
    260                     public void onCheckedChanged(RadioGroup group, int checkedId) {
    261                         resetFalsingCheck();
    262                     }
    263                 });
    264         mBlock = (RadioButton) importanceButtons.findViewById(R.id.block_importance);
    265         mSilent = (RadioButton) importanceButtons.findViewById(R.id.silent_importance);
    266         mReset = (RadioButton) importanceButtons.findViewById(R.id.reset_importance);
    267         if (nonBlockable) {
    268             mBlock.setVisibility(View.GONE);
    269             mReset.setText(mContext.getString(R.string.do_not_silence));
    270         } else {
    271             mReset.setText(mContext.getString(R.string.do_not_silence_block));
    272         }
    273         mBlock.setText(mContext.getString(R.string.block));
    274         mSilent.setText(mContext.getString(R.string.show_silently));
    275         if (importance == NotificationListenerService.Ranking.IMPORTANCE_LOW) {
    276             mSilent.setChecked(true);
    277         } else {
    278             mReset.setChecked(true);
    279         }
    280     }
    281 
    282     private void bindSlider(final View importanceSlider, final boolean nonBlockable) {
    283         mActiveSliderTint = ColorStateList.valueOf(Utils.getColorAccent(mContext));
    284         mInactiveSliderTint = loadColorStateList(R.color.notification_guts_disabled_slider_color);
    285 
    286         mImportanceSummary = ((TextView) importanceSlider.findViewById(R.id.summary));
    287         mImportanceTitle = ((TextView) importanceSlider.findViewById(R.id.title));
    288         mSeekBar = (SeekBar) importanceSlider.findViewById(R.id.seekbar);
    289 
    290         final int minProgress = nonBlockable ?
    291                 NotificationListenerService.Ranking.IMPORTANCE_MIN
    292                 : NotificationListenerService.Ranking.IMPORTANCE_NONE;
    293         mSeekBar.setMax(NotificationListenerService.Ranking.IMPORTANCE_MAX);
    294         mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    295             @Override
    296             public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    297                 resetFalsingCheck();
    298                 if (progress < minProgress) {
    299                     seekBar.setProgress(minProgress);
    300                     progress = minProgress;
    301                 }
    302                 updateTitleAndSummary(progress);
    303                 if (fromUser) {
    304                     MetricsLogger.action(mContext, MetricsEvent.ACTION_MODIFY_IMPORTANCE_SLIDER);
    305                 }
    306             }
    307 
    308             @Override
    309             public void onStartTrackingTouch(SeekBar seekBar) {
    310                 resetFalsingCheck();
    311             }
    312 
    313             @Override
    314             public void onStopTrackingTouch(SeekBar seekBar) {
    315                 // no-op
    316             }
    317 
    318 
    319         });
    320         mSeekBar.setProgress(mNotificationImportance);
    321 
    322         mAutoButton = (ImageView) importanceSlider.findViewById(R.id.auto_importance);
    323         mAutoButton.setOnClickListener(new OnClickListener() {
    324             @Override
    325             public void onClick(View v) {
    326                 mAuto = !mAuto;
    327                 applyAuto();
    328             }
    329         });
    330         mAuto = mStartingUserImportance == Ranking.IMPORTANCE_UNSPECIFIED;
    331         applyAuto();
    332     }
    333 
    334     private void applyAuto() {
    335         mSeekBar.setEnabled(!mAuto);
    336 
    337         final ColorStateList starTint = mAuto ?  mActiveSliderTint : mInactiveSliderTint;
    338         final float alpha = mAuto ? mInactiveSliderAlpha : mActiveSliderAlpha;
    339         Drawable icon = mAutoButton.getDrawable().mutate();
    340         icon.setTintList(starTint);
    341         mAutoButton.setImageDrawable(icon);
    342         mSeekBar.setAlpha(alpha);
    343 
    344         if (mAuto) {
    345             mSeekBar.setProgress(mNotificationImportance);
    346             mImportanceSummary.setText(mContext.getString(
    347                     R.string.notification_importance_user_unspecified));
    348             mImportanceTitle.setText(mContext.getString(
    349                     R.string.user_unspecified_importance));
    350         } else {
    351             updateTitleAndSummary(mSeekBar.getProgress());
    352         }
    353     }
    354 
    355     private void updateTitleAndSummary(int progress) {
    356         switch (progress) {
    357             case Ranking.IMPORTANCE_NONE:
    358                 mImportanceSummary.setText(mContext.getString(
    359                         R.string.notification_importance_blocked));
    360                 mImportanceTitle.setText(mContext.getString(R.string.blocked_importance));
    361                 break;
    362             case Ranking.IMPORTANCE_MIN:
    363                 mImportanceSummary.setText(mContext.getString(
    364                         R.string.notification_importance_min));
    365                 mImportanceTitle.setText(mContext.getString(R.string.min_importance));
    366                 break;
    367             case Ranking.IMPORTANCE_LOW:
    368                 mImportanceSummary.setText(mContext.getString(
    369                         R.string.notification_importance_low));
    370                 mImportanceTitle.setText(mContext.getString(R.string.low_importance));
    371                 break;
    372             case Ranking.IMPORTANCE_DEFAULT:
    373                 mImportanceSummary.setText(mContext.getString(
    374                         R.string.notification_importance_default));
    375                 mImportanceTitle.setText(mContext.getString(R.string.default_importance));
    376                 break;
    377             case Ranking.IMPORTANCE_HIGH:
    378                 mImportanceSummary.setText(mContext.getString(
    379                         R.string.notification_importance_high));
    380                 mImportanceTitle.setText(mContext.getString(R.string.high_importance));
    381                 break;
    382             case Ranking.IMPORTANCE_MAX:
    383                 mImportanceSummary.setText(mContext.getString(
    384                         R.string.notification_importance_max));
    385                 mImportanceTitle.setText(mContext.getString(R.string.max_importance));
    386                 break;
    387         }
    388     }
    389 
    390     private ColorStateList loadColorStateList(int colorResId) {
    391         return ColorStateList.valueOf(mContext.getColor(colorResId));
    392     }
    393 
    394     public void closeControls(int x, int y, boolean notify) {
    395         if (getWindowToken() == null) {
    396             if (notify && mListener != null) {
    397                 mListener.onGutsClosed(this);
    398             }
    399             return;
    400         }
    401         if (x == -1 || y == -1) {
    402             x = (getLeft() + getRight()) / 2;
    403             y = (getTop() + getHeight() / 2);
    404         }
    405         final double horz = Math.max(getWidth() - x, x);
    406         final double vert = Math.max(getHeight() - y, y);
    407         final float r = (float) Math.hypot(horz, vert);
    408         final Animator a = ViewAnimationUtils.createCircularReveal(this,
    409                 x, y, r, 0);
    410         a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
    411         a.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
    412         a.addListener(new AnimatorListenerAdapter() {
    413             @Override
    414             public void onAnimationEnd(Animator animation) {
    415                 super.onAnimationEnd(animation);
    416                 setVisibility(View.GONE);
    417             }
    418         });
    419         a.start();
    420         setExposed(false, mNeedsFalsingProtection);
    421         if (notify && mListener != null) {
    422             mListener.onGutsClosed(this);
    423         }
    424     }
    425 
    426     public void setActualHeight(int actualHeight) {
    427         mActualHeight = actualHeight;
    428         invalidate();
    429     }
    430 
    431     public int getActualHeight() {
    432         return mActualHeight;
    433     }
    434 
    435     public void setClipTopAmount(int clipTopAmount) {
    436         mClipTopAmount = clipTopAmount;
    437         invalidate();
    438     }
    439 
    440     @Override
    441     public boolean hasOverlappingRendering() {
    442         // Prevents this view from creating a layer when alpha is animating.
    443         return false;
    444     }
    445 
    446     public void setClosedListener(OnGutsClosedListener listener) {
    447         mListener = listener;
    448     }
    449 
    450     public void setExposed(boolean exposed, boolean needsFalsingProtection) {
    451         mExposed = exposed;
    452         mNeedsFalsingProtection = needsFalsingProtection;
    453         if (mExposed && mNeedsFalsingProtection) {
    454             resetFalsingCheck();
    455         } else {
    456             mHandler.removeCallbacks(mFalsingCheck);
    457         }
    458     }
    459 
    460     public boolean areGutsExposed() {
    461         return mExposed;
    462     }
    463 
    464     @Override
    465     public void onTuningChanged(String key, String newValue) {
    466         if (SHOW_SLIDER.equals(key)) {
    467             mShowSlider = newValue != null && Integer.parseInt(newValue) != 0;
    468         }
    469     }
    470 }
    471