• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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