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
17package com.android.systemui.statusbar;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.app.INotificationManager;
22import android.content.Context;
23import android.content.pm.PackageInfo;
24import android.content.pm.PackageManager;
25import android.content.res.ColorStateList;
26import android.content.res.TypedArray;
27import android.graphics.Canvas;
28import android.graphics.drawable.Drawable;
29import android.os.Handler;
30import android.os.RemoteException;
31import android.os.ServiceManager;
32import android.service.notification.NotificationListenerService;
33import android.service.notification.NotificationListenerService.Ranking;
34import android.service.notification.StatusBarNotification;
35import android.util.AttributeSet;
36import android.view.View;
37import android.view.ViewAnimationUtils;
38import android.widget.ImageView;
39import android.widget.LinearLayout;
40import android.widget.RadioButton;
41import android.widget.RadioGroup;
42import android.widget.SeekBar;
43import android.widget.TextView;
44
45import com.android.internal.logging.MetricsLogger;
46import com.android.internal.logging.MetricsProto.MetricsEvent;
47import com.android.settingslib.Utils;
48import com.android.systemui.Interpolators;
49import com.android.systemui.R;
50import com.android.systemui.statusbar.stack.StackStateAnimator;
51import com.android.systemui.tuner.TunerService;
52
53/**
54 * The guts of a notification revealed when performing a long press.
55 */
56public 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(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 = 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
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