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