• 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;
18 
19 import static android.app.NotificationManager.IMPORTANCE_MIN;
20 import static android.app.NotificationManager.IMPORTANCE_NONE;
21 
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;
50 
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.NotificationCounters;
58 
59 import java.util.List;
60 
61 /**
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.
64  */
65 public class NotificationInfo extends LinearLayout implements NotificationGuts.GutsContent {
66     private static final String TAG = "InfoGuts";
67 
68     private INotificationManager mINotificationManager;
69     private PackageManager mPm;
70     private MetricsLogger mMetricsLogger;
71 
72     private String mPackageName;
73     private String mAppName;
74     private int mAppUid;
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;
84 
85     private CheckSaveListener mCheckSaveListener;
86     private OnSettingsClickListener mOnSettingsClickListener;
87     private OnAppSettingsClickListener mAppSettingsClickListener;
88     private NotificationGuts mGutsContainer;
89 
90     /** Whether this view is being shown as part of the blocking helper. */
91     private boolean mIsForBlockingHelper;
92     private boolean mNegativeUserSentiment;
93 
94     /**
95      * String that describes how the user exit or quit out of this view, also used as a counter tag.
96      */
97     private String mExitReason = NotificationCounters.BLOCKING_HELPER_DISMISSED;
98 
99     private OnClickListener mOnKeepShowing = v -> {
100         mExitReason = NotificationCounters.BLOCKING_HELPER_KEEP_SHOWING;
101         closeControls(v);
102     };
103 
104     private OnClickListener mOnStopOrMinimizeNotifications = v -> {
105         mExitReason = NotificationCounters.BLOCKING_HELPER_STOP_NOTIFICATIONS;
106         swapContent(false);
107     };
108 
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);
113         swapContent(true);
114     };
115 
NotificationInfo(Context context, AttributeSet attrs)116     public NotificationInfo(Context context, AttributeSet attrs) {
117         super(context, attrs);
118     }
119 
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.
checkSave(Runnable saveImportance, StatusBarNotification sbn)124         void checkSave(Runnable saveImportance, StatusBarNotification sbn);
125     }
126 
127     public interface OnSettingsClickListener {
onClick(View v, NotificationChannel channel, int appUid)128         void onClick(View v, NotificationChannel channel, int appUid);
129     }
130 
131     public interface OnAppSettingsClickListener {
onClick(View v, Intent intent)132         void onClick(View v, Intent intent);
133     }
134 
135     @VisibleForTesting
bindNotification( final PackageManager pm, final INotificationManager iNotificationManager, final String pkg, final NotificationChannel notificationChannel, final int numUniqueChannelsInRow, final StatusBarNotification sbn, final CheckSaveListener checkSaveListener, final OnSettingsClickListener onSettingsClick, final OnAppSettingsClickListener onAppSettingsClick, boolean isNonblockable)136     void bindNotification(
137             final PackageManager pm,
138             final INotificationManager iNotificationManager,
139             final String pkg,
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 */);
152     }
153 
bindNotification( PackageManager pm, INotificationManager iNotificationManager, String pkg, NotificationChannel notificationChannel, int numUniqueChannelsInRow, StatusBarNotification sbn, CheckSaveListener checkSaveListener, OnSettingsClickListener onSettingsClick, OnAppSettingsClickListener onAppSettingsClick, boolean isNonblockable, boolean isForBlockingHelper, boolean isUserSentimentNegative)154     public void bindNotification(
155             PackageManager pm,
156             INotificationManager iNotificationManager,
157             String pkg,
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);
170         mPackageName = pkg;
171         mNumUniqueChannelsInRow = numUniqueChannelsInRow;
172         mSbn = sbn;
173         mPm = pm;
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;
182         mIsForeground =
183                 (mSbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
184         mIsForBlockingHelper = isForBlockingHelper;
185         mAppUid = mSbn.getUid();
186 
187         int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage(
188                 pkg, mAppUid, false /* includeDeleted */);
189         if (mNumUniqueChannelsInRow == 0) {
190             throw new IllegalArgumentException("bindNotification requires at least one channel");
191         } else  {
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;
197         }
198 
199         bindHeader();
200         bindPrompt();
201         bindButtons();
202     }
203 
bindHeader()204     private void bindHeader() throws RemoteException {
205         // Package name
206         Drawable pkgicon = null;
207         ApplicationInfo info;
208         try {
209             info = mPm.getApplicationInfo(
210                     mPackageName,
211                     PackageManager.MATCH_UNINSTALLED_PACKAGES
212                             | PackageManager.MATCH_DISABLED_COMPONENTS
213                             | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
214                             | PackageManager.MATCH_DIRECT_BOOT_AWARE);
215             if (info != null) {
216                 mAppName = String.valueOf(mPm.getApplicationLabel(info));
217                 pkgicon = mPm.getApplicationIcon(info);
218             }
219         } catch (PackageManager.NameNotFoundException e) {
220             // app is gone, just show package name and generic icon
221             pkgicon = mPm.getDefaultActivityIcon();
222         }
223         ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon);
224         ((TextView) findViewById(R.id.pkgname)).setText(mAppName);
225 
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();
234             }
235         }
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);
242         } else {
243             groupNameView.setVisibility(View.GONE);
244             groupDividerView.setVisibility(View.GONE);
245         }
246 
247         // Settings button.
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(
253                     (View view) -> {
254                         logBlockingHelperCounter(
255                                 NotificationCounters.BLOCKING_HELPER_NOTIF_SETTINGS);
256                         mOnSettingsClickListener.onClick(view,
257                                 mNumUniqueChannelsInRow > 1 ? null : mSingleNotificationChannel,
258                                 appUidF);
259                     });
260         } else {
261             settingsButton.setVisibility(View.GONE);
262         }
263     }
264 
bindPrompt()265     private void bindPrompt() {
266         final TextView blockPrompt = findViewById(R.id.block_prompt);
267         bindName();
268         if (mIsNonblockable) {
269             blockPrompt.setText(R.string.notification_unblockable_desc);
270         } else {
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);
275             } else {
276                 blockPrompt.setText(R.string.inline_keep_showing);
277             }
278         }
279     }
280 
bindName()281     private void bindName() {
282         final TextView channelName = findViewById(R.id.channel_name);
283         if (mIsSingleDefaultChannel || mNumUniqueChannelsInRow > 1) {
284             channelName.setVisibility(View.GONE);
285         } else {
286             channelName.setText(mSingleNotificationChannel.getName());
287         }
288     }
289 
290     @VisibleForTesting
logBlockingHelperCounter(String counterTag)291     void logBlockingHelperCounter(String counterTag) {
292         if (mIsForBlockingHelper) {
293             mMetricsLogger.count(counterTag, 1);
294         }
295     }
296 
hasImportanceChanged()297     private boolean hasImportanceChanged() {
298         return mSingleNotificationChannel != null && mStartingUserImportance != mChosenImportance;
299     }
300 
saveImportance()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(
307                             mExitReason)) {
308                 mCheckSaveListener.checkSave(this::updateImportance, mSbn);
309             } else {
310                 updateImportance();
311             }
312         }
313     }
314 
315     /**
316      * Commits the updated importance values on the background thread.
317      */
updateImportance()318     private void updateImportance() {
319         MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
320                 mChosenImportance - mStartingUserImportance);
321 
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));
326     }
327 
bindButtons()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);
333 
334         findViewById(R.id.undo).setOnClickListener(mOnUndo);
335         block.setOnClickListener(mOnStopOrMinimizeNotifications);
336         keep.setOnClickListener(mOnKeepShowing);
337         minimize.setOnClickListener(mOnStopOrMinimizeNotifications);
338 
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);
349         }
350 
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);
362             });
363         } else {
364             settingsLinkView.setVisibility(View.GONE);
365         }
366     }
367 
swapContent(boolean showPrompt)368     private void swapContent(boolean showPrompt) {
369         if (mExpandAnimation != null) {
370             mExpandAnimation.cancel();
371         }
372 
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);
377 
378         if (showPrompt) {
379             mChosenImportance = mStartingUserImportance;
380         } else if (mIsForeground) {
381             mChosenImportance = IMPORTANCE_MIN;
382             confirmationText.setText(R.string.notification_channel_minimized);
383         } else {
384             mChosenImportance = IMPORTANCE_NONE;
385             confirmationText.setText(R.string.notification_channel_disabled);
386         }
387 
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);
394 
395         prompt.setVisibility(showPrompt ? VISIBLE : GONE);
396         confirmation.setVisibility(showPrompt ? GONE : VISIBLE);
397         header.setVisibility(showPrompt ? VISIBLE : GONE);
398 
399         mExpandAnimation = new AnimatorSet();
400         mExpandAnimation.playTogether(promptAnim, confirmAnim);
401         mExpandAnimation.setDuration(150);
402         mExpandAnimation.addListener(new AnimatorListenerAdapter() {
403             boolean cancelled = false;
404 
405             @Override
406             public void onAnimationCancel(Animator animation) {
407                 cancelled = true;
408             }
409 
410             @Override
411             public void onAnimationEnd(Animator animation) {
412                 if (!cancelled) {
413                     prompt.setVisibility(showPrompt ? VISIBLE : GONE);
414                     confirmation.setVisibility(showPrompt ? GONE : VISIBLE);
415                 }
416             }
417         });
418         mExpandAnimation.start();
419     }
420 
421     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)422     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
423         super.onInitializeAccessibilityEvent(event);
424         if (mGutsContainer != null &&
425                 event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
426             if (mGutsContainer.isExposed()) {
427                 event.getText().add(mContext.getString(
428                         R.string.notification_channel_controls_opened_accessibility, mAppName));
429             } else {
430                 event.getText().add(mContext.getString(
431                         R.string.notification_channel_controls_closed_accessibility, mAppName));
432             }
433         }
434     }
435 
getAppSettingsIntent(PackageManager pm, String packageName, NotificationChannel channel, int id, String tag)436     private Intent getAppSettingsIntent(PackageManager pm, String packageName,
437             NotificationChannel channel, int id, String tag) {
438         Intent intent = new Intent(Intent.ACTION_MAIN)
439                 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
440                 .setPackage(packageName);
441         final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
442                 intent,
443                 PackageManager.MATCH_DEFAULT_ONLY
444         );
445         if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
446             return null;
447         }
448         final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
449         intent.setClassName(activityInfo.packageName, activityInfo.name);
450         if (channel != null) {
451             intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId());
452         }
453         intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id);
454         intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag);
455         return intent;
456     }
457 
458     /**
459      * Closes the controls and commits the updated importance values (indirectly). If this view is
460      * being used to show the blocking helper, this will immediately dismiss the blocking helper and
461      * commit the updated importance.
462      *
463      * <p><b>Note,</b> this will only get called once the view is dismissing. This means that the
464      * user does not have the ability to undo the action anymore. See {@link #swapContent(boolean)}
465      * for where undo is handled.
466      */
467     @VisibleForTesting
closeControls(View v)468     void closeControls(View v) {
469         int[] parentLoc = new int[2];
470         int[] targetLoc = new int[2];
471         mGutsContainer.getLocationOnScreen(parentLoc);
472         v.getLocationOnScreen(targetLoc);
473         final int centerX = v.getWidth() / 2;
474         final int centerY = v.getHeight() / 2;
475         final int x = targetLoc[0] - parentLoc[0] + centerX;
476         final int y = targetLoc[1] - parentLoc[1] + centerY;
477         mGutsContainer.closeControls(x, y, true /* save */, false /* force */);
478     }
479 
480     @Override
setGutsParent(NotificationGuts guts)481     public void setGutsParent(NotificationGuts guts) {
482         mGutsContainer = guts;
483     }
484 
485     @Override
willBeRemoved()486     public boolean willBeRemoved() {
487         return hasImportanceChanged();
488     }
489 
490     @Override
shouldBeSaved()491     public boolean shouldBeSaved() {
492         return hasImportanceChanged();
493     }
494 
495     @Override
getContentView()496     public View getContentView() {
497         return this;
498     }
499 
500     @Override
handleCloseControls(boolean save, boolean force)501     public boolean handleCloseControls(boolean save, boolean force) {
502         // Save regardless of the importance so we can lock the importance field if the user wants
503         // to keep getting notifications
504         if (save) {
505             saveImportance();
506         }
507         logBlockingHelperCounter(mExitReason);
508         return false;
509     }
510 
511     @Override
getActualHeight()512     public int getActualHeight() {
513         return getHeight();
514     }
515 
516     /**
517      * Runnable to either update the given channel (with a new importance value) or, if no channel
518      * is provided, update notifications enabled state for the package.
519      */
520     private static class UpdateImportanceRunnable implements Runnable {
521         private final INotificationManager mINotificationManager;
522         private final String mPackageName;
523         private final int mAppUid;
524         private final @Nullable NotificationChannel mChannelToUpdate;
525         private final int mCurrentImportance;
526         private final int mNewImportance;
527 
528 
UpdateImportanceRunnable(INotificationManager notificationManager, String packageName, int appUid, @Nullable NotificationChannel channelToUpdate, int currentImportance, int newImportance)529         public UpdateImportanceRunnable(INotificationManager notificationManager,
530                 String packageName, int appUid, @Nullable NotificationChannel channelToUpdate,
531                 int currentImportance, int newImportance) {
532             mINotificationManager = notificationManager;
533             mPackageName = packageName;
534             mAppUid = appUid;
535             mChannelToUpdate = channelToUpdate;
536             mCurrentImportance = currentImportance;
537             mNewImportance = newImportance;
538         }
539 
540         @Override
run()541         public void run() {
542             try {
543                 if (mChannelToUpdate != null) {
544                     mChannelToUpdate.setImportance(mNewImportance);
545                     mChannelToUpdate.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
546                     mINotificationManager.updateNotificationChannelForPackage(
547                             mPackageName, mAppUid, mChannelToUpdate);
548                 } else {
549                     // For notifications with more than one channel, update notification enabled
550                     // state. If the importance was lowered, we disable notifications.
551                     mINotificationManager.setNotificationsEnabledWithImportanceLockForPackage(
552                             mPackageName, mAppUid, mNewImportance >= mCurrentImportance);
553                 }
554             } catch (RemoteException e) {
555                 Log.e(TAG, "Unable to update notification importance", e);
556             }
557         }
558     }
559 }
560