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_NONE;
     20 
     21 import android.app.INotificationManager;
     22 import android.app.Notification;
     23 import android.app.NotificationChannel;
     24 import android.app.NotificationChannelGroup;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.pm.ActivityInfo;
     28 import android.content.pm.ApplicationInfo;
     29 import android.content.pm.PackageInfo;
     30 import android.content.pm.PackageManager;
     31 import android.content.pm.ResolveInfo;
     32 import android.graphics.drawable.Drawable;
     33 import android.os.RemoteException;
     34 import android.service.notification.StatusBarNotification;
     35 import android.text.TextUtils;
     36 import android.util.AttributeSet;
     37 import android.view.View;
     38 import android.view.accessibility.AccessibilityEvent;
     39 import android.widget.ImageView;
     40 import android.widget.LinearLayout;
     41 import android.widget.Switch;
     42 import android.widget.TextView;
     43 
     44 import com.android.internal.logging.MetricsLogger;
     45 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     46 import com.android.settingslib.Utils;
     47 import com.android.systemui.R;
     48 
     49 import java.lang.IllegalArgumentException;
     50 import java.util.List;
     51 import java.util.Set;
     52 
     53 /**
     54  * The guts of a notification revealed when performing a long press.
     55  */
     56 public class NotificationInfo extends LinearLayout implements NotificationGuts.GutsContent {
     57     private static final String TAG = "InfoGuts";
     58 
     59     private INotificationManager mINotificationManager;
     60     private String mPkg;
     61     private String mAppName;
     62     private int mAppUid;
     63     private List<NotificationChannel> mNotificationChannels;
     64     private NotificationChannel mSingleNotificationChannel;
     65     private boolean mIsSingleDefaultChannel;
     66     private StatusBarNotification mSbn;
     67     private int mStartingUserImportance;
     68 
     69     private TextView mNumChannelsView;
     70     private View mChannelDisabledView;
     71     private TextView mSettingsLinkView;
     72     private Switch mChannelEnabledSwitch;
     73     private CheckSaveListener mCheckSaveListener;
     74     private OnAppSettingsClickListener mAppSettingsClickListener;
     75     private PackageManager mPm;
     76 
     77     private NotificationGuts mGutsContainer;
     78 
     79     public NotificationInfo(Context context, AttributeSet attrs) {
     80         super(context, attrs);
     81     }
     82 
     83     // Specify a CheckSaveListener to override when/if the user's changes are committed.
     84     public interface CheckSaveListener {
     85         // Invoked when importance has changed and the NotificationInfo wants to try to save it.
     86         // Listener should run saveImportance unless the change should be canceled.
     87         void checkSave(Runnable saveImportance);
     88     }
     89 
     90     public interface OnSettingsClickListener {
     91         void onClick(View v, NotificationChannel channel, int appUid);
     92     }
     93 
     94     public interface OnAppSettingsClickListener {
     95         void onClick(View v, Intent intent);
     96     }
     97 
     98     public void bindNotification(final PackageManager pm,
     99             final INotificationManager iNotificationManager,
    100             final String pkg,
    101             final List<NotificationChannel> notificationChannels,
    102             int startingUserImportance,
    103             final StatusBarNotification sbn,
    104             OnSettingsClickListener onSettingsClick,
    105             OnAppSettingsClickListener onAppSettingsClick,
    106             OnClickListener onDoneClick,
    107             CheckSaveListener checkSaveListener,
    108             final Set<String> nonBlockablePkgs)
    109             throws RemoteException {
    110         mINotificationManager = iNotificationManager;
    111         mPkg = pkg;
    112         mNotificationChannels = notificationChannels;
    113         mCheckSaveListener = checkSaveListener;
    114         mSbn = sbn;
    115         mPm = pm;
    116         mAppSettingsClickListener = onAppSettingsClick;
    117         mStartingUserImportance = startingUserImportance;
    118         mAppName = mPkg;
    119         Drawable pkgicon = null;
    120         CharSequence channelNameText = "";
    121         ApplicationInfo info = null;
    122         try {
    123             info = pm.getApplicationInfo(mPkg,
    124                     PackageManager.MATCH_UNINSTALLED_PACKAGES
    125                             | PackageManager.MATCH_DISABLED_COMPONENTS
    126                             | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
    127                             | PackageManager.MATCH_DIRECT_BOOT_AWARE);
    128             if (info != null) {
    129                 mAppUid = info.uid;
    130                 mAppName = String.valueOf(pm.getApplicationLabel(info));
    131                 pkgicon = pm.getApplicationIcon(info);
    132             }
    133         } catch (PackageManager.NameNotFoundException e) {
    134             // app is gone, just show package name and generic icon
    135             pkgicon = pm.getDefaultActivityIcon();
    136         }
    137         ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon);
    138 
    139         int numTotalChannels = iNotificationManager.getNumNotificationChannelsForPackage(
    140                 pkg, mAppUid, false /* includeDeleted */);
    141         if (mNotificationChannels.isEmpty()) {
    142             throw new IllegalArgumentException("bindNotification requires at least one channel");
    143         } else  {
    144             if (mNotificationChannels.size() == 1) {
    145                 mSingleNotificationChannel = mNotificationChannels.get(0);
    146                 // Special behavior for the Default channel if no other channels have been defined.
    147                 mIsSingleDefaultChannel =
    148                         (mSingleNotificationChannel.getId()
    149                                 .equals(NotificationChannel.DEFAULT_CHANNEL_ID) &&
    150                         numTotalChannels <= 1);
    151             } else {
    152                 mSingleNotificationChannel = null;
    153                 mIsSingleDefaultChannel = false;
    154             }
    155         }
    156 
    157         String channelsDescText;
    158         mNumChannelsView = findViewById(R.id.num_channels_desc);
    159         if (mIsSingleDefaultChannel) {
    160             channelsDescText = mContext.getString(R.string.notification_default_channel_desc);
    161         } else {
    162             switch (mNotificationChannels.size()) {
    163                 case 1:
    164                     channelsDescText = String.format(mContext.getResources().getQuantityString(
    165                             R.plurals.notification_num_channels_desc, numTotalChannels),
    166                             numTotalChannels);
    167                     break;
    168                 case 2:
    169                     channelsDescText = mContext.getString(
    170                             R.string.notification_channels_list_desc_2,
    171                             mNotificationChannels.get(0).getName(),
    172                             mNotificationChannels.get(1).getName());
    173                     break;
    174                 default:
    175                     final int numOthers = mNotificationChannels.size() - 2;
    176                     channelsDescText = String.format(
    177                             mContext.getResources().getQuantityString(
    178                                     R.plurals.notification_channels_list_desc_2_and_others,
    179                                     numOthers),
    180                             mNotificationChannels.get(0).getName(),
    181                             mNotificationChannels.get(1).getName(),
    182                             numOthers);
    183             }
    184         }
    185         mNumChannelsView.setText(channelsDescText);
    186 
    187         if (mSingleNotificationChannel == null) {
    188             // Multiple channels don't use a channel name for the title.
    189             channelNameText = mContext.getString(R.string.notification_num_channels,
    190                     mNotificationChannels.size());
    191         } else if (mIsSingleDefaultChannel) {
    192             // If this is the default channel, don't use our channel-specific text.
    193             channelNameText = mContext.getString(R.string.notification_header_default_channel);
    194         } else {
    195             channelNameText = mSingleNotificationChannel.getName();
    196         }
    197         ((TextView) findViewById(R.id.pkgname)).setText(mAppName);
    198         ((TextView) findViewById(R.id.channel_name)).setText(channelNameText);
    199 
    200         // Set group information if this channel has an associated group.
    201         CharSequence groupName = null;
    202         if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) {
    203             final NotificationChannelGroup notificationChannelGroup =
    204                     iNotificationManager.getNotificationChannelGroupForPackage(
    205                             mSingleNotificationChannel.getGroup(), pkg, mAppUid);
    206             if (notificationChannelGroup != null) {
    207                 groupName = notificationChannelGroup.getName();
    208             }
    209         }
    210         TextView groupNameView = ((TextView) findViewById(R.id.group_name));
    211         TextView groupDividerView = ((TextView) findViewById(R.id.pkg_group_divider));
    212         if (groupName != null) {
    213             groupNameView.setText(groupName);
    214             groupNameView.setVisibility(View.VISIBLE);
    215             groupDividerView.setVisibility(View.VISIBLE);
    216         } else {
    217             groupNameView.setVisibility(View.GONE);
    218             groupDividerView.setVisibility(View.GONE);
    219         }
    220 
    221         boolean nonBlockable = false;
    222         try {
    223             final PackageInfo pkgInfo = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES);
    224             nonBlockable = Utils.isSystemPackage(getResources(), pm, pkgInfo)
    225                     && (mSingleNotificationChannel == null
    226                     || !mSingleNotificationChannel.isBlockableSystem());
    227         } catch (PackageManager.NameNotFoundException e) {
    228             // unlikely.
    229         }
    230         if (nonBlockablePkgs != null) {
    231             nonBlockable |= nonBlockablePkgs.contains(pkg);
    232         }
    233 
    234         bindButtons(nonBlockable);
    235 
    236         // Top-level importance group
    237         mChannelDisabledView = findViewById(R.id.channel_disabled);
    238         updateSecondaryText();
    239 
    240         // Settings button.
    241         final TextView settingsButton = (TextView) findViewById(R.id.more_settings);
    242         if (mAppUid >= 0 && onSettingsClick != null) {
    243             settingsButton.setVisibility(View.VISIBLE);
    244             final int appUidF = mAppUid;
    245             settingsButton.setOnClickListener(
    246                     (View view) -> {
    247                         onSettingsClick.onClick(view, mSingleNotificationChannel, appUidF);
    248                     });
    249             if (numTotalChannels > 1) {
    250                 settingsButton.setText(R.string.notification_all_categories);
    251             } else {
    252                 settingsButton.setText(R.string.notification_more_settings);
    253             }
    254 
    255         } else {
    256             settingsButton.setVisibility(View.GONE);
    257         }
    258 
    259         // Done button.
    260         final TextView doneButton = (TextView) findViewById(R.id.done);
    261         doneButton.setText(R.string.notification_done);
    262         doneButton.setOnClickListener(onDoneClick);
    263 
    264         // Optional settings link
    265         updateAppSettingsLink();
    266     }
    267 
    268     private boolean hasImportanceChanged() {
    269         return mSingleNotificationChannel != null &&
    270                 mChannelEnabledSwitch != null &&
    271                 mStartingUserImportance != getSelectedImportance();
    272     }
    273 
    274     private void saveImportance() {
    275         if (!hasImportanceChanged()) {
    276             return;
    277         }
    278         final int selectedImportance = getSelectedImportance();
    279         MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
    280                 selectedImportance - mStartingUserImportance);
    281         mSingleNotificationChannel.setImportance(selectedImportance);
    282         mSingleNotificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
    283         try {
    284             mINotificationManager.updateNotificationChannelForPackage(
    285                     mPkg, mAppUid, mSingleNotificationChannel);
    286         } catch (RemoteException e) {
    287             // :(
    288         }
    289     }
    290 
    291     private int getSelectedImportance() {
    292         if (!mChannelEnabledSwitch.isChecked()) {
    293             return IMPORTANCE_NONE;
    294         } else {
    295             return mStartingUserImportance;
    296         }
    297     }
    298 
    299     private void bindButtons(final boolean nonBlockable) {
    300         // Enabled Switch
    301         mChannelEnabledSwitch = (Switch) findViewById(R.id.channel_enabled_switch);
    302         mChannelEnabledSwitch.setChecked(
    303                 mStartingUserImportance != IMPORTANCE_NONE);
    304         final boolean visible = !nonBlockable && mSingleNotificationChannel != null;
    305         mChannelEnabledSwitch.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
    306 
    307         // Callback when checked.
    308         mChannelEnabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
    309             if (mGutsContainer != null) {
    310                 mGutsContainer.resetFalsingCheck();
    311             }
    312             updateSecondaryText();
    313             updateAppSettingsLink();
    314         });
    315     }
    316 
    317     @Override
    318     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    319         super.onInitializeAccessibilityEvent(event);
    320         if (mGutsContainer != null &&
    321                 event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
    322             if (mGutsContainer.isExposed()) {
    323                 event.getText().add(mContext.getString(
    324                         R.string.notification_channel_controls_opened_accessibility, mAppName));
    325             } else {
    326                 event.getText().add(mContext.getString(
    327                         R.string.notification_channel_controls_closed_accessibility, mAppName));
    328             }
    329         }
    330     }
    331 
    332     private void updateSecondaryText() {
    333         final boolean disabled = mSingleNotificationChannel != null &&
    334                 getSelectedImportance() == IMPORTANCE_NONE;
    335         if (disabled) {
    336             mChannelDisabledView.setVisibility(View.VISIBLE);
    337             mNumChannelsView.setVisibility(View.GONE);
    338         } else {
    339             mChannelDisabledView.setVisibility(View.GONE);
    340             mNumChannelsView.setVisibility(mIsSingleDefaultChannel ? View.INVISIBLE : View.VISIBLE);
    341         }
    342     }
    343 
    344     private void updateAppSettingsLink() {
    345         mSettingsLinkView = findViewById(R.id.app_settings);
    346         Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel,
    347                 mSbn.getId(), mSbn.getTag());
    348         if (settingsIntent != null && getSelectedImportance() != IMPORTANCE_NONE
    349                 && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
    350             mSettingsLinkView.setVisibility(View.VISIBLE);
    351             mSettingsLinkView.setText(mContext.getString(R.string.notification_app_settings,
    352                     mSbn.getNotification().getSettingsText()));
    353             mSettingsLinkView.setOnClickListener((View view) -> {
    354                 mAppSettingsClickListener.onClick(view, settingsIntent);
    355             });
    356         } else {
    357             mSettingsLinkView.setVisibility(View.GONE);
    358         }
    359     }
    360 
    361     private Intent getAppSettingsIntent(PackageManager pm, String packageName,
    362             NotificationChannel channel, int id, String tag) {
    363         Intent intent = new Intent(Intent.ACTION_MAIN)
    364                 .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES)
    365                 .setPackage(packageName);
    366         final List<ResolveInfo> resolveInfos = pm.queryIntentActivities(
    367                 intent,
    368                 PackageManager.MATCH_DEFAULT_ONLY
    369         );
    370         if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) {
    371             return null;
    372         }
    373         final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
    374         intent.setClassName(activityInfo.packageName, activityInfo.name);
    375         if (channel != null) {
    376             intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId());
    377         }
    378         intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id);
    379         intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag);
    380         return intent;
    381     }
    382 
    383     @Override
    384     public void setGutsParent(NotificationGuts guts) {
    385         mGutsContainer = guts;
    386     }
    387 
    388     @Override
    389     public boolean willBeRemoved() {
    390         return mChannelEnabledSwitch != null && !mChannelEnabledSwitch.isChecked();
    391     }
    392 
    393     @Override
    394     public View getContentView() {
    395         return this;
    396     }
    397 
    398     @Override
    399     public boolean handleCloseControls(boolean save, boolean force) {
    400         if (save && hasImportanceChanged()) {
    401             if (mCheckSaveListener != null) {
    402                 mCheckSaveListener.checkSave(() -> { saveImportance(); });
    403             } else {
    404                 saveImportance();
    405             }
    406         }
    407         return false;
    408     }
    409 
    410     @Override
    411     public int getActualHeight() {
    412         return getHeight();
    413     }
    414 }
    415