blob: 3b23a0c0db89a052c2619c2ca5f8b8e7cdfe0249 [file] [log] [blame]
/*
* 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<NotificationChannel> 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<NotificationChannel> notificationChannels,
int startingUserImportance,
final StatusBarNotification sbn,
OnSettingsClickListener onSettingsClick,
OnAppSettingsClickListener onAppSettingsClick,
OnClickListener onDoneClick,
CheckSaveListener checkSaveListener,
final Set<String> 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<ResolveInfo> 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();
}
}