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 { onGutsClosed(NotificationGuts guts)92 public void onGutsClosed(NotificationGuts guts); 93 } 94 NotificationGuts(Context context, AttributeSet attrs)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 onAttachedToWindow()115 protected void onAttachedToWindow() { 116 super.onAttachedToWindow(); 117 TunerService.get(mContext).addTunable(this, SHOW_SLIDER); 118 } 119 120 @Override onDetachedFromWindow()121 protected void onDetachedFromWindow() { 122 TunerService.get(mContext).removeTunable(this); 123 super.onDetachedFromWindow(); 124 } 125 resetFalsingCheck()126 public void resetFalsingCheck() { 127 mHandler.removeCallbacks(mFalsingCheck); 128 if (mNeedsFalsingProtection && mExposed) { 129 mHandler.postDelayed(mFalsingCheck, CLOSE_GUTS_DELAY); 130 } 131 } 132 133 @Override onDraw(Canvas canvas)134 protected void onDraw(Canvas canvas) { 135 draw(canvas, mBackground); 136 } 137 draw(Canvas canvas, Drawable drawable)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 onFinishInflate()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 verifyDrawable(Drawable who)155 protected boolean verifyDrawable(Drawable who) { 156 return super.verifyDrawable(who) || who == mBackground; 157 } 158 159 @Override drawableStateChanged()160 protected void drawableStateChanged() { 161 drawableStateChanged(mBackground); 162 } 163 drawableStateChanged(Drawable d)164 private void drawableStateChanged(Drawable d) { 165 if (d != null && d.isStateful()) { 166 d.setState(getDrawableState()); 167 } 168 } 169 170 @Override drawableHotspotChanged(float x, float y)171 public void drawableHotspotChanged(float x, float y) { 172 if (mBackground != null) { 173 mBackground.setHotspot(x, y); 174 } 175 } 176 bindImportance(final PackageManager pm, final StatusBarNotification sbn, final Set<String> nonBlockablePkgs, final int importance)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 hasImportanceChanged()222 public boolean hasImportanceChanged() { 223 return mStartingUserImportance != getSelectedImportance(); 224 } 225 saveImportance(final StatusBarNotification sbn)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 getSelectedImportance()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 != null && mBlock.isChecked()) { 246 return Ranking.IMPORTANCE_NONE; 247 } else if (mSilent != null && mSilent.isChecked()) { 248 return Ranking.IMPORTANCE_LOW; 249 } else { 250 return Ranking.IMPORTANCE_UNSPECIFIED; 251 } 252 } 253 } 254 bindToggles(final View importanceButtons, final int importance, final boolean nonBlockable)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 bindSlider(final View importanceSlider, final boolean nonBlockable)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 applyAuto()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 updateTitleAndSummary(int progress)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 loadColorStateList(int colorResId)390 private ColorStateList loadColorStateList(int colorResId) { 391 return ColorStateList.valueOf(mContext.getColor(colorResId)); 392 } 393 closeControls(int x, int y, boolean notify)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 setActualHeight(int actualHeight)426 public void setActualHeight(int actualHeight) { 427 mActualHeight = actualHeight; 428 invalidate(); 429 } 430 getActualHeight()431 public int getActualHeight() { 432 return mActualHeight; 433 } 434 setClipTopAmount(int clipTopAmount)435 public void setClipTopAmount(int clipTopAmount) { 436 mClipTopAmount = clipTopAmount; 437 invalidate(); 438 } 439 440 @Override hasOverlappingRendering()441 public boolean hasOverlappingRendering() { 442 // Prevents this view from creating a layer when alpha is animating. 443 return false; 444 } 445 setClosedListener(OnGutsClosedListener listener)446 public void setClosedListener(OnGutsClosedListener listener) { 447 mListener = listener; 448 } 449 setExposed(boolean exposed, boolean needsFalsingProtection)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 areGutsExposed()460 public boolean areGutsExposed() { 461 return mExposed; 462 } 463 464 @Override onTuningChanged(String key, String newValue)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