Loading res/values/strings.xml +10 −0 Original line number Diff line number Diff line Loading @@ -10524,4 +10524,14 @@ <!-- Text for permission bar chart details in Privacy page. [CHAR LIMIT=NONE] --> <string name="permission_bar_chart_details">See all usage</string> <!-- Title for notification channel slice. [CHAR LIMIT=NONE] --> <string name="manage_app_notification">Manage <xliff:g id="app_name" example="Settings">%1$s</xliff:g> Notifications</string> <!-- Title for no suggested app in notification channel slice. [CHAR LIMIT=NONE] --> <string name="no_suggested_app">No suggested application</string> <!-- Summary for notification channel slice. [CHAR LIMIT=NONE] --> <plurals name="notification_channel_count_summary"> <item quantity="one"><xliff:g id="notification_channel_count">%1$d</xliff:g> notification channel. Tap to manage all.</item> <item quantity="other"><xliff:g id="notification_channel_count">%1$d</xliff:g> notification channels. Tap to manage all.</item> </plurals> </resources> src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java +3 −1 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import static androidx.slice.widget.SliceLiveData.SUPPORTED_SPECS; import static com.android.settings.slices.CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI; import static com.android.settings.slices.CustomSliceRegistry.CONTEXTUAL_WIFI_SLICE_URI; import static com.android.settings.slices.CustomSliceRegistry.NOTIFICATION_CHANNEL_SLICE_URI; import android.content.ContentProviderClient; import android.content.ContentResolver; Loading Loading @@ -205,7 +206,8 @@ public class ContextualCardLoader extends AsyncLoaderCompat<List<ContextualCard> private boolean isLargeCard(ContextualCard card) { return card.getSliceUri().equals(CONTEXTUAL_WIFI_SLICE_URI) || card.getSliceUri().equals(BLUETOOTH_DEVICES_SLICE_URI); || card.getSliceUri().equals(BLUETOOTH_DEVICES_SLICE_URI) || card.getSliceUri().equals(NOTIFICATION_CHANNEL_SLICE_URI); } public interface CardContentLoaderListener { Loading src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java +7 −0 Original line number Diff line number Diff line Loading @@ -56,11 +56,18 @@ public class SettingsContextualCardProvider extends ContextualCardProvider { .setCardName(CustomSliceRegistry.BATTERY_FIX_SLICE_URI.toString()) .setCardCategory(ContextualCard.Category.IMPORTANT) .build(); final ContextualCard notificationChannelCard = ContextualCard.newBuilder() .setSliceUri(CustomSliceRegistry.NOTIFICATION_CHANNEL_SLICE_URI.toString()) .setCardName(CustomSliceRegistry.NOTIFICATION_CHANNEL_SLICE_URI.toString()) .setCardCategory(ContextualCard.Category.POSSIBLE) .build(); final ContextualCardList cards = ContextualCardList.newBuilder() .addCard(wifiCard) .addCard(connectedDeviceCard) .addCard(lowStorageCard) .addCard(batteryFixCard) .addCard(notificationChannelCard) .build(); return cards; Loading src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java 0 → 100644 +474 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019 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.settings.homepage.contextualcards.slices; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; import static com.android.settings.notification.NotificationSettingsBase.ARG_FROM_SETTINGS; import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import androidx.core.graphics.drawable.IconCompat; import androidx.slice.Slice; import androidx.slice.builders.ListBuilder; import androidx.slice.builders.SliceAction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SubSettings; import com.android.settings.Utils; import com.android.settings.applications.AppAndNotificationDashboardFragment; import com.android.settings.applications.AppInfoBase; import com.android.settings.core.SubSettingLauncher; import com.android.settings.notification.AppNotificationSettings; import com.android.settings.notification.ChannelNotificationSettings; import com.android.settings.notification.NotificationBackend; import com.android.settings.slices.CustomSliceRegistry; import com.android.settings.slices.CustomSliceable; import com.android.settings.slices.SliceBroadcastReceiver; import com.android.settings.slices.SliceBuilderUtils; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.applications.ApplicationsState; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class NotificationChannelSlice implements CustomSliceable { /** * Recently app condition: * App was installed between 3 and 7 days ago. */ @VisibleForTesting static final long DURATION_START_DAYS = TimeUnit.DAYS.toMillis(7); @VisibleForTesting static final long DURATION_END_DAYS = TimeUnit.DAYS.toMillis(3); /** * Notification count condition: * App has sent at least ~10 notifications. */ @VisibleForTesting static final int MIN_NOTIFICATION_SENT_COUNT = 10; /** * Limit rows when the number of notification channel is more than {@link * #DEFAULT_EXPANDED_ROW_COUNT}. */ @VisibleForTesting static final int DEFAULT_EXPANDED_ROW_COUNT = 3; private static final String TAG = "NotifChannelSlice"; private static final String PACKAGE_NAME = "package_name"; private static final String PACKAGE_UID = "package_uid"; private static final String CHANNEL_ID = "channel_id"; /** * TODO(b/119831690): Change to notification count sorting. * This is the default sorting from NotificationSettingsBase, will be replaced with notification * count sorting mechanism. */ private static final Comparator<NotificationChannel> mChannelComparator = (left, right) -> { if (TextUtils.equals(left.getId(), NotificationChannel.DEFAULT_CHANNEL_ID)) { // Uncategorized/miscellaneous legacy channel goes last return 1; } else if (TextUtils.equals(right.getId(), NotificationChannel.DEFAULT_CHANNEL_ID)) { return -1; } return left.getId().compareTo(right.getId()); }; private final Context mContext; @VisibleForTesting NotificationBackend mNotificationBackend; private String mPackageName; private int mUid; public NotificationChannelSlice(Context context) { mContext = context; mNotificationBackend = new NotificationBackend(); } private static Bitmap drawableToBitmap(Drawable drawable) { if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } @Override public Slice getSlice() { final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY).setAccentColor(-1); /** * Get package which is satisfied with: * 1. Recently installed. * 2. Multiple channels. * 3. Sent at least ~10 notifications. */ // TODO(b/123065955): Review latency of NotificationChannelSlice final List<PackageInfo> multiChannelPackages = getMultiChannelPackages( getRecentlyInstalledPackages()); final PackageInfo packageInfo = getMaxSentNotificationsPackage(multiChannelPackages); // Return a header with IsError flag, if package is not found. if (packageInfo == null) { return listBuilder.setHeader(getNoSuggestedAppHeader()) .setIsError(true).build(); } // Save eligible package name and its uid, they will be used in getIntent(). mPackageName = packageInfo.packageName; mUid = getApplicationUid(mPackageName); // Add notification channel header. final IconCompat icon = getApplicationIcon(mPackageName); final CharSequence title = mContext.getString(R.string.manage_app_notification, Utils.getApplicationLabel(mContext, mPackageName)); listBuilder.addRow(new ListBuilder.RowBuilder() .setTitleItem(icon, ListBuilder.ICON_IMAGE) .setTitle(title) .setSubtitle(getSubTitle(mPackageName, mUid)) .setPrimaryAction(getPrimarySliceAction(icon, title, getIntent()))); // Get rows by notification channel. final List<ListBuilder.RowBuilder> rows = getNotificationChannelRows(packageInfo, icon); // Get displayable notification channel count. final int channelCount = Math.min(rows.size(), DEFAULT_EXPANDED_ROW_COUNT); // According to the displayable channel count to add rows. for (int i = 0; i < channelCount; i++) { listBuilder.addRow(rows.get(i)); } return listBuilder.build(); } @Override public Uri getUri() { return CustomSliceRegistry.NOTIFICATION_CHANNEL_SLICE_URI; } @Override public void onNotifyChange(Intent intent) { final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, false); final String packageName = intent.getStringExtra(PACKAGE_NAME); final int uid = intent.getIntExtra(PACKAGE_UID, -1); final String channelId = intent.getStringExtra(CHANNEL_ID); final PackageInfo packageInfo = getPackageInfo(packageName); final NotificationBackend.AppRow appRow = mNotificationBackend.loadAppRow(mContext, mContext.getPackageManager(), packageInfo); final List<NotificationChannel> notificationChannels = getEnabledChannels(packageName, uid, appRow); for (NotificationChannel channel : notificationChannels) { if (TextUtils.equals(channel.getId(), channelId)) { final int importance = newState ? IMPORTANCE_LOW : IMPORTANCE_NONE; channel.setImportance(importance); channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); mNotificationBackend.updateChannel(packageName, uid, channel); return; } } } @Override public Intent getIntent() { final Bundle args = new Bundle(); args.putString(AppInfoBase.ARG_PACKAGE_NAME, mPackageName); args.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); return new SubSettingLauncher(mContext) .setDestination(AppNotificationSettings.class.getName()) .setTitleRes(R.string.notifications_title) .setArguments(args) .setSourceMetricsCategory(MetricsProto.MetricsEvent.SLICE) .toIntent(); } @VisibleForTesting IconCompat getApplicationIcon(String packageName) { final Drawable drawable; try { drawable = mContext.getPackageManager().getApplicationIcon(packageName); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "No such package to get application icon."); return null; } return IconCompat.createWithBitmap(drawableToBitmap(drawable)); } @VisibleForTesting int getApplicationUid(String packageName) { final ApplicationsState.AppEntry appEntry = ApplicationsState.getInstance((Application) mContext.getApplicationContext()) .getEntry(packageName, UserHandle.myUserId()); return appEntry.info.uid; } private SliceAction buildRowSliceAction(NotificationChannel channel, IconCompat icon) { final Bundle channelArgs = new Bundle(); channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mPackageName); channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId()); channelArgs.putBoolean(ARG_FROM_SETTINGS, true); final Intent channelIntent = new SubSettingLauncher(mContext) .setDestination(ChannelNotificationSettings.class.getName()) .setArguments(channelArgs) .setTitleRes(R.string.notification_channel_title) .setSourceMetricsCategory(MetricsProto.MetricsEvent.SLICE) .toIntent(); return SliceAction.createDeeplink( PendingIntent.getActivity(mContext, channel.hashCode(), channelIntent, 0), icon, ListBuilder.ICON_IMAGE, channel.getName()); } private ListBuilder.HeaderBuilder getNoSuggestedAppHeader() { final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.ic_homepage_apps); final CharSequence titleNoSuggestedApp = mContext.getString(R.string.no_suggested_app); final SliceAction primarySliceActionForNoSuggestedApp = getPrimarySliceAction(icon, titleNoSuggestedApp, getAppAndNotificationPageIntent()); return new ListBuilder.HeaderBuilder() .setTitle(titleNoSuggestedApp) .setPrimaryAction(primarySliceActionForNoSuggestedApp); } private List<ListBuilder.RowBuilder> getNotificationChannelRows(PackageInfo packageInfo, IconCompat icon) { final List<ListBuilder.RowBuilder> notificationChannelRows = new ArrayList<>(); final NotificationBackend.AppRow appRow = mNotificationBackend.loadAppRow(mContext, mContext.getPackageManager(), packageInfo); final List<NotificationChannel> enabledChannels = getEnabledChannels(mPackageName, mUid, appRow); for (NotificationChannel channel : enabledChannels) { notificationChannelRows.add(new ListBuilder.RowBuilder() .setTitle(channel.getName()) .setSubtitle(NotificationBackend.getSentSummary( mContext, appRow.sentByChannel.get(channel.getId()), false)) .setPrimaryAction(buildRowSliceAction(channel, icon)) .addEndItem(SliceAction.createToggle(getToggleIntent(channel.getId()), null /* actionTitle */, channel.getImportance() != IMPORTANCE_NONE))); } return notificationChannelRows; } private PendingIntent getToggleIntent(String channelId) { // Send broadcast to enable/disable channel. final Intent intent = new Intent(getUri().toString()) .setClass(mContext, SliceBroadcastReceiver.class) .putExtra(PACKAGE_NAME, mPackageName) .putExtra(PACKAGE_UID, mUid) .putExtra(CHANNEL_ID, channelId); return PendingIntent.getBroadcast(mContext, intent.hashCode(), intent, 0); } private List<PackageInfo> getMultiChannelPackages(List<PackageInfo> packageInfoList) { final List<PackageInfo> multiChannelPackages = new ArrayList<>(); if (packageInfoList.isEmpty()) { return multiChannelPackages; } for (PackageInfo packageInfo : packageInfoList) { final int channelCount = mNotificationBackend.getChannelCount(packageInfo.packageName, getApplicationUid(packageInfo.packageName)); if (channelCount > 1) { multiChannelPackages.add(packageInfo); } } // TODO(b/119831690): Filter the packages which doesn't have any configurable channel. return multiChannelPackages; } private List<PackageInfo> getRecentlyInstalledPackages() { final long startTime = System.currentTimeMillis() - DURATION_START_DAYS; final long endTime = System.currentTimeMillis() - DURATION_END_DAYS; // Get recently installed packages between 3 and 7 days ago. final List<PackageInfo> recentlyInstalledPackages = new ArrayList<>(); final List<PackageInfo> installedPackages = mContext.getPackageManager().getInstalledPackages(0); for (PackageInfo packageInfo : installedPackages) { // Not include system app. if (packageInfo.applicationInfo.isSystemApp()) { continue; } if (packageInfo.firstInstallTime >= startTime && packageInfo.firstInstallTime <= endTime) { recentlyInstalledPackages.add(packageInfo); } } return recentlyInstalledPackages; } private SliceAction getPrimarySliceAction(IconCompat icon, CharSequence title, Intent intent) { return SliceAction.createDeeplink( PendingIntent.getActivity(mContext, intent.hashCode(), intent, 0), icon, ListBuilder.ICON_IMAGE, title); } private List<NotificationChannel> getEnabledChannels(String packageName, int uid, NotificationBackend.AppRow appRow) { final List<NotificationChannelGroup> channelGroupList = mNotificationBackend.getGroups(packageName, uid).getList(); final List<NotificationChannel> channels = channelGroupList.stream() .flatMap(group -> group.getChannels().stream().filter( channel -> isChannelEnabled(group, channel, appRow))) .collect(Collectors.toList()); // TODO(b/119831690): Sort the channels by notification count. Collections.sort(channels, mChannelComparator); return channels; } private PackageInfo getMaxSentNotificationsPackage(List<PackageInfo> packageInfoList) { if (packageInfoList.isEmpty()) { return null; } // Get the package which has sent at least ~10 notifications and not turn off channels. int maxSentCount = 0; PackageInfo maxSentCountPackage = null; for (PackageInfo packageInfo : packageInfoList) { final NotificationBackend.AppRow appRow = mNotificationBackend.loadAppRow(mContext, mContext.getPackageManager(), packageInfo); // Get sent notification count from app. final int sentCount = appRow.sentByApp.sentCount; if (!appRow.banned && sentCount >= MIN_NOTIFICATION_SENT_COUNT && sentCount > maxSentCount) { maxSentCount = sentCount; maxSentCountPackage = packageInfo; } } return maxSentCountPackage; } private CharSequence getSubTitle(String packageName, int uid) { final int channelCount = mNotificationBackend.getChannelCount(packageName, uid); return mContext.getResources().getQuantityString( R.plurals.notification_channel_count_summary, channelCount, channelCount); } private Intent getAppAndNotificationPageIntent() { final String screenTitle = mContext.getText(R.string.app_and_notification_dashboard_title) .toString(); return SliceBuilderUtils.buildSearchResultPageIntent(mContext, AppAndNotificationDashboardFragment.class.getName(), "" /* key */, screenTitle, MetricsProto.MetricsEvent.SLICE) .setClassName(mContext.getPackageName(), SubSettings.class.getName()) .setData(getUri()); } private PackageInfo getPackageInfo(String packageName) { try { return mContext.getPackageManager().getPackageInfo(packageName, 0); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "No such package to get package info."); return null; } } private boolean isChannelEnabled(NotificationChannelGroup group, NotificationChannel channel, NotificationBackend.AppRow appRow) { final RestrictedLockUtils.EnforcedAdmin suspendedAppsAdmin = RestrictedLockUtilsInternal.checkIfApplicationIsSuspended(mContext, mPackageName, mUid); return suspendedAppsAdmin == null && isChannelBlockable(channel, appRow) && isChannelConfigurable(channel, appRow) && !group.isBlocked(); } private boolean isChannelConfigurable(NotificationChannel channel, NotificationBackend.AppRow appRow) { if (channel != null && appRow != null) { return !TextUtils.equals(channel.getId(), appRow.lockedChannelId); } return false; } private boolean isChannelBlockable(NotificationChannel channel, NotificationBackend.AppRow appRow) { if (channel != null && appRow != null) { if (!appRow.systemApp) { return true; } return channel.isBlockableSystem() || channel.getImportance() == IMPORTANCE_NONE; } return false; } } No newline at end of file src/com/android/settings/notification/NotificationSettingsBase.java +1 −1 Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ import java.util.List; abstract public class NotificationSettingsBase extends DashboardFragment { private static final String TAG = "NotifiSettingsBase"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); protected static final String ARG_FROM_SETTINGS = "fromSettings"; public static final String ARG_FROM_SETTINGS = "fromSettings"; protected PackageManager mPm; protected NotificationBackend mBackend = new NotificationBackend(); Loading Loading
res/values/strings.xml +10 −0 Original line number Diff line number Diff line Loading @@ -10524,4 +10524,14 @@ <!-- Text for permission bar chart details in Privacy page. [CHAR LIMIT=NONE] --> <string name="permission_bar_chart_details">See all usage</string> <!-- Title for notification channel slice. [CHAR LIMIT=NONE] --> <string name="manage_app_notification">Manage <xliff:g id="app_name" example="Settings">%1$s</xliff:g> Notifications</string> <!-- Title for no suggested app in notification channel slice. [CHAR LIMIT=NONE] --> <string name="no_suggested_app">No suggested application</string> <!-- Summary for notification channel slice. [CHAR LIMIT=NONE] --> <plurals name="notification_channel_count_summary"> <item quantity="one"><xliff:g id="notification_channel_count">%1$d</xliff:g> notification channel. Tap to manage all.</item> <item quantity="other"><xliff:g id="notification_channel_count">%1$d</xliff:g> notification channels. Tap to manage all.</item> </plurals> </resources>
src/com/android/settings/homepage/contextualcards/ContextualCardLoader.java +3 −1 Original line number Diff line number Diff line Loading @@ -22,6 +22,7 @@ import static androidx.slice.widget.SliceLiveData.SUPPORTED_SPECS; import static com.android.settings.slices.CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI; import static com.android.settings.slices.CustomSliceRegistry.CONTEXTUAL_WIFI_SLICE_URI; import static com.android.settings.slices.CustomSliceRegistry.NOTIFICATION_CHANNEL_SLICE_URI; import android.content.ContentProviderClient; import android.content.ContentResolver; Loading Loading @@ -205,7 +206,8 @@ public class ContextualCardLoader extends AsyncLoaderCompat<List<ContextualCard> private boolean isLargeCard(ContextualCard card) { return card.getSliceUri().equals(CONTEXTUAL_WIFI_SLICE_URI) || card.getSliceUri().equals(BLUETOOTH_DEVICES_SLICE_URI); || card.getSliceUri().equals(BLUETOOTH_DEVICES_SLICE_URI) || card.getSliceUri().equals(NOTIFICATION_CHANNEL_SLICE_URI); } public interface CardContentLoaderListener { Loading
src/com/android/settings/homepage/contextualcards/SettingsContextualCardProvider.java +7 −0 Original line number Diff line number Diff line Loading @@ -56,11 +56,18 @@ public class SettingsContextualCardProvider extends ContextualCardProvider { .setCardName(CustomSliceRegistry.BATTERY_FIX_SLICE_URI.toString()) .setCardCategory(ContextualCard.Category.IMPORTANT) .build(); final ContextualCard notificationChannelCard = ContextualCard.newBuilder() .setSliceUri(CustomSliceRegistry.NOTIFICATION_CHANNEL_SLICE_URI.toString()) .setCardName(CustomSliceRegistry.NOTIFICATION_CHANNEL_SLICE_URI.toString()) .setCardCategory(ContextualCard.Category.POSSIBLE) .build(); final ContextualCardList cards = ContextualCardList.newBuilder() .addCard(wifiCard) .addCard(connectedDeviceCard) .addCard(lowStorageCard) .addCard(batteryFixCard) .addCard(notificationChannelCard) .build(); return cards; Loading
src/com/android/settings/homepage/contextualcards/slices/NotificationChannelSlice.java 0 → 100644 +474 −0 Original line number Diff line number Diff line /* * Copyright (C) 2019 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.settings.homepage.contextualcards.slices; import static android.app.NotificationManager.IMPORTANCE_LOW; import static android.app.NotificationManager.IMPORTANCE_NONE; import static android.app.slice.Slice.EXTRA_TOGGLE_STATE; import static com.android.settings.notification.NotificationSettingsBase.ARG_FROM_SETTINGS; import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import androidx.core.graphics.drawable.IconCompat; import androidx.slice.Slice; import androidx.slice.builders.ListBuilder; import androidx.slice.builders.SliceAction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SubSettings; import com.android.settings.Utils; import com.android.settings.applications.AppAndNotificationDashboardFragment; import com.android.settings.applications.AppInfoBase; import com.android.settings.core.SubSettingLauncher; import com.android.settings.notification.AppNotificationSettings; import com.android.settings.notification.ChannelNotificationSettings; import com.android.settings.notification.NotificationBackend; import com.android.settings.slices.CustomSliceRegistry; import com.android.settings.slices.CustomSliceable; import com.android.settings.slices.SliceBroadcastReceiver; import com.android.settings.slices.SliceBuilderUtils; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.applications.ApplicationsState; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class NotificationChannelSlice implements CustomSliceable { /** * Recently app condition: * App was installed between 3 and 7 days ago. */ @VisibleForTesting static final long DURATION_START_DAYS = TimeUnit.DAYS.toMillis(7); @VisibleForTesting static final long DURATION_END_DAYS = TimeUnit.DAYS.toMillis(3); /** * Notification count condition: * App has sent at least ~10 notifications. */ @VisibleForTesting static final int MIN_NOTIFICATION_SENT_COUNT = 10; /** * Limit rows when the number of notification channel is more than {@link * #DEFAULT_EXPANDED_ROW_COUNT}. */ @VisibleForTesting static final int DEFAULT_EXPANDED_ROW_COUNT = 3; private static final String TAG = "NotifChannelSlice"; private static final String PACKAGE_NAME = "package_name"; private static final String PACKAGE_UID = "package_uid"; private static final String CHANNEL_ID = "channel_id"; /** * TODO(b/119831690): Change to notification count sorting. * This is the default sorting from NotificationSettingsBase, will be replaced with notification * count sorting mechanism. */ private static final Comparator<NotificationChannel> mChannelComparator = (left, right) -> { if (TextUtils.equals(left.getId(), NotificationChannel.DEFAULT_CHANNEL_ID)) { // Uncategorized/miscellaneous legacy channel goes last return 1; } else if (TextUtils.equals(right.getId(), NotificationChannel.DEFAULT_CHANNEL_ID)) { return -1; } return left.getId().compareTo(right.getId()); }; private final Context mContext; @VisibleForTesting NotificationBackend mNotificationBackend; private String mPackageName; private int mUid; public NotificationChannelSlice(Context context) { mContext = context; mNotificationBackend = new NotificationBackend(); } private static Bitmap drawableToBitmap(Drawable drawable) { if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } @Override public Slice getSlice() { final ListBuilder listBuilder = new ListBuilder(mContext, getUri(), ListBuilder.INFINITY).setAccentColor(-1); /** * Get package which is satisfied with: * 1. Recently installed. * 2. Multiple channels. * 3. Sent at least ~10 notifications. */ // TODO(b/123065955): Review latency of NotificationChannelSlice final List<PackageInfo> multiChannelPackages = getMultiChannelPackages( getRecentlyInstalledPackages()); final PackageInfo packageInfo = getMaxSentNotificationsPackage(multiChannelPackages); // Return a header with IsError flag, if package is not found. if (packageInfo == null) { return listBuilder.setHeader(getNoSuggestedAppHeader()) .setIsError(true).build(); } // Save eligible package name and its uid, they will be used in getIntent(). mPackageName = packageInfo.packageName; mUid = getApplicationUid(mPackageName); // Add notification channel header. final IconCompat icon = getApplicationIcon(mPackageName); final CharSequence title = mContext.getString(R.string.manage_app_notification, Utils.getApplicationLabel(mContext, mPackageName)); listBuilder.addRow(new ListBuilder.RowBuilder() .setTitleItem(icon, ListBuilder.ICON_IMAGE) .setTitle(title) .setSubtitle(getSubTitle(mPackageName, mUid)) .setPrimaryAction(getPrimarySliceAction(icon, title, getIntent()))); // Get rows by notification channel. final List<ListBuilder.RowBuilder> rows = getNotificationChannelRows(packageInfo, icon); // Get displayable notification channel count. final int channelCount = Math.min(rows.size(), DEFAULT_EXPANDED_ROW_COUNT); // According to the displayable channel count to add rows. for (int i = 0; i < channelCount; i++) { listBuilder.addRow(rows.get(i)); } return listBuilder.build(); } @Override public Uri getUri() { return CustomSliceRegistry.NOTIFICATION_CHANNEL_SLICE_URI; } @Override public void onNotifyChange(Intent intent) { final boolean newState = intent.getBooleanExtra(EXTRA_TOGGLE_STATE, false); final String packageName = intent.getStringExtra(PACKAGE_NAME); final int uid = intent.getIntExtra(PACKAGE_UID, -1); final String channelId = intent.getStringExtra(CHANNEL_ID); final PackageInfo packageInfo = getPackageInfo(packageName); final NotificationBackend.AppRow appRow = mNotificationBackend.loadAppRow(mContext, mContext.getPackageManager(), packageInfo); final List<NotificationChannel> notificationChannels = getEnabledChannels(packageName, uid, appRow); for (NotificationChannel channel : notificationChannels) { if (TextUtils.equals(channel.getId(), channelId)) { final int importance = newState ? IMPORTANCE_LOW : IMPORTANCE_NONE; channel.setImportance(importance); channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); mNotificationBackend.updateChannel(packageName, uid, channel); return; } } } @Override public Intent getIntent() { final Bundle args = new Bundle(); args.putString(AppInfoBase.ARG_PACKAGE_NAME, mPackageName); args.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); return new SubSettingLauncher(mContext) .setDestination(AppNotificationSettings.class.getName()) .setTitleRes(R.string.notifications_title) .setArguments(args) .setSourceMetricsCategory(MetricsProto.MetricsEvent.SLICE) .toIntent(); } @VisibleForTesting IconCompat getApplicationIcon(String packageName) { final Drawable drawable; try { drawable = mContext.getPackageManager().getApplicationIcon(packageName); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "No such package to get application icon."); return null; } return IconCompat.createWithBitmap(drawableToBitmap(drawable)); } @VisibleForTesting int getApplicationUid(String packageName) { final ApplicationsState.AppEntry appEntry = ApplicationsState.getInstance((Application) mContext.getApplicationContext()) .getEntry(packageName, UserHandle.myUserId()); return appEntry.info.uid; } private SliceAction buildRowSliceAction(NotificationChannel channel, IconCompat icon) { final Bundle channelArgs = new Bundle(); channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mPackageName); channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId()); channelArgs.putBoolean(ARG_FROM_SETTINGS, true); final Intent channelIntent = new SubSettingLauncher(mContext) .setDestination(ChannelNotificationSettings.class.getName()) .setArguments(channelArgs) .setTitleRes(R.string.notification_channel_title) .setSourceMetricsCategory(MetricsProto.MetricsEvent.SLICE) .toIntent(); return SliceAction.createDeeplink( PendingIntent.getActivity(mContext, channel.hashCode(), channelIntent, 0), icon, ListBuilder.ICON_IMAGE, channel.getName()); } private ListBuilder.HeaderBuilder getNoSuggestedAppHeader() { final IconCompat icon = IconCompat.createWithResource(mContext, R.drawable.ic_homepage_apps); final CharSequence titleNoSuggestedApp = mContext.getString(R.string.no_suggested_app); final SliceAction primarySliceActionForNoSuggestedApp = getPrimarySliceAction(icon, titleNoSuggestedApp, getAppAndNotificationPageIntent()); return new ListBuilder.HeaderBuilder() .setTitle(titleNoSuggestedApp) .setPrimaryAction(primarySliceActionForNoSuggestedApp); } private List<ListBuilder.RowBuilder> getNotificationChannelRows(PackageInfo packageInfo, IconCompat icon) { final List<ListBuilder.RowBuilder> notificationChannelRows = new ArrayList<>(); final NotificationBackend.AppRow appRow = mNotificationBackend.loadAppRow(mContext, mContext.getPackageManager(), packageInfo); final List<NotificationChannel> enabledChannels = getEnabledChannels(mPackageName, mUid, appRow); for (NotificationChannel channel : enabledChannels) { notificationChannelRows.add(new ListBuilder.RowBuilder() .setTitle(channel.getName()) .setSubtitle(NotificationBackend.getSentSummary( mContext, appRow.sentByChannel.get(channel.getId()), false)) .setPrimaryAction(buildRowSliceAction(channel, icon)) .addEndItem(SliceAction.createToggle(getToggleIntent(channel.getId()), null /* actionTitle */, channel.getImportance() != IMPORTANCE_NONE))); } return notificationChannelRows; } private PendingIntent getToggleIntent(String channelId) { // Send broadcast to enable/disable channel. final Intent intent = new Intent(getUri().toString()) .setClass(mContext, SliceBroadcastReceiver.class) .putExtra(PACKAGE_NAME, mPackageName) .putExtra(PACKAGE_UID, mUid) .putExtra(CHANNEL_ID, channelId); return PendingIntent.getBroadcast(mContext, intent.hashCode(), intent, 0); } private List<PackageInfo> getMultiChannelPackages(List<PackageInfo> packageInfoList) { final List<PackageInfo> multiChannelPackages = new ArrayList<>(); if (packageInfoList.isEmpty()) { return multiChannelPackages; } for (PackageInfo packageInfo : packageInfoList) { final int channelCount = mNotificationBackend.getChannelCount(packageInfo.packageName, getApplicationUid(packageInfo.packageName)); if (channelCount > 1) { multiChannelPackages.add(packageInfo); } } // TODO(b/119831690): Filter the packages which doesn't have any configurable channel. return multiChannelPackages; } private List<PackageInfo> getRecentlyInstalledPackages() { final long startTime = System.currentTimeMillis() - DURATION_START_DAYS; final long endTime = System.currentTimeMillis() - DURATION_END_DAYS; // Get recently installed packages between 3 and 7 days ago. final List<PackageInfo> recentlyInstalledPackages = new ArrayList<>(); final List<PackageInfo> installedPackages = mContext.getPackageManager().getInstalledPackages(0); for (PackageInfo packageInfo : installedPackages) { // Not include system app. if (packageInfo.applicationInfo.isSystemApp()) { continue; } if (packageInfo.firstInstallTime >= startTime && packageInfo.firstInstallTime <= endTime) { recentlyInstalledPackages.add(packageInfo); } } return recentlyInstalledPackages; } private SliceAction getPrimarySliceAction(IconCompat icon, CharSequence title, Intent intent) { return SliceAction.createDeeplink( PendingIntent.getActivity(mContext, intent.hashCode(), intent, 0), icon, ListBuilder.ICON_IMAGE, title); } private List<NotificationChannel> getEnabledChannels(String packageName, int uid, NotificationBackend.AppRow appRow) { final List<NotificationChannelGroup> channelGroupList = mNotificationBackend.getGroups(packageName, uid).getList(); final List<NotificationChannel> channels = channelGroupList.stream() .flatMap(group -> group.getChannels().stream().filter( channel -> isChannelEnabled(group, channel, appRow))) .collect(Collectors.toList()); // TODO(b/119831690): Sort the channels by notification count. Collections.sort(channels, mChannelComparator); return channels; } private PackageInfo getMaxSentNotificationsPackage(List<PackageInfo> packageInfoList) { if (packageInfoList.isEmpty()) { return null; } // Get the package which has sent at least ~10 notifications and not turn off channels. int maxSentCount = 0; PackageInfo maxSentCountPackage = null; for (PackageInfo packageInfo : packageInfoList) { final NotificationBackend.AppRow appRow = mNotificationBackend.loadAppRow(mContext, mContext.getPackageManager(), packageInfo); // Get sent notification count from app. final int sentCount = appRow.sentByApp.sentCount; if (!appRow.banned && sentCount >= MIN_NOTIFICATION_SENT_COUNT && sentCount > maxSentCount) { maxSentCount = sentCount; maxSentCountPackage = packageInfo; } } return maxSentCountPackage; } private CharSequence getSubTitle(String packageName, int uid) { final int channelCount = mNotificationBackend.getChannelCount(packageName, uid); return mContext.getResources().getQuantityString( R.plurals.notification_channel_count_summary, channelCount, channelCount); } private Intent getAppAndNotificationPageIntent() { final String screenTitle = mContext.getText(R.string.app_and_notification_dashboard_title) .toString(); return SliceBuilderUtils.buildSearchResultPageIntent(mContext, AppAndNotificationDashboardFragment.class.getName(), "" /* key */, screenTitle, MetricsProto.MetricsEvent.SLICE) .setClassName(mContext.getPackageName(), SubSettings.class.getName()) .setData(getUri()); } private PackageInfo getPackageInfo(String packageName) { try { return mContext.getPackageManager().getPackageInfo(packageName, 0); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "No such package to get package info."); return null; } } private boolean isChannelEnabled(NotificationChannelGroup group, NotificationChannel channel, NotificationBackend.AppRow appRow) { final RestrictedLockUtils.EnforcedAdmin suspendedAppsAdmin = RestrictedLockUtilsInternal.checkIfApplicationIsSuspended(mContext, mPackageName, mUid); return suspendedAppsAdmin == null && isChannelBlockable(channel, appRow) && isChannelConfigurable(channel, appRow) && !group.isBlocked(); } private boolean isChannelConfigurable(NotificationChannel channel, NotificationBackend.AppRow appRow) { if (channel != null && appRow != null) { return !TextUtils.equals(channel.getId(), appRow.lockedChannelId); } return false; } private boolean isChannelBlockable(NotificationChannel channel, NotificationBackend.AppRow appRow) { if (channel != null && appRow != null) { if (!appRow.systemApp) { return true; } return channel.isBlockableSystem() || channel.getImportance() == IMPORTANCE_NONE; } return false; } } No newline at end of file
src/com/android/settings/notification/NotificationSettingsBase.java +1 −1 Original line number Diff line number Diff line Loading @@ -59,7 +59,7 @@ import java.util.List; abstract public class NotificationSettingsBase extends DashboardFragment { private static final String TAG = "NotifiSettingsBase"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); protected static final String ARG_FROM_SETTINGS = "fromSettings"; public static final String ARG_FROM_SETTINGS = "fromSettings"; protected PackageManager mPm; protected NotificationBackend mBackend = new NotificationBackend(); Loading