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