1 /* 2 * Copyright (C) 2017 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.notification.row; 18 19 import static android.app.NotificationManager.IMPORTANCE_DEFAULT; 20 import static android.app.NotificationManager.IMPORTANCE_LOW; 21 import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; 22 23 import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN; 24 25 import static java.lang.annotation.RetentionPolicy.SOURCE; 26 27 import android.animation.Animator; 28 import android.animation.AnimatorListenerAdapter; 29 import android.animation.AnimatorSet; 30 import android.animation.ObjectAnimator; 31 import android.annotation.IntDef; 32 import android.annotation.Nullable; 33 import android.app.INotificationManager; 34 import android.app.Notification; 35 import android.app.NotificationChannel; 36 import android.app.NotificationChannelGroup; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.pm.ActivityInfo; 40 import android.content.pm.ApplicationInfo; 41 import android.content.pm.PackageManager; 42 import android.content.pm.ResolveInfo; 43 import android.graphics.drawable.Drawable; 44 import android.metrics.LogMaker; 45 import android.os.Handler; 46 import android.os.RemoteException; 47 import android.service.notification.StatusBarNotification; 48 import android.text.TextUtils; 49 import android.transition.ChangeBounds; 50 import android.transition.Fade; 51 import android.transition.TransitionManager; 52 import android.transition.TransitionSet; 53 import android.util.AttributeSet; 54 import android.util.Log; 55 import android.view.View; 56 import android.view.ViewGroup; 57 import android.view.accessibility.AccessibilityEvent; 58 import android.widget.ImageView; 59 import android.widget.LinearLayout; 60 import android.widget.TextView; 61 62 import com.android.internal.annotations.VisibleForTesting; 63 import com.android.internal.logging.MetricsLogger; 64 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 65 import com.android.systemui.Dependency; 66 import com.android.systemui.Interpolators; 67 import com.android.systemui.R; 68 import com.android.systemui.statusbar.notification.VisualStabilityManager; 69 import com.android.systemui.statusbar.notification.logging.NotificationCounters; 70 71 import java.lang.annotation.Retention; 72 import java.util.List; 73 import java.util.Set; 74 75 /** 76 * The guts of a notification revealed when performing a long press. This also houses the blocking 77 * helper affordance that allows a user to keep/stop notifications after swiping one away. 78 */ 79 public class NotificationInfo extends LinearLayout implements NotificationGuts.GutsContent { 80 private static final String TAG = "InfoGuts"; 81 82 @IntDef(prefix = { "ACTION_" }, value = { 83 ACTION_NONE, 84 ACTION_UNDO, 85 ACTION_TOGGLE_SILENT, 86 ACTION_BLOCK, 87 }) 88 public @interface NotificationInfoAction { 89 } 90 91 public static final int ACTION_NONE = 0; 92 static final int ACTION_UNDO = 1; 93 // standard controls 94 static final int ACTION_TOGGLE_SILENT = 2; 95 // unused 96 static final int ACTION_BLOCK = 3; 97 // blocking helper 98 static final int ACTION_DELIVER_SILENTLY = 4; 99 // standard controls 100 private static final int ACTION_ALERT = 5; 101 102 private TextView mPriorityDescriptionView; 103 private TextView mSilentDescriptionView; 104 105 private INotificationManager mINotificationManager; 106 private PackageManager mPm; 107 private MetricsLogger mMetricsLogger; 108 private VisualStabilityManager mVisualStabilityManager; 109 private ChannelEditorDialogController mChannelEditorDialogController; 110 111 private String mPackageName; 112 private String mAppName; 113 private int mAppUid; 114 private String mDelegatePkg; 115 private int mNumUniqueChannelsInRow; 116 private Set<NotificationChannel> mUniqueChannelsInRow; 117 private NotificationChannel mSingleNotificationChannel; 118 private int mStartingChannelImportance; 119 private boolean mWasShownHighPriority; 120 private boolean mPressedApply; 121 private boolean mPresentingChannelEditorDialog = false; 122 123 /** 124 * The last importance level chosen by the user. Null if the user has not chosen an importance 125 * level; non-null once the user takes an action which indicates an explicit preference. 126 */ 127 @Nullable private Integer mChosenImportance; 128 private boolean mIsSingleDefaultChannel; 129 private boolean mIsNonblockable; 130 private StatusBarNotification mSbn; 131 private AnimatorSet mExpandAnimation; 132 private boolean mIsDeviceProvisioned; 133 134 private CheckSaveListener mCheckSaveListener; 135 private OnSettingsClickListener mOnSettingsClickListener; 136 private OnAppSettingsClickListener mAppSettingsClickListener; 137 private NotificationGuts mGutsContainer; 138 private Drawable mPkgIcon; 139 140 /** Whether this view is being shown as part of the blocking helper. */ 141 private boolean mIsForBlockingHelper; 142 143 /** 144 * String that describes how the user exit or quit out of this view, also used as a counter tag. 145 */ 146 private String mExitReason = NotificationCounters.BLOCKING_HELPER_DISMISSED; 147 148 // used by standard ui 149 private OnClickListener mOnAlert = v -> { 150 mExitReason = NotificationCounters.BLOCKING_HELPER_KEEP_SHOWING; 151 mChosenImportance = IMPORTANCE_DEFAULT; 152 applyAlertingBehavior(BEHAVIOR_ALERTING, true /* userTriggered */); 153 }; 154 155 // used by standard ui 156 private OnClickListener mOnSilent = v -> { 157 mExitReason = NotificationCounters.BLOCKING_HELPER_DELIVER_SILENTLY; 158 mChosenImportance = IMPORTANCE_LOW; 159 applyAlertingBehavior(BEHAVIOR_SILENT, true /* userTriggered */); 160 }; 161 162 // used by standard ui 163 private OnClickListener mOnDismissSettings = v -> { 164 mPressedApply = true; 165 closeControls(v, true); 166 }; 167 168 // used by blocking helper 169 private OnClickListener mOnKeepShowing = v -> { 170 mExitReason = NotificationCounters.BLOCKING_HELPER_KEEP_SHOWING; 171 closeControls(v, true); 172 mMetricsLogger.write(getLogMaker().setCategory( 173 MetricsEvent.NOTIFICATION_BLOCKING_HELPER) 174 .setType(MetricsEvent.TYPE_ACTION) 175 .setSubtype(MetricsEvent.BLOCKING_HELPER_CLICK_STAY_SILENT)); 176 }; 177 178 // used by blocking helper 179 private OnClickListener mOnDeliverSilently = v -> { 180 handleSaveImportance( 181 ACTION_DELIVER_SILENTLY, MetricsEvent.BLOCKING_HELPER_CLICK_STAY_SILENT); 182 }; 183 handleSaveImportance(int action, int metricsSubtype)184 private void handleSaveImportance(int action, int metricsSubtype) { 185 Runnable saveImportance = () -> { 186 saveImportanceAndExitReason(action); 187 if (mIsForBlockingHelper) { 188 swapContent(action, true /* animate */); 189 mMetricsLogger.write(getLogMaker() 190 .setCategory(MetricsEvent.NOTIFICATION_BLOCKING_HELPER) 191 .setType(MetricsEvent.TYPE_ACTION) 192 .setSubtype(metricsSubtype)); 193 } 194 }; 195 if (mCheckSaveListener != null) { 196 mCheckSaveListener.checkSave(saveImportance, mSbn); 197 } else { 198 saveImportance.run(); 199 } 200 } 201 202 private OnClickListener mOnUndo = v -> { 203 // Reset exit counter that we'll log and record an undo event separately (not an exit event) 204 mExitReason = NotificationCounters.BLOCKING_HELPER_DISMISSED; 205 if (mIsForBlockingHelper) { 206 logBlockingHelperCounter(NotificationCounters.BLOCKING_HELPER_UNDO); 207 mMetricsLogger.write(getLogMaker().setCategory( 208 MetricsEvent.NOTIFICATION_BLOCKING_HELPER) 209 .setType(MetricsEvent.TYPE_DISMISS) 210 .setSubtype(MetricsEvent.BLOCKING_HELPER_CLICK_UNDO)); 211 } else { 212 // TODO: this can't happen? 213 mMetricsLogger.write(importanceChangeLogMaker().setType(MetricsEvent.TYPE_DISMISS)); 214 } 215 saveImportanceAndExitReason(ACTION_UNDO); 216 swapContent(ACTION_UNDO, true /* animate */); 217 }; 218 NotificationInfo(Context context, AttributeSet attrs)219 public NotificationInfo(Context context, AttributeSet attrs) { 220 super(context, attrs); 221 } 222 223 @Override onFinishInflate()224 protected void onFinishInflate() { 225 super.onFinishInflate(); 226 227 mPriorityDescriptionView = findViewById(R.id.alert_summary); 228 mSilentDescriptionView = findViewById(R.id.silence_summary); 229 } 230 231 // Specify a CheckSaveListener to override when/if the user's changes are committed. 232 public interface CheckSaveListener { 233 // Invoked when importance has changed and the NotificationInfo wants to try to save it. 234 // Listener should run saveImportance unless the change should be canceled. checkSave(Runnable saveImportance, StatusBarNotification sbn)235 void checkSave(Runnable saveImportance, StatusBarNotification sbn); 236 } 237 238 public interface OnSettingsClickListener { onClick(View v, NotificationChannel channel, int appUid)239 void onClick(View v, NotificationChannel channel, int appUid); 240 } 241 242 public interface OnAppSettingsClickListener { onClick(View v, Intent intent)243 void onClick(View v, Intent intent); 244 } 245 246 @VisibleForTesting bindNotification( final PackageManager pm, final INotificationManager iNotificationManager, final VisualStabilityManager visualStabilityManager, final String pkg, final NotificationChannel notificationChannel, final Set<NotificationChannel> uniqueChannelsInRow, final StatusBarNotification sbn, final CheckSaveListener checkSaveListener, final OnSettingsClickListener onSettingsClick, final OnAppSettingsClickListener onAppSettingsClick, boolean isDeviceProvisioned, boolean isNonblockable, int importance, boolean wasShownHighPriority)247 void bindNotification( 248 final PackageManager pm, 249 final INotificationManager iNotificationManager, 250 final VisualStabilityManager visualStabilityManager, 251 final String pkg, 252 final NotificationChannel notificationChannel, 253 final Set<NotificationChannel> uniqueChannelsInRow, 254 final StatusBarNotification sbn, 255 final CheckSaveListener checkSaveListener, 256 final OnSettingsClickListener onSettingsClick, 257 final OnAppSettingsClickListener onAppSettingsClick, 258 boolean isDeviceProvisioned, 259 boolean isNonblockable, 260 int importance, 261 boolean wasShownHighPriority) 262 throws RemoteException { 263 bindNotification(pm, iNotificationManager, visualStabilityManager, pkg, notificationChannel, 264 uniqueChannelsInRow, sbn, checkSaveListener, onSettingsClick, 265 onAppSettingsClick, isDeviceProvisioned, isNonblockable, 266 false /* isBlockingHelper */, 267 importance, wasShownHighPriority); 268 } 269 bindNotification( PackageManager pm, INotificationManager iNotificationManager, VisualStabilityManager visualStabilityManager, String pkg, NotificationChannel notificationChannel, Set<NotificationChannel> uniqueChannelsInRow, StatusBarNotification sbn, CheckSaveListener checkSaveListener, OnSettingsClickListener onSettingsClick, OnAppSettingsClickListener onAppSettingsClick, boolean isDeviceProvisioned, boolean isNonblockable, boolean isForBlockingHelper, int importance, boolean wasShownHighPriority)270 public void bindNotification( 271 PackageManager pm, 272 INotificationManager iNotificationManager, 273 VisualStabilityManager visualStabilityManager, 274 String pkg, 275 NotificationChannel notificationChannel, 276 Set<NotificationChannel> uniqueChannelsInRow, 277 StatusBarNotification sbn, 278 CheckSaveListener checkSaveListener, 279 OnSettingsClickListener onSettingsClick, 280 OnAppSettingsClickListener onAppSettingsClick, 281 boolean isDeviceProvisioned, 282 boolean isNonblockable, 283 boolean isForBlockingHelper, 284 int importance, 285 boolean wasShownHighPriority) 286 throws RemoteException { 287 mINotificationManager = iNotificationManager; 288 mMetricsLogger = Dependency.get(MetricsLogger.class); 289 mVisualStabilityManager = visualStabilityManager; 290 mChannelEditorDialogController = Dependency.get(ChannelEditorDialogController.class); 291 mPackageName = pkg; 292 mUniqueChannelsInRow = uniqueChannelsInRow; 293 mNumUniqueChannelsInRow = uniqueChannelsInRow.size(); 294 mSbn = sbn; 295 mPm = pm; 296 mAppSettingsClickListener = onAppSettingsClick; 297 mAppName = mPackageName; 298 mCheckSaveListener = checkSaveListener; 299 mOnSettingsClickListener = onSettingsClick; 300 mSingleNotificationChannel = notificationChannel; 301 mStartingChannelImportance = mSingleNotificationChannel.getImportance(); 302 mWasShownHighPriority = wasShownHighPriority; 303 mIsNonblockable = isNonblockable; 304 mIsForBlockingHelper = isForBlockingHelper; 305 mAppUid = mSbn.getUid(); 306 mDelegatePkg = mSbn.getOpPkg(); 307 mIsDeviceProvisioned = isDeviceProvisioned; 308 309 int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage( 310 pkg, mAppUid, false /* includeDeleted */); 311 if (mNumUniqueChannelsInRow == 0) { 312 throw new IllegalArgumentException("bindNotification requires at least one channel"); 313 } else { 314 // Special behavior for the Default channel if no other channels have been defined. 315 mIsSingleDefaultChannel = mNumUniqueChannelsInRow == 1 316 && mSingleNotificationChannel.getId().equals( 317 NotificationChannel.DEFAULT_CHANNEL_ID) 318 && numTotalChannels == 1; 319 } 320 321 bindHeader(); 322 bindChannelDetails(); 323 324 if (mIsForBlockingHelper) { 325 bindBlockingHelper(); 326 } else { 327 bindInlineControls(); 328 } 329 330 mMetricsLogger.write(notificationControlsLogMaker()); 331 } 332 bindBlockingHelper()333 private void bindBlockingHelper() { 334 findViewById(R.id.inline_controls).setVisibility(GONE); 335 findViewById(R.id.blocking_helper).setVisibility(VISIBLE); 336 337 findViewById(R.id.undo).setOnClickListener(mOnUndo); 338 339 View turnOffButton = findViewById(R.id.blocking_helper_turn_off_notifications); 340 turnOffButton.setOnClickListener(getSettingsOnClickListener()); 341 turnOffButton.setVisibility(turnOffButton.hasOnClickListeners() ? VISIBLE : GONE); 342 343 TextView keepShowing = findViewById(R.id.keep_showing); 344 keepShowing.setOnClickListener(mOnKeepShowing); 345 346 View deliverSilently = findViewById(R.id.deliver_silently); 347 deliverSilently.setOnClickListener(mOnDeliverSilently); 348 } 349 bindInlineControls()350 private void bindInlineControls() { 351 findViewById(R.id.inline_controls).setVisibility(VISIBLE); 352 findViewById(R.id.blocking_helper).setVisibility(GONE); 353 354 if (mIsNonblockable) { 355 findViewById(R.id.non_configurable_text).setVisibility(VISIBLE); 356 findViewById(R.id.non_configurable_multichannel_text).setVisibility(GONE); 357 findViewById(R.id.interruptiveness_settings).setVisibility(GONE); 358 ((TextView) findViewById(R.id.done)).setText(R.string.inline_done_button); 359 findViewById(R.id.turn_off_notifications).setVisibility(GONE); 360 } else if (mNumUniqueChannelsInRow > 1) { 361 findViewById(R.id.non_configurable_text).setVisibility(GONE); 362 findViewById(R.id.interruptiveness_settings).setVisibility(GONE); 363 findViewById(R.id.non_configurable_multichannel_text).setVisibility(VISIBLE); 364 } else { 365 findViewById(R.id.non_configurable_text).setVisibility(GONE); 366 findViewById(R.id.non_configurable_multichannel_text).setVisibility(GONE); 367 findViewById(R.id.interruptiveness_settings).setVisibility(VISIBLE); 368 } 369 370 View turnOffButton = findViewById(R.id.turn_off_notifications); 371 turnOffButton.setOnClickListener(getTurnOffNotificationsClickListener()); 372 turnOffButton.setVisibility(turnOffButton.hasOnClickListeners() && !mIsNonblockable 373 ? VISIBLE : GONE); 374 375 View done = findViewById(R.id.done); 376 done.setOnClickListener(mOnDismissSettings); 377 378 379 View silent = findViewById(R.id.silence); 380 View alert = findViewById(R.id.alert); 381 silent.setOnClickListener(mOnSilent); 382 alert.setOnClickListener(mOnAlert); 383 384 applyAlertingBehavior( 385 mWasShownHighPriority ? BEHAVIOR_ALERTING : BEHAVIOR_SILENT, 386 false /* userTriggered */); 387 } 388 bindHeader()389 private void bindHeader() { 390 // Package name 391 mPkgIcon = null; 392 ApplicationInfo info; 393 try { 394 info = mPm.getApplicationInfo( 395 mPackageName, 396 PackageManager.MATCH_UNINSTALLED_PACKAGES 397 | PackageManager.MATCH_DISABLED_COMPONENTS 398 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 399 | PackageManager.MATCH_DIRECT_BOOT_AWARE); 400 if (info != null) { 401 mAppName = String.valueOf(mPm.getApplicationLabel(info)); 402 mPkgIcon = mPm.getApplicationIcon(info); 403 } 404 } catch (PackageManager.NameNotFoundException e) { 405 // app is gone, just show package name and generic icon 406 mPkgIcon = mPm.getDefaultActivityIcon(); 407 } 408 ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(mPkgIcon); 409 ((TextView) findViewById(R.id.pkgname)).setText(mAppName); 410 411 // Delegate 412 bindDelegate(); 413 414 // Set up app settings link (i.e. Customize) 415 View settingsLinkView = findViewById(R.id.app_settings); 416 Intent settingsIntent = getAppSettingsIntent(mPm, mPackageName, 417 mSingleNotificationChannel, 418 mSbn.getId(), mSbn.getTag()); 419 if (settingsIntent != null 420 && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) { 421 settingsLinkView.setVisibility(VISIBLE); 422 settingsLinkView.setOnClickListener((View view) -> { 423 mAppSettingsClickListener.onClick(view, settingsIntent); 424 }); 425 } else { 426 settingsLinkView.setVisibility(View.GONE); 427 } 428 429 // System Settings button. 430 final View settingsButton = findViewById(R.id.info); 431 settingsButton.setOnClickListener(getSettingsOnClickListener()); 432 settingsButton.setVisibility(settingsButton.hasOnClickListeners() ? VISIBLE : GONE); 433 } 434 getSettingsOnClickListener()435 private OnClickListener getSettingsOnClickListener() { 436 if (mAppUid >= 0 && mOnSettingsClickListener != null && mIsDeviceProvisioned) { 437 final int appUidF = mAppUid; 438 return ((View view) -> { 439 logBlockingHelperCounter( 440 NotificationCounters.BLOCKING_HELPER_NOTIF_SETTINGS); 441 mOnSettingsClickListener.onClick(view, 442 mNumUniqueChannelsInRow > 1 ? null : mSingleNotificationChannel, 443 appUidF); 444 }); 445 } 446 return null; 447 } 448 getTurnOffNotificationsClickListener()449 private OnClickListener getTurnOffNotificationsClickListener() { 450 return ((View view) -> { 451 if (!mPresentingChannelEditorDialog && mChannelEditorDialogController != null) { 452 mPresentingChannelEditorDialog = true; 453 454 mChannelEditorDialogController.prepareDialogForApp(mAppName, mPackageName, mAppUid, 455 mUniqueChannelsInRow, mPkgIcon, mOnSettingsClickListener); 456 mChannelEditorDialogController.setOnFinishListener(() -> { 457 mPresentingChannelEditorDialog = false; 458 closeControls(this, false); 459 }); 460 mChannelEditorDialogController.show(); 461 } 462 }); 463 } 464 465 private void bindChannelDetails() throws RemoteException { 466 bindName(); 467 bindGroup(); 468 } 469 470 private void bindName() { 471 final TextView channelName = findViewById(R.id.channel_name); 472 if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) { 473 channelName.setVisibility(View.GONE); 474 } else { 475 channelName.setText(mSingleNotificationChannel.getName()); 476 } 477 } 478 479 private void bindDelegate() { 480 TextView delegateView = findViewById(R.id.delegate_name); 481 TextView dividerView = findViewById(R.id.pkg_divider); 482 483 CharSequence delegatePkg = null; 484 if (!TextUtils.equals(mPackageName, mDelegatePkg)) { 485 // this notification was posted by a delegate! 486 delegateView.setVisibility(View.VISIBLE); 487 dividerView.setVisibility(View.VISIBLE); 488 } else { 489 delegateView.setVisibility(View.GONE); 490 dividerView.setVisibility(View.GONE); 491 } 492 } 493 494 private void bindGroup() throws RemoteException { 495 // Set group information if this channel has an associated group. 496 CharSequence groupName = null; 497 if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) { 498 final NotificationChannelGroup notificationChannelGroup = 499 mINotificationManager.getNotificationChannelGroupForPackage( 500 mSingleNotificationChannel.getGroup(), mPackageName, mAppUid); 501 if (notificationChannelGroup != null) { 502 groupName = notificationChannelGroup.getName(); 503 } 504 } 505 TextView groupNameView = findViewById(R.id.group_name); 506 if (groupName != null) { 507 groupNameView.setText(groupName); 508 groupNameView.setVisibility(View.VISIBLE); 509 } else { 510 groupNameView.setVisibility(View.GONE); 511 } 512 } 513 514 515 @VisibleForTesting 516 void logBlockingHelperCounter(String counterTag) { 517 if (mIsForBlockingHelper) { 518 mMetricsLogger.count(counterTag, 1); 519 } 520 } 521 522 private void saveImportance() { 523 if (!mIsNonblockable 524 || mExitReason != NotificationCounters.BLOCKING_HELPER_STOP_NOTIFICATIONS) { 525 if (mChosenImportance == null) { 526 mChosenImportance = mStartingChannelImportance; 527 } 528 updateImportance(); 529 } 530 } 531 532 /** 533 * Commits the updated importance values on the background thread. 534 */ 535 private void updateImportance() { 536 if (mChosenImportance != null) { 537 mMetricsLogger.write(importanceChangeLogMaker()); 538 539 int newImportance = mChosenImportance; 540 if (mStartingChannelImportance != IMPORTANCE_UNSPECIFIED) { 541 if ((mWasShownHighPriority && mChosenImportance >= IMPORTANCE_DEFAULT) 542 || (!mWasShownHighPriority && mChosenImportance < IMPORTANCE_DEFAULT)) { 543 newImportance = mStartingChannelImportance; 544 } 545 } 546 547 Handler bgHandler = new Handler(Dependency.get(Dependency.BG_LOOPER)); 548 bgHandler.post( 549 new UpdateImportanceRunnable(mINotificationManager, mPackageName, mAppUid, 550 mNumUniqueChannelsInRow == 1 ? mSingleNotificationChannel : null, 551 mStartingChannelImportance, newImportance)); 552 mVisualStabilityManager.temporarilyAllowReordering(); 553 } 554 } 555 556 private void applyAlertingBehavior(@AlertingBehavior int behavior, boolean userTriggered) { 557 if (userTriggered) { 558 TransitionSet transition = new TransitionSet(); 559 transition.setOrdering(TransitionSet.ORDERING_TOGETHER); 560 transition.addTransition(new Fade(Fade.OUT)) 561 .addTransition(new ChangeBounds()) 562 .addTransition( 563 new Fade(Fade.IN) 564 .setStartDelay(150) 565 .setDuration(200) 566 .setInterpolator(FAST_OUT_SLOW_IN)); 567 transition.setDuration(350); 568 transition.setInterpolator(FAST_OUT_SLOW_IN); 569 TransitionManager.beginDelayedTransition(this, transition); 570 } 571 572 View alert = findViewById(R.id.alert); 573 View silence = findViewById(R.id.silence); 574 575 switch (behavior) { 576 case BEHAVIOR_ALERTING: 577 mPriorityDescriptionView.setVisibility(VISIBLE); 578 mSilentDescriptionView.setVisibility(GONE); 579 post(() -> { 580 alert.setSelected(true); 581 silence.setSelected(false); 582 }); 583 break; 584 case BEHAVIOR_SILENT: 585 586 mSilentDescriptionView.setVisibility(VISIBLE); 587 mPriorityDescriptionView.setVisibility(GONE); 588 post(() -> { 589 alert.setSelected(false); 590 silence.setSelected(true); 591 }); 592 break; 593 default: 594 throw new IllegalArgumentException("Unrecognized alerting behavior: " + behavior); 595 } 596 597 boolean isAChange = mWasShownHighPriority != (behavior == BEHAVIOR_ALERTING); 598 TextView done = findViewById(R.id.done); 599 done.setText(isAChange ? R.string.inline_ok_button : R.string.inline_done_button); 600 } 601 602 private void saveImportanceAndExitReason(@NotificationInfoAction int action) { 603 switch (action) { 604 case ACTION_UNDO: 605 mChosenImportance = mStartingChannelImportance; 606 break; 607 case ACTION_DELIVER_SILENTLY: 608 mExitReason = NotificationCounters.BLOCKING_HELPER_DELIVER_SILENTLY; 609 mChosenImportance = mWasShownHighPriority 610 ? IMPORTANCE_LOW : mStartingChannelImportance; 611 break; 612 default: 613 throw new IllegalArgumentException(); 614 } 615 } 616 617 // only used for blocking helper 618 private void swapContent(@NotificationInfoAction int action, boolean animate) { 619 if (mExpandAnimation != null) { 620 mExpandAnimation.cancel(); 621 } 622 623 View blockingHelper = findViewById(R.id.blocking_helper); 624 ViewGroup confirmation = findViewById(R.id.confirmation); 625 TextView confirmationText = findViewById(R.id.confirmation_text); 626 627 saveImportanceAndExitReason(action); 628 629 switch (action) { 630 case ACTION_UNDO: 631 break; 632 case ACTION_DELIVER_SILENTLY: 633 confirmationText.setText(R.string.notification_channel_silenced); 634 break; 635 default: 636 throw new IllegalArgumentException(); 637 } 638 639 boolean isUndo = action == ACTION_UNDO; 640 641 blockingHelper.setVisibility(isUndo ? VISIBLE : GONE); 642 findViewById(R.id.channel_info).setVisibility(isUndo ? VISIBLE : GONE); 643 findViewById(R.id.header).setVisibility(isUndo ? VISIBLE : GONE); 644 confirmation.setVisibility(isUndo ? GONE : VISIBLE); 645 646 if (animate) { 647 ObjectAnimator promptAnim = ObjectAnimator.ofFloat(blockingHelper, View.ALPHA, 648 blockingHelper.getAlpha(), isUndo ? 1f : 0f); 649 promptAnim.setInterpolator(isUndo ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT); 650 ObjectAnimator confirmAnim = ObjectAnimator.ofFloat(confirmation, View.ALPHA, 651 confirmation.getAlpha(), isUndo ? 0f : 1f); 652 confirmAnim.setInterpolator(isUndo ? Interpolators.ALPHA_OUT : Interpolators.ALPHA_IN); 653 654 mExpandAnimation = new AnimatorSet(); 655 mExpandAnimation.playTogether(promptAnim, confirmAnim); 656 mExpandAnimation.setDuration(150); 657 mExpandAnimation.addListener(new AnimatorListenerAdapter() { 658 boolean mCancelled = false; 659 660 @Override 661 public void onAnimationCancel(Animator animation) { 662 mCancelled = true; 663 } 664 665 @Override 666 public void onAnimationEnd(Animator animation) { 667 if (!mCancelled) { 668 blockingHelper.setVisibility(isUndo ? VISIBLE : GONE); 669 confirmation.setVisibility(isUndo ? GONE : VISIBLE); 670 } 671 } 672 }); 673 mExpandAnimation.start(); 674 } 675 676 // Since we're swapping/update the content, reset the timeout so the UI can't close 677 // immediately after the update. 678 if (mGutsContainer != null) { 679 mGutsContainer.resetFalsingCheck(); 680 } 681 } 682 683 @Override 684 public void onFinishedClosing() { 685 if (mChosenImportance != null) { 686 mStartingChannelImportance = mChosenImportance; 687 } 688 mExitReason = NotificationCounters.BLOCKING_HELPER_DISMISSED; 689 690 if (mIsForBlockingHelper) { 691 bindBlockingHelper(); 692 } else { 693 bindInlineControls(); 694 } 695 696 mMetricsLogger.write(notificationControlsLogMaker().setType(MetricsEvent.TYPE_CLOSE)); 697 } 698 699 @Override 700 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 701 super.onInitializeAccessibilityEvent(event); 702 if (mGutsContainer != null && 703 event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { 704 if (mGutsContainer.isExposed()) { 705 event.getText().add(mContext.getString( 706 R.string.notification_channel_controls_opened_accessibility, mAppName)); 707 } else { 708 event.getText().add(mContext.getString( 709 R.string.notification_channel_controls_closed_accessibility, mAppName)); 710 } 711 } 712 } 713 714 private Intent getAppSettingsIntent(PackageManager pm, String packageName, 715 NotificationChannel channel, int id, String tag) { 716 Intent intent = new Intent(Intent.ACTION_MAIN) 717 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES) 718 .setPackage(packageName); 719 final List<ResolveInfo> resolveInfos = pm.queryIntentActivities( 720 intent, 721 PackageManager.MATCH_DEFAULT_ONLY 722 ); 723 if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) { 724 return null; 725 } 726 final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo; 727 intent.setClassName(activityInfo.packageName, activityInfo.name); 728 if (channel != null) { 729 intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId()); 730 } 731 intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id); 732 intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag); 733 return intent; 734 } 735 736 /** 737 * Closes the controls and commits the updated importance values (indirectly). If this view is 738 * being used to show the blocking helper, this will immediately dismiss the blocking helper and 739 * commit the updated importance. 740 * 741 * <p><b>Note,</b> this will only get called once the view is dismissing. This means that the 742 * user does not have the ability to undo the action anymore. See 743 * {@link #swapContent(boolean, boolean)} for where undo is handled. 744 */ 745 @VisibleForTesting 746 void closeControls(View v, boolean save) { 747 int[] parentLoc = new int[2]; 748 int[] targetLoc = new int[2]; 749 mGutsContainer.getLocationOnScreen(parentLoc); 750 v.getLocationOnScreen(targetLoc); 751 final int centerX = v.getWidth() / 2; 752 final int centerY = v.getHeight() / 2; 753 final int x = targetLoc[0] - parentLoc[0] + centerX; 754 final int y = targetLoc[1] - parentLoc[1] + centerY; 755 mGutsContainer.closeControls(x, y, save, false /* force */); 756 } 757 758 @Override 759 public void setGutsParent(NotificationGuts guts) { 760 mGutsContainer = guts; 761 } 762 763 @Override 764 public boolean willBeRemoved() { 765 return false; 766 } 767 768 @Override 769 public boolean shouldBeSaved() { 770 return mPressedApply; 771 } 772 773 @Override 774 public View getContentView() { 775 return this; 776 } 777 778 @Override 779 public boolean handleCloseControls(boolean save, boolean force) { 780 if (mPresentingChannelEditorDialog && mChannelEditorDialogController != null) { 781 mPresentingChannelEditorDialog = false; 782 // No need for the finish listener because we're closing 783 mChannelEditorDialogController.setOnFinishListener(null); 784 mChannelEditorDialogController.close(); 785 } 786 787 // Save regardless of the importance so we can lock the importance field if the user wants 788 // to keep getting notifications 789 if (save) { 790 saveImportance(); 791 } 792 logBlockingHelperCounter(mExitReason); 793 return false; 794 } 795 796 @Override 797 public int getActualHeight() { 798 return getHeight(); 799 } 800 801 @VisibleForTesting 802 public boolean isAnimating() { 803 return mExpandAnimation != null && mExpandAnimation.isRunning(); 804 } 805 806 /** 807 * Runnable to either update the given channel (with a new importance value) or, if no channel 808 * is provided, update notifications enabled state for the package. 809 */ 810 private static class UpdateImportanceRunnable implements Runnable { 811 private final INotificationManager mINotificationManager; 812 private final String mPackageName; 813 private final int mAppUid; 814 private final @Nullable NotificationChannel mChannelToUpdate; 815 private final int mCurrentImportance; 816 private final int mNewImportance; 817 818 819 public UpdateImportanceRunnable(INotificationManager notificationManager, 820 String packageName, int appUid, @Nullable NotificationChannel channelToUpdate, 821 int currentImportance, int newImportance) { 822 mINotificationManager = notificationManager; 823 mPackageName = packageName; 824 mAppUid = appUid; 825 mChannelToUpdate = channelToUpdate; 826 mCurrentImportance = currentImportance; 827 mNewImportance = newImportance; 828 } 829 830 @Override 831 public void run() { 832 try { 833 if (mChannelToUpdate != null) { 834 mChannelToUpdate.setImportance(mNewImportance); 835 mChannelToUpdate.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); 836 mINotificationManager.updateNotificationChannelForPackage( 837 mPackageName, mAppUid, mChannelToUpdate); 838 } else { 839 // For notifications with more than one channel, update notification enabled 840 // state. If the importance was lowered, we disable notifications. 841 mINotificationManager.setNotificationsEnabledWithImportanceLockForPackage( 842 mPackageName, mAppUid, mNewImportance >= mCurrentImportance); 843 } 844 } catch (RemoteException e) { 845 Log.e(TAG, "Unable to update notification importance", e); 846 } 847 } 848 } 849 850 /** 851 * Returns a LogMaker with all available notification information. 852 * Caller should set category, type, and maybe subtype, before passing it to mMetricsLogger. 853 * @return LogMaker 854 */ 855 private LogMaker getLogMaker() { 856 // The constructor requires a category, so also do it in the other branch for consistency. 857 return mSbn == null ? new LogMaker(MetricsEvent.NOTIFICATION_BLOCKING_HELPER) 858 : mSbn.getLogMaker().setCategory(MetricsEvent.NOTIFICATION_BLOCKING_HELPER); 859 } 860 861 /** 862 * Returns an initialized LogMaker for logging importance changes. 863 * The caller may override the type before passing it to mMetricsLogger. 864 * @return LogMaker 865 */ 866 private LogMaker importanceChangeLogMaker() { 867 Integer chosenImportance = 868 mChosenImportance != null ? mChosenImportance : mStartingChannelImportance; 869 return getLogMaker().setCategory(MetricsEvent.ACTION_SAVE_IMPORTANCE) 870 .setType(MetricsEvent.TYPE_ACTION) 871 .setSubtype(chosenImportance - mStartingChannelImportance); 872 } 873 874 /** 875 * Returns an initialized LogMaker for logging open/close of the info display. 876 * The caller may override the type before passing it to mMetricsLogger. 877 * @return LogMaker 878 */ 879 private LogMaker notificationControlsLogMaker() { 880 return getLogMaker().setCategory(MetricsEvent.ACTION_NOTE_CONTROLS) 881 .setType(MetricsEvent.TYPE_OPEN) 882 .setSubtype(mIsForBlockingHelper ? MetricsEvent.BLOCKING_HELPER_DISPLAY 883 : MetricsEvent.BLOCKING_HELPER_UNKNOWN); 884 } 885 886 @Retention(SOURCE) 887 @IntDef({BEHAVIOR_ALERTING, BEHAVIOR_SILENT}) 888 private @interface AlertingBehavior {} 889 private static final int BEHAVIOR_ALERTING = 0; 890 private static final int BEHAVIOR_SILENT = 1; 891 } 892