Home | History | Annotate | Download | only in statusbar
      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 
    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 
    421     @Override
    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 
    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
    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
    481     public void setGutsParent(NotificationGuts guts) {
    482         mGutsContainer = guts;
    483     }
    484 
    485     @Override
    486     public boolean willBeRemoved() {
    487         return hasImportanceChanged();
    488     }
    489 
    490     @Override
    491     public boolean shouldBeSaved() {
    492         return hasImportanceChanged();
    493     }
    494 
    495     @Override
    496     public View getContentView() {
    497         return this;
    498     }
    499 
    500     @Override
    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
    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 
    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
    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