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 {
onGutsClosed(NotificationGuts guts)90         public void onGutsClosed(NotificationGuts guts);
91     }
92 
NotificationGuts(Context context, AttributeSet attrs)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
onAttachedToWindow()113     protected void onAttachedToWindow() {
114         super.onAttachedToWindow();
115         TunerService.get(mContext).addTunable(this, SHOW_SLIDER);
116     }
117 
118     @Override
onDetachedFromWindow()119     protected void onDetachedFromWindow() {
120         TunerService.get(mContext).removeTunable(this);
121         super.onDetachedFromWindow();
122     }
123 
resetFalsingCheck()124     public void resetFalsingCheck() {
125         mHandler.removeCallbacks(mFalsingCheck);
126         if (mNeedsFalsingProtection && mExposed) {
127             mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY);
128         }
129     }
130 
131     @Override
onDraw(Canvas canvas)132     protected void onDraw(Canvas canvas) {
133         draw(canvas, mBackground);
134     }
135 
draw(Canvas canvas, Drawable drawable)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
onFinishInflate()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
verifyDrawable(Drawable who)153     protected boolean verifyDrawable(Drawable who) {
154         return super.verifyDrawable(who) || who == mBackground;
155     }
156 
157     @Override
drawableStateChanged()158     protected void drawableStateChanged() {
159         drawableStateChanged(mBackground);
160     }
161 
drawableStateChanged(Drawable d)162     private void drawableStateChanged(Drawable d) {
163         if (d != null && d.isStateful()) {
164             d.setState(getDrawableState());
165         }
166     }
167 
168     @Override
drawableHotspotChanged(float x, float y)169     public void drawableHotspotChanged(float x, float y) {
170         if (mBackground != null) {
171             mBackground.setHotspot(x, y);
172         }
173     }
174 
bindImportance(final PackageManager pm, final StatusBarNotification sbn, final int importance)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(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 
hasImportanceChanged()208     public boolean hasImportanceChanged() {
209         return mStartingUserImportance != getSelectedImportance();
210     }
211 
saveImportance(final StatusBarNotification sbn)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 
getSelectedImportance()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 
bindToggles(final View importanceButtons, final int importance, final boolean systemApp)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 
bindSlider(final View importanceSlider, final boolean systemApp)268     private void bindSlider(final View importanceSlider, final boolean systemApp) {
269         mActiveSliderTint = loadColorStateList(R.color.notification_guts_slider_color);
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 
applyAuto()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 
updateTitleAndSummary(int progress)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 
loadColorStateList(int colorResId)376     private ColorStateList loadColorStateList(int colorResId) {
377         return ColorStateList.valueOf(mContext.getColor(colorResId));
378     }
379 
closeControls(int x, int y, boolean notify)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 
setActualHeight(int actualHeight)412     public void setActualHeight(int actualHeight) {
413         mActualHeight = actualHeight;
414         invalidate();
415     }
416 
getActualHeight()417     public int getActualHeight() {
418         return mActualHeight;
419     }
420 
setClipTopAmount(int clipTopAmount)421     public void setClipTopAmount(int clipTopAmount) {
422         mClipTopAmount = clipTopAmount;
423         invalidate();
424     }
425 
426     @Override
hasOverlappingRendering()427     public boolean hasOverlappingRendering() {
428         // Prevents this view from creating a layer when alpha is animating.
429         return false;
430     }
431 
setClosedListener(OnGutsClosedListener listener)432     public void setClosedListener(OnGutsClosedListener listener) {
433         mListener = listener;
434     }
435 
setExposed(boolean exposed, boolean needsFalsingProtection)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 
areGutsExposed()446     public boolean areGutsExposed() {
447         return mExposed;
448     }
449 
450     @Override
onTuningChanged(String key, String newValue)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