2 * Copyright (C) 2017 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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
17 package com.android.systemui.statusbar.notification.row;
19 import static android.app.NotificationManager.IMPORTANCE_MIN;
20 import static android.app.NotificationManager.IMPORTANCE_NONE;
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.AnimatorSet;
25 import android.animation.ObjectAnimator;
26 import android.annotation.Nullable;
27 import android.app.INotificationManager;
28 import android.app.Notification;
29 import android.app.NotificationChannel;
30 import android.app.NotificationChannelGroup;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.pm.ActivityInfo;
34 import android.content.pm.ApplicationInfo;
35 import android.content.pm.PackageManager;
36 import android.content.pm.ResolveInfo;
37 import android.graphics.drawable.Drawable;
38 import android.os.Handler;
39 import android.os.RemoteException;
40 import android.service.notification.StatusBarNotification;
41 import android.text.TextUtils;
42 import android.util.AttributeSet;
43 import android.util.Log;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.view.accessibility.AccessibilityEvent;
47 import android.widget.ImageView;
48 import android.widget.LinearLayout;
49 import android.widget.TextView;
51 import com.android.internal.annotations.VisibleForTesting;
52 import com.android.internal.logging.MetricsLogger;
53 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
54 import com.android.systemui.Dependency;
55 import com.android.systemui.Interpolators;
56 import com.android.systemui.R;
57 import com.android.systemui.statusbar.notification.logging.NotificationCounters;
59 import java.util.List;
62 * The guts of a notification revealed when performing a long press. This also houses the blocking
63 * helper affordance that allows a user to keep/stop notifications after swiping one away.
65 public class NotificationInfo extends LinearLayout implements NotificationGuts.GutsContent {
66 private static final String TAG = "InfoGuts";
68 private INotificationManager mINotificationManager;
69 private PackageManager mPm;
70 private MetricsLogger mMetricsLogger;
72 private String mPackageName;
73 private String mAppName;
75 private int mNumUniqueChannelsInRow;
76 private NotificationChannel mSingleNotificationChannel;
77 private int mStartingUserImportance;
78 private int mChosenImportance;
79 private boolean mIsSingleDefaultChannel;
80 private boolean mIsNonblockable;
81 private StatusBarNotification mSbn;
82 private AnimatorSet mExpandAnimation;
83 private boolean mIsForeground;
85 private CheckSaveListener mCheckSaveListener;
86 private OnSettingsClickListener mOnSettingsClickListener;
87 private OnAppSettingsClickListener mAppSettingsClickListener;
88 private NotificationGuts mGutsContainer;
90 /** Whether this view is being shown as part of the blocking helper. */
91 private boolean mIsForBlockingHelper;
92 private boolean mNegativeUserSentiment;
95 * String that describes how the user exit or quit out of this view, also used as a counter tag.
97 private String mExitReason = NotificationCounters.BLOCKING_HELPER_DISMISSED;
99 private OnClickListener mOnKeepShowing = v -> {
100 mExitReason = NotificationCounters.BLOCKING_HELPER_KEEP_SHOWING;
104 private OnClickListener mOnStopOrMinimizeNotifications = v -> {
105 mExitReason = NotificationCounters.BLOCKING_HELPER_STOP_NOTIFICATIONS;
109 private OnClickListener mOnUndo = v -> {
110 // Reset exit counter that we'll log and record an undo event separately (not an exit event)
111 mExitReason = NotificationCounters.BLOCKING_HELPER_DISMISSED;
112 logBlockingHelperCounter(NotificationCounters.BLOCKING_HELPER_UNDO);
116 public NotificationInfo(Context context, AttributeSet attrs) {
117 super(context, attrs);
120 // Specify a CheckSaveListener to override when/if the user's changes are committed.
121 public interface CheckSaveListener {
122 // Invoked when importance has changed and the NotificationInfo wants to try to save it.
123 // Listener should run saveImportance unless the change should be canceled.
124 void checkSave(Runnable saveImportance, StatusBarNotification sbn);
127 public interface OnSettingsClickListener {
128 void onClick(View v, NotificationChannel channel, int appUid);
131 public interface OnAppSettingsClickListener {
132 void onClick(View v, Intent intent);
136 void bindNotification(
137 final PackageManager pm,
138 final INotificationManager iNotificationManager,
140 final NotificationChannel notificationChannel,
141 final int numUniqueChannelsInRow,
142 final StatusBarNotification sbn,
143 final CheckSaveListener checkSaveListener,
144 final OnSettingsClickListener onSettingsClick,
145 final OnAppSettingsClickListener onAppSettingsClick,
146 boolean isNonblockable)
147 throws RemoteException {
148 bindNotification(pm, iNotificationManager, pkg, notificationChannel,
149 numUniqueChannelsInRow, sbn, checkSaveListener, onSettingsClick,
150 onAppSettingsClick, isNonblockable, false /* isBlockingHelper */,
151 false /* isUserSentimentNegative */);
154 public void bindNotification(
156 INotificationManager iNotificationManager,
158 NotificationChannel notificationChannel,
159 int numUniqueChannelsInRow,
160 StatusBarNotification sbn,
161 CheckSaveListener checkSaveListener,
162 OnSettingsClickListener onSettingsClick,
163 OnAppSettingsClickListener onAppSettingsClick,
164 boolean isNonblockable,
165 boolean isForBlockingHelper,
166 boolean isUserSentimentNegative)
167 throws RemoteException {
168 mINotificationManager = iNotificationManager;
169 mMetricsLogger = Dependency.get(MetricsLogger.class);
171 mNumUniqueChannelsInRow = numUniqueChannelsInRow;
174 mAppSettingsClickListener = onAppSettingsClick;
175 mAppName = mPackageName;
176 mCheckSaveListener = checkSaveListener;
177 mOnSettingsClickListener = onSettingsClick;
178 mSingleNotificationChannel = notificationChannel;
179 mStartingUserImportance = mChosenImportance = mSingleNotificationChannel.getImportance();
180 mNegativeUserSentiment = isUserSentimentNegative;
181 mIsNonblockable = isNonblockable;
183 (mSbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
184 mIsForBlockingHelper = isForBlockingHelper;
185 mAppUid = mSbn.getUid();
187 int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage(
188 pkg, mAppUid, false /* includeDeleted */);
189 if (mNumUniqueChannelsInRow == 0) {
190 throw new IllegalArgumentException("bindNotification requires at least one channel");
192 // Special behavior for the Default channel if no other channels have been defined.
193 mIsSingleDefaultChannel = mNumUniqueChannelsInRow == 1
194 && mSingleNotificationChannel.getId().equals(
195 NotificationChannel.DEFAULT_CHANNEL_ID)
196 && numTotalChannels == 1;
204 private void bindHeader() throws RemoteException {
206 Drawable pkgicon = null;
207 ApplicationInfo info;
209 info = mPm.getApplicationInfo(
211 PackageManager.MATCH_UNINSTALLED_PACKAGES
212 | PackageManager.MATCH_DISABLED_COMPONENTS
213 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
214 | PackageManager.MATCH_DIRECT_BOOT_AWARE);
216 mAppName = String.valueOf(mPm.getApplicationLabel(info));
217 pkgicon = mPm.getApplicationIcon(info);
219 } catch (PackageManager.NameNotFoundException e) {
220 // app is gone, just show package name and generic icon
221 pkgicon = mPm.getDefaultActivityIcon();
223 ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon);
224 ((TextView) findViewById(R.id.pkgname)).setText(mAppName);
226 // Set group information if this channel has an associated group.
227 CharSequence groupName = null;
228 if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) {
229 final NotificationChannelGroup notificationChannelGroup =
230 mINotificationManager.getNotificationChannelGroupForPackage(
231 mSingleNotificationChannel.getGroup(), mPackageName, mAppUid);
232 if (notificationChannelGroup != null) {
233 groupName = notificationChannelGroup.getName();
236 TextView groupNameView = findViewById(R.id.group_name);
237 TextView groupDividerView = findViewById(R.id.pkg_group_divider);
238 if (groupName != null) {
239 groupNameView.setText(groupName);
240 groupNameView.setVisibility(View.VISIBLE);
241 groupDividerView.setVisibility(View.VISIBLE);
243 groupNameView.setVisibility(View.GONE);
244 groupDividerView.setVisibility(View.GONE);
248 final View settingsButton = findViewById(R.id.info);
249 if (mAppUid >= 0 && mOnSettingsClickListener != null) {
250 settingsButton.setVisibility(View.VISIBLE);
251 final int appUidF = mAppUid;
252 settingsButton.setOnClickListener(
254 logBlockingHelperCounter(
255 NotificationCounters.BLOCKING_HELPER_NOTIF_SETTINGS);
256 mOnSettingsClickListener.onClick(view,
257 mNumUniqueChannelsInRow > 1 ? null : mSingleNotificationChannel,
261 settingsButton.setVisibility(View.GONE);
265 private void bindPrompt() {
266 final TextView blockPrompt = findViewById(R.id.block_prompt);
268 if (mIsNonblockable) {
269 blockPrompt.setText(R.string.notification_unblockable_desc);
271 if (mNegativeUserSentiment) {
272 blockPrompt.setText(R.string.inline_blocking_helper);
273 } else if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) {
274 blockPrompt.setText(R.string.inline_keep_showing_app);
276 blockPrompt.setText(R.string.inline_keep_showing);
281 private void bindName() {
282 final TextView channelName = findViewById(R.id.channel_name);
283 if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) {
284 channelName.setVisibility(View.GONE);
286 channelName.setText(mSingleNotificationChannel.getName());
291 void logBlockingHelperCounter(String counterTag) {
292 if (mIsForBlockingHelper) {
293 mMetricsLogger.count(counterTag, 1);
297 private boolean hasImportanceChanged() {
298 return mSingleNotificationChannel != null && mStartingUserImportance != mChosenImportance;
301 private void saveImportance() {
302 if (!mIsNonblockable) {
303 // Only go through the lock screen/bouncer if the user hit 'Stop notifications'.
304 // Otherwise, update the importance immediately.
305 if (mCheckSaveListener != null
306 && NotificationCounters.BLOCKING_HELPER_STOP_NOTIFICATIONS.equals(
308 mCheckSaveListener.checkSave(this::updateImportance, mSbn);
316 * Commits the updated importance values on the background thread.
318 private void updateImportance() {
319 MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
320 mChosenImportance - mStartingUserImportance);
322 Handler bgHandler = new Handler(Dependency.get(Dependency.BG_LOOPER));
323 bgHandler.post(new UpdateImportanceRunnable(mINotificationManager, mPackageName, mAppUid,
324 mNumUniqueChannelsInRow == 1 ? mSingleNotificationChannel : null,
325 mStartingUserImportance, mChosenImportance));
328 private void bindButtons() {
329 // Set up stay-in-notification actions
330 View block = findViewById(R.id.block);
331 TextView keep = findViewById(R.id.keep);
332 View minimize = findViewById(R.id.minimize);
334 findViewById(R.id.undo).setOnClickListener(mOnUndo);
335 block.setOnClickListener(mOnStopOrMinimizeNotifications);
336 keep.setOnClickListener(mOnKeepShowing);
337 minimize.setOnClickListener(mOnStopOrMinimizeNotifications);
339 if (mIsNonblockable) {
340 keep.setText(android.R.string.ok);
341 block.setVisibility(GONE);
342 minimize.setVisibility(GONE);
343 } else if (mIsForeground) {
344 block.setVisibility(GONE);
345 minimize.setVisibility(VISIBLE);
346 } else if (!mIsForeground) {
347 block.setVisibility(VISIBLE);
348 minimize.setVisibility(GONE);
351 // Set up app settings link (i.e. Customize)
352 TextView settingsLinkView = findViewById(R.id.app_settings);
353 Intent settingsIntent = getAppSettingsIntent(mPm, mPackageName, mSingleNotificationChannel,
354 mSbn.getId(), mSbn.getTag());
355 if (!mIsForBlockingHelper
356 && settingsIntent != null
357 && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
358 settingsLinkView.setVisibility(VISIBLE);
359 settingsLinkView.setText(mContext.getString(R.string.notification_app_settings));
360 settingsLinkView.setOnClickListener((View view) -> {
361 mAppSettingsClickListener.onClick(view, settingsIntent);
364 settingsLinkView.setVisibility(View.GONE);
368 private void swapContent(boolean showPrompt) {
369 if (mExpandAnimation != null) {
370 mExpandAnimation.cancel();
373 View prompt = findViewById(R.id.prompt);
374 ViewGroup confirmation = findViewById(R.id.confirmation);
375 TextView confirmationText = findViewById(R.id.confirmation_text);
376 View header = findViewById(R.id.header);
379 mChosenImportance = mStartingUserImportance;
380 } else if (mIsForeground) {
381 mChosenImportance = IMPORTANCE_MIN;
382 confirmationText.setText(R.string.notification_channel_minimized);
384 mChosenImportance = IMPORTANCE_NONE;
385 confirmationText.setText(R.string.notification_channel_disabled);
388 ObjectAnimator promptAnim = ObjectAnimator.ofFloat(prompt, View.ALPHA,
389 prompt.getAlpha(), showPrompt ? 1f : 0f);
390 promptAnim.setInterpolator(showPrompt ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT);
391 ObjectAnimator confirmAnim = ObjectAnimator.ofFloat(confirmation, View.ALPHA,
392 confirmation.getAlpha(), showPrompt ? 0f : 1f);
393 confirmAnim.setInterpolator(showPrompt ? Interpolators.ALPHA_OUT : Interpolators.ALPHA_IN);
395 prompt.setVisibility(showPrompt ? VISIBLE : GONE);
396 confirmation.setVisibility(showPrompt ? GONE : VISIBLE);
397 header.setVisibility(showPrompt ? VISIBLE : GONE);
399 mExpandAnimation = new AnimatorSet();
400 mExpandAnimation.playTogether(promptAnim, confirmAnim);
401 mExpandAnimation.setDuration(150);
402 mExpandAnimation.addListener(new AnimatorListenerAdapter() {
403 boolean cancelled = false;
406 public void onAnimationCancel(Animator animation) {
411 public void onAnimationEnd(Animator animation) {
413 prompt.setVisibility(showPrompt ? VISIBLE : GONE);
414 confirmation.setVisibility(showPrompt ? GONE : VISIBLE);
418 mExpandAnimation.start();
420 // Since we're swapping/update the content, reset the timeout so the UI can't close
421 // immediately after the update.
422 if (mGutsContainer != null) {
423 mGutsContainer.resetFalsingCheck();
428 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
429 super.onInitializeAccessibilityEvent(event);
430 if (mGutsContainer != null &&
431 event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
432 if (mGutsContainer.isExposed()) {
433 event.getText().add(mContext.getString(
434 R.string.notification_channel_controls_opened_accessibility, mAppName));
436 event.getText().add(mContext.getString(
437 R.string.notification_channel_controls_closed_accessibility, mAppName));
442 private Intent getAppSettingsIntent(PackageManager pm, String packageName,
443 NotificationChannel channel, int id, String tag) {
444 Intent intent = new Intent(Intent.ACTION_MAIN)
445 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
446 .setPackage(packageName);
447 final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
449 PackageManager.MATCH_DEFAULT_ONLY
451 if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
454 final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
455 intent.setClassName(activityInfo.packageName, activityInfo.name);
456 if (channel != null) {
457 intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId());
459 intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id);
460 intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag);
465 * Closes the controls and commits the updated importance values (indirectly). If this view is
466 * being used to show the blocking helper, this will immediately dismiss the blocking helper and
467 * commit the updated importance.
469 * <p><b>Note,</b> this will only get called once the view is dismissing. This means that the
470 * user does not have the ability to undo the action anymore. See {@link #swapContent(boolean)}
471 * for where undo is handled.
474 void closeControls(View v) {
475 int[] parentLoc = new int[2];
476 int[] targetLoc = new int[2];
477 mGutsContainer.getLocationOnScreen(parentLoc);
478 v.getLocationOnScreen(targetLoc);
479 final int centerX = v.getWidth() / 2;
480 final int centerY = v.getHeight() / 2;
481 final int x = targetLoc[0] - parentLoc[0] + centerX;
482 final int y = targetLoc[1] - parentLoc[1] + centerY;
483 mGutsContainer.closeControls(x, y, true /* save */, false /* force */);
487 public void setGutsParent(NotificationGuts guts) {
488 mGutsContainer = guts;
492 public boolean willBeRemoved() {
493 return hasImportanceChanged();
497 public boolean shouldBeSaved() {
498 return hasImportanceChanged();
502 public View getContentView() {
507 public boolean handleCloseControls(boolean save, boolean force) {
508 // Save regardless of the importance so we can lock the importance field if the user wants
509 // to keep getting notifications
513 logBlockingHelperCounter(mExitReason);
518 public int getActualHeight() {
523 * Runnable to either update the given channel (with a new importance value) or, if no channel
524 * is provided, update notifications enabled state for the package.
526 private static class UpdateImportanceRunnable implements Runnable {
527 private final INotificationManager mINotificationManager;
528 private final String mPackageName;
529 private final int mAppUid;
530 private final @Nullable NotificationChannel mChannelToUpdate;
531 private final int mCurrentImportance;
532 private final int mNewImportance;
535 public UpdateImportanceRunnable(INotificationManager notificationManager,
536 String packageName, int appUid, @Nullable NotificationChannel channelToUpdate,
537 int currentImportance, int newImportance) {
538 mINotificationManager = notificationManager;
539 mPackageName = packageName;
541 mChannelToUpdate = channelToUpdate;
542 mCurrentImportance = currentImportance;
543 mNewImportance = newImportance;
549 if (mChannelToUpdate != null) {
550 mChannelToUpdate.setImportance(mNewImportance);
551 mChannelToUpdate.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
552 mINotificationManager.updateNotificationChannelForPackage(
553 mPackageName, mAppUid, mChannelToUpdate);
555 // For notifications with more than one channel, update notification enabled
556 // state. If the importance was lowered, we disable notifications.
557 mINotificationManager.setNotificationsEnabledWithImportanceLockForPackage(
558 mPackageName, mAppUid, mNewImportance >= mCurrentImportance);
560 } catch (RemoteException e) {
561 Log.e(TAG, "Unable to update notification importance", e);