OSDN Git Service

Organize notification classes in row/stack
[android-x86/frameworks-base.git] / packages / SystemUI / src / com / android / systemui / statusbar / notification / row / NotificationInfo.java
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_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.logging.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
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.
124         void checkSave(Runnable saveImportance, StatusBarNotification sbn);
125     }
126
127     public interface OnSettingsClickListener {
128         void onClick(View v, NotificationChannel channel, int appUid);
129     }
130
131     public interface OnAppSettingsClickListener {
132         void onClick(View v, Intent intent);
133     }
134
135     @VisibleForTesting
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
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
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
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
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
291     void logBlockingHelperCounter(String counterTag) {
292         if (mIsForBlockingHelper) {
293             mMetricsLogger.count(counterTag, 1);
294         }
295     }
296
297     private boolean hasImportanceChanged() {
298         return mSingleNotificationChannel != null && mStartingUserImportance != mChosenImportance;
299     }
300
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      */
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
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
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         // 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();
424         }
425     }
426
427     @Override
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));
435             } else {
436                 event.getText().add(mContext.getString(
437                         R.string.notification_channel_controls_closed_accessibility, mAppName));
438             }
439         }
440     }
441
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(
448                 intent,
449                 PackageManager.MATCH_DEFAULT_ONLY
450         );
451         if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
452             return null;
453         }
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());
458         }
459         intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id);
460         intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag);
461         return intent;
462     }
463
464     /**
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.
468      *
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.
472      */
473     @VisibleForTesting
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 */);
484     }
485
486     @Override
487     public void setGutsParent(NotificationGuts guts) {
488         mGutsContainer = guts;
489     }
490
491     @Override
492     public boolean willBeRemoved() {
493         return hasImportanceChanged();
494     }
495
496     @Override
497     public boolean shouldBeSaved() {
498         return hasImportanceChanged();
499     }
500
501     @Override
502     public View getContentView() {
503         return this;
504     }
505
506     @Override
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
510         if (save) {
511             saveImportance();
512         }
513         logBlockingHelperCounter(mExitReason);
514         return false;
515     }
516
517     @Override
518     public int getActualHeight() {
519         return getHeight();
520     }
521
522     /**
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.
525      */
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;
533
534
535         public UpdateImportanceRunnable(INotificationManager notificationManager,
536                 String packageName, int appUid, @Nullable NotificationChannel channelToUpdate,
537                 int currentImportance, int newImportance) {
538             mINotificationManager = notificationManager;
539             mPackageName = packageName;
540             mAppUid = appUid;
541             mChannelToUpdate = channelToUpdate;
542             mCurrentImportance = currentImportance;
543             mNewImportance = newImportance;
544         }
545
546         @Override
547         public void run() {
548             try {
549                 if (mChannelToUpdate != null) {
550                     mChannelToUpdate.setImportance(mNewImportance);
551                     mChannelToUpdate.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
552                     mINotificationManager.updateNotificationChannelForPackage(
553                             mPackageName, mAppUid, mChannelToUpdate);
554                 } else {
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);
559                 }
560             } catch (RemoteException e) {
561                 Log.e(TAG, "Unable to update notification importance", e);
562             }
563         }
564     }
565 }