/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.systemui.statusbar; import static android.app.NotificationManager.IMPORTANCE_NONE; import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; import android.os.RemoteException; import android.service.notification.StatusBarNotification; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Switch; import android.widget.TextView; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settingslib.Utils; import com.android.systemui.R; import java.lang.IllegalArgumentException; import java.util.List; import java.util.Set; /** * The guts of a notification revealed when performing a long press. */ public class NotificationInfo extends LinearLayout implements NotificationGuts.GutsContent { private static final String TAG = "InfoGuts"; private INotificationManager mINotificationManager; private String mPkg; private String mAppName; private int mAppUid; private List mNotificationChannels; private NotificationChannel mSingleNotificationChannel; private boolean mIsSingleDefaultChannel; private StatusBarNotification mSbn; private int mStartingUserImportance; private TextView mNumChannelsView; private View mChannelDisabledView; private TextView mSettingsLinkView; private Switch mChannelEnabledSwitch; private CheckSaveListener mCheckSaveListener; private OnAppSettingsClickListener mAppSettingsClickListener; private PackageManager mPm; private NotificationGuts mGutsContainer; public NotificationInfo(Context context, AttributeSet attrs) { super(context, attrs); } // Specify a CheckSaveListener to override when/if the user's changes are committed. public interface CheckSaveListener { // Invoked when importance has changed and the NotificationInfo wants to try to save it. // Listener should run saveImportance unless the change should be canceled. void checkSave(Runnable saveImportance); } public interface OnSettingsClickListener { void onClick(View v, NotificationChannel channel, int appUid); } public interface OnAppSettingsClickListener { void onClick(View v, Intent intent); } public void bindNotification(final PackageManager pm, final INotificationManager iNotificationManager, final String pkg, final List notificationChannels, int startingUserImportance, final StatusBarNotification sbn, OnSettingsClickListener onSettingsClick, OnAppSettingsClickListener onAppSettingsClick, OnClickListener onDoneClick, CheckSaveListener checkSaveListener, final Set nonBlockablePkgs) throws RemoteException { mINotificationManager = iNotificationManager; mPkg = pkg; mNotificationChannels = notificationChannels; mCheckSaveListener = checkSaveListener; mSbn = sbn; mPm = pm; mAppSettingsClickListener = onAppSettingsClick; mStartingUserImportance = startingUserImportance; mAppName = mPkg; Drawable pkgicon = null; CharSequence channelNameText = ""; ApplicationInfo info = null; try { info = pm.getApplicationInfo(mPkg, PackageManager.MATCH_UNINSTALLED_PACKAGES | PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_DIRECT_BOOT_AWARE); if (info != null) { mAppUid = sbn.getUid(); mAppName = String.valueOf(pm.getApplicationLabel(info)); pkgicon = pm.getApplicationIcon(info); } } catch (PackageManager.NameNotFoundException e) { // app is gone, just show package name and generic icon pkgicon = pm.getDefaultActivityIcon(); } ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon); int numTotalChannels = iNotificationManager.getNumNotificationChannelsForPackage( pkg, mAppUid, false /* includeDeleted */); if (mNotificationChannels.isEmpty()) { throw new IllegalArgumentException("bindNotification requires at least one channel"); } else { if (mNotificationChannels.size() == 1) { mSingleNotificationChannel = mNotificationChannels.get(0); // Special behavior for the Default channel if no other channels have been defined. mIsSingleDefaultChannel = (mSingleNotificationChannel.getId() .equals(NotificationChannel.DEFAULT_CHANNEL_ID) && numTotalChannels <= 1); } else { mSingleNotificationChannel = null; mIsSingleDefaultChannel = false; } } boolean nonBlockable = false; try { final PackageInfo pkgInfo = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES); if (Utils.isSystemPackage(getResources(), pm, pkgInfo)) { final int numChannels = mNotificationChannels.size(); for (int i = 0; i < numChannels; i++) { final NotificationChannel notificationChannel = mNotificationChannels.get(i); // If any of the system channels is not blockable, the bundle is nonblockable if (!notificationChannel.isBlockableSystem()) { nonBlockable = true; break; } } } } catch (PackageManager.NameNotFoundException e) { // unlikely. } if (nonBlockablePkgs != null) { nonBlockable |= nonBlockablePkgs.contains(pkg); } String channelsDescText; mNumChannelsView = findViewById(R.id.num_channels_desc); if (nonBlockable) { channelsDescText = mContext.getString(R.string.notification_unblockable_desc); } else if (mIsSingleDefaultChannel) { channelsDescText = mContext.getString(R.string.notification_default_channel_desc); } else { switch (mNotificationChannels.size()) { case 1: channelsDescText = String.format(mContext.getResources().getQuantityString( R.plurals.notification_num_channels_desc, numTotalChannels), numTotalChannels); break; case 2: channelsDescText = mContext.getString( R.string.notification_channels_list_desc_2, mNotificationChannels.get(0).getName(), mNotificationChannels.get(1).getName()); break; default: final int numOthers = mNotificationChannels.size() - 2; channelsDescText = String.format( mContext.getResources().getQuantityString( R.plurals.notification_channels_list_desc_2_and_others, numOthers), mNotificationChannels.get(0).getName(), mNotificationChannels.get(1).getName(), numOthers); } } mNumChannelsView.setText(channelsDescText); if (mSingleNotificationChannel == null) { // Multiple channels don't use a channel name for the title. channelNameText = mContext.getString(R.string.notification_num_channels, mNotificationChannels.size()); } else if (mIsSingleDefaultChannel || nonBlockable) { // If this is the default channel or the app is unblockable, // don't use our channel-specific text. channelNameText = mContext.getString(R.string.notification_header_default_channel); } else { channelNameText = mSingleNotificationChannel.getName(); } ((TextView) findViewById(R.id.pkgname)).setText(mAppName); ((TextView) findViewById(R.id.channel_name)).setText(channelNameText); // Set group information if this channel has an associated group. CharSequence groupName = null; if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) { final NotificationChannelGroup notificationChannelGroup = iNotificationManager.getNotificationChannelGroupForPackage( mSingleNotificationChannel.getGroup(), pkg, mAppUid); if (notificationChannelGroup != null) { groupName = notificationChannelGroup.getName(); } } TextView groupNameView = ((TextView) findViewById(R.id.group_name)); TextView groupDividerView = ((TextView) findViewById(R.id.pkg_group_divider)); if (groupName != null) { groupNameView.setText(groupName); groupNameView.setVisibility(View.VISIBLE); groupDividerView.setVisibility(View.VISIBLE); } else { groupNameView.setVisibility(View.GONE); groupDividerView.setVisibility(View.GONE); } bindButtons(nonBlockable); // Top-level importance group mChannelDisabledView = findViewById(R.id.channel_disabled); updateSecondaryText(); // Settings button. final TextView settingsButton = (TextView) findViewById(R.id.more_settings); if (mAppUid >= 0 && onSettingsClick != null) { settingsButton.setVisibility(View.VISIBLE); final int appUidF = mAppUid; settingsButton.setOnClickListener( (View view) -> { onSettingsClick.onClick(view, mSingleNotificationChannel, appUidF); }); if (numTotalChannels <= 1 || nonBlockable) { settingsButton.setText(R.string.notification_more_settings); } else { settingsButton.setText(R.string.notification_all_categories); } } else { settingsButton.setVisibility(View.GONE); } // Done button. final TextView doneButton = (TextView) findViewById(R.id.done); doneButton.setText(R.string.notification_done); doneButton.setOnClickListener(onDoneClick); // Optional settings link updateAppSettingsLink(); } private boolean hasImportanceChanged() { return mSingleNotificationChannel != null && mChannelEnabledSwitch != null && mStartingUserImportance != getSelectedImportance(); } private void saveImportance() { if (!hasImportanceChanged()) { return; } final int selectedImportance = getSelectedImportance(); MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE, selectedImportance - mStartingUserImportance); mSingleNotificationChannel.setImportance(selectedImportance); mSingleNotificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); try { mINotificationManager.updateNotificationChannelForPackage( mPkg, mAppUid, mSingleNotificationChannel); } catch (RemoteException e) { // :( } } private int getSelectedImportance() { if (!mChannelEnabledSwitch.isChecked()) { return IMPORTANCE_NONE; } else { return mStartingUserImportance; } } private void bindButtons(final boolean nonBlockable) { // Enabled Switch mChannelEnabledSwitch = (Switch) findViewById(R.id.channel_enabled_switch); mChannelEnabledSwitch.setChecked( mStartingUserImportance != IMPORTANCE_NONE); final boolean visible = !nonBlockable && mSingleNotificationChannel != null; mChannelEnabledSwitch.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); // Callback when checked. mChannelEnabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { if (mGutsContainer != null) { mGutsContainer.resetFalsingCheck(); } updateSecondaryText(); updateAppSettingsLink(); }); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); if (mGutsContainer != null && event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { if (mGutsContainer.isExposed()) { event.getText().add(mContext.getString( R.string.notification_channel_controls_opened_accessibility, mAppName)); } else { event.getText().add(mContext.getString( R.string.notification_channel_controls_closed_accessibility, mAppName)); } } } private void updateSecondaryText() { final boolean disabled = mSingleNotificationChannel != null && getSelectedImportance() == IMPORTANCE_NONE; if (disabled) { mChannelDisabledView.setVisibility(View.VISIBLE); mNumChannelsView.setVisibility(View.GONE); } else { mChannelDisabledView.setVisibility(View.GONE); mNumChannelsView.setVisibility(mIsSingleDefaultChannel ? View.INVISIBLE : View.VISIBLE); } } private void updateAppSettingsLink() { mSettingsLinkView = findViewById(R.id.app_settings); Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel, mSbn.getId(), mSbn.getTag()); if (settingsIntent != null && getSelectedImportance() != IMPORTANCE_NONE && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) { mSettingsLinkView.setVisibility(View.VISIBLE); mSettingsLinkView.setText(mContext.getString(R.string.notification_app_settings, mSbn.getNotification().getSettingsText())); mSettingsLinkView.setOnClickListener((View view) -> { mAppSettingsClickListener.onClick(view, settingsIntent); }); } else { mSettingsLinkView.setVisibility(View.GONE); } } private Intent getAppSettingsIntent(PackageManager pm, String packageName, NotificationChannel channel, int id, String tag) { Intent intent = new Intent(Intent.ACTION_MAIN) .addCategory(Notification.INTENT_CATEGORY_NOTIFICATION_PREFERENCES) .setPackage(packageName); final List resolveInfos = pm.queryIntentActivities( intent, PackageManager.MATCH_DEFAULT_ONLY ); if (resolveInfos == null || resolveInfos.size() == 0 || resolveInfos.get(0) == null) { return null; } final ActivityInfo activityInfo = resolveInfos.get(0).activityInfo; intent.setClassName(activityInfo.packageName, activityInfo.name); if (channel != null) { intent.putExtra(Notification.EXTRA_CHANNEL_ID, channel.getId()); } intent.putExtra(Notification.EXTRA_NOTIFICATION_ID, id); intent.putExtra(Notification.EXTRA_NOTIFICATION_TAG, tag); return intent; } @Override public void setGutsParent(NotificationGuts guts) { mGutsContainer = guts; } @Override public boolean willBeRemoved() { return mChannelEnabledSwitch != null && !mChannelEnabledSwitch.isChecked(); } @Override public View getContentView() { return this; } @Override public boolean handleCloseControls(boolean save, boolean force) { if (save && hasImportanceChanged()) { if (mCheckSaveListener != null) { mCheckSaveListener.checkSave(() -> { saveImportance(); }); } else { saveImportance(); } } return false; } @Override public int getActualHeight() { return getHeight(); } }