| /* |
| * Copyright (C) 2020 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.wmshell; |
| |
| import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE; |
| import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED; |
| import static android.provider.Settings.Secure.NOTIFICATION_BUBBLES; |
| import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL; |
| import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL; |
| import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; |
| import static android.service.notification.NotificationStats.DISMISSAL_BUBBLE; |
| import static android.service.notification.NotificationStats.DISMISS_SENTIMENT_NEUTRAL; |
| |
| import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; |
| import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; |
| |
| import android.app.INotificationManager; |
| import android.app.Notification; |
| import android.app.NotificationChannel; |
| import android.app.NotificationManager; |
| import android.content.Context; |
| import android.content.pm.UserInfo; |
| import android.content.res.Configuration; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.service.notification.NotificationListenerService.RankingMap; |
| import android.service.notification.ZenModeConfig; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.util.SparseArray; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.statusbar.IStatusBarService; |
| import com.android.systemui.Dumpable; |
| import com.android.systemui.dagger.SysUISingleton; |
| import com.android.systemui.dump.DumpManager; |
| import com.android.systemui.model.SysUiState; |
| import com.android.systemui.shared.system.QuickStepContract; |
| import com.android.systemui.statusbar.NotificationLockscreenUserManager; |
| import com.android.systemui.statusbar.NotificationShadeWindowController; |
| import com.android.systemui.statusbar.notification.NotificationChannelHelper; |
| import com.android.systemui.statusbar.notification.NotificationEntryManager; |
| import com.android.systemui.statusbar.notification.collection.NotifCollection; |
| import com.android.systemui.statusbar.notification.collection.NotifPipeline; |
| import com.android.systemui.statusbar.notification.collection.NotificationEntry; |
| import com.android.systemui.statusbar.notification.collection.coordinator.BubbleCoordinator; |
| import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy; |
| import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; |
| import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats; |
| import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; |
| import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; |
| import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider; |
| import com.android.systemui.statusbar.phone.ShadeController; |
| import com.android.systemui.statusbar.phone.StatusBarWindowCallback; |
| import com.android.systemui.statusbar.policy.ConfigurationController; |
| import com.android.systemui.statusbar.policy.KeyguardStateController; |
| import com.android.systemui.statusbar.policy.ZenModeController; |
| import com.android.wm.shell.bubbles.Bubble; |
| import com.android.wm.shell.bubbles.BubbleEntry; |
| import com.android.wm.shell.bubbles.Bubbles; |
| |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.concurrent.Executor; |
| import java.util.function.Consumer; |
| import java.util.function.IntConsumer; |
| |
| /** |
| * The SysUi side bubbles manager which communicate with other SysUi components. |
| */ |
| @SysUISingleton |
| public class BubblesManager implements Dumpable { |
| |
| private static final String TAG = TAG_WITH_CLASS_NAME ? "BubblesManager" : TAG_BUBBLES; |
| |
| private final Context mContext; |
| private final Bubbles mBubbles; |
| private final NotificationShadeWindowController mNotificationShadeWindowController; |
| private final ShadeController mShadeController; |
| private final IStatusBarService mBarService; |
| private final INotificationManager mNotificationManager; |
| private final NotificationVisibilityProvider mVisibilityProvider; |
| private final NotificationInterruptStateProvider mNotificationInterruptStateProvider; |
| private final NotificationLockscreenUserManager mNotifUserManager; |
| private final NotificationGroupManagerLegacy mNotificationGroupManager; |
| private final CommonNotifCollection mCommonNotifCollection; |
| private final NotifPipeline mNotifPipeline; |
| private final Executor mSysuiMainExecutor; |
| |
| private final Bubbles.SysuiProxy mSysuiProxy; |
| // TODO (b/145659174): allow for multiple callbacks to support the "shadow" new notif pipeline |
| private final List<NotifCallback> mCallbacks = new ArrayList<>(); |
| private final StatusBarWindowCallback mStatusBarWindowCallback; |
| |
| /** |
| * Creates {@link BubblesManager}, returns {@code null} if Optional {@link Bubbles} not present |
| * which means bubbles feature not support. |
| */ |
| @Nullable |
| public static BubblesManager create(Context context, |
| Optional<Bubbles> bubblesOptional, |
| NotificationShadeWindowController notificationShadeWindowController, |
| KeyguardStateController keyguardStateController, |
| ShadeController shadeController, |
| ConfigurationController configurationController, |
| @Nullable IStatusBarService statusBarService, |
| INotificationManager notificationManager, |
| NotificationVisibilityProvider visibilityProvider, |
| NotificationInterruptStateProvider interruptionStateProvider, |
| ZenModeController zenModeController, |
| NotificationLockscreenUserManager notifUserManager, |
| NotificationGroupManagerLegacy groupManager, |
| CommonNotifCollection notifCollection, |
| NotifPipeline notifPipeline, |
| SysUiState sysUiState, |
| DumpManager dumpManager, |
| Executor sysuiMainExecutor) { |
| if (bubblesOptional.isPresent()) { |
| return new BubblesManager(context, |
| bubblesOptional.get(), |
| notificationShadeWindowController, |
| keyguardStateController, |
| shadeController, |
| configurationController, |
| statusBarService, |
| notificationManager, |
| visibilityProvider, |
| interruptionStateProvider, |
| zenModeController, |
| notifUserManager, |
| groupManager, |
| notifCollection, |
| notifPipeline, |
| sysUiState, |
| dumpManager, |
| sysuiMainExecutor); |
| } else { |
| return null; |
| } |
| } |
| |
| @VisibleForTesting |
| BubblesManager(Context context, |
| Bubbles bubbles, |
| NotificationShadeWindowController notificationShadeWindowController, |
| KeyguardStateController keyguardStateController, |
| ShadeController shadeController, |
| ConfigurationController configurationController, |
| @Nullable IStatusBarService statusBarService, |
| INotificationManager notificationManager, |
| NotificationVisibilityProvider visibilityProvider, |
| NotificationInterruptStateProvider interruptionStateProvider, |
| ZenModeController zenModeController, |
| NotificationLockscreenUserManager notifUserManager, |
| NotificationGroupManagerLegacy groupManager, |
| CommonNotifCollection notifCollection, |
| NotifPipeline notifPipeline, |
| SysUiState sysUiState, |
| DumpManager dumpManager, |
| Executor sysuiMainExecutor) { |
| mContext = context; |
| mBubbles = bubbles; |
| mNotificationShadeWindowController = notificationShadeWindowController; |
| mShadeController = shadeController; |
| mNotificationManager = notificationManager; |
| mVisibilityProvider = visibilityProvider; |
| mNotificationInterruptStateProvider = interruptionStateProvider; |
| mNotifUserManager = notifUserManager; |
| mNotificationGroupManager = groupManager; |
| mCommonNotifCollection = notifCollection; |
| mNotifPipeline = notifPipeline; |
| mSysuiMainExecutor = sysuiMainExecutor; |
| |
| mBarService = statusBarService == null |
| ? IStatusBarService.Stub.asInterface( |
| ServiceManager.getService(Context.STATUS_BAR_SERVICE)) |
| : statusBarService; |
| |
| setupNotifPipeline(); |
| |
| dumpManager.registerDumpable(TAG, this); |
| |
| keyguardStateController.addCallback(new KeyguardStateController.Callback() { |
| @Override |
| public void onKeyguardShowingChanged() { |
| boolean isUnlockedShade = !keyguardStateController.isShowing() |
| && !keyguardStateController.isOccluded(); |
| bubbles.onStatusBarStateChanged(isUnlockedShade); |
| } |
| }); |
| |
| configurationController.addCallback(new ConfigurationController.ConfigurationListener() { |
| @Override |
| public void onConfigChanged(Configuration newConfig) { |
| mBubbles.onConfigChanged(newConfig); |
| } |
| |
| @Override |
| public void onUiModeChanged() { |
| mBubbles.updateForThemeChanges(); |
| } |
| |
| @Override |
| public void onThemeChanged() { |
| mBubbles.updateForThemeChanges(); |
| } |
| }); |
| |
| zenModeController.addCallback(new ZenModeController.Callback() { |
| @Override |
| public void onZenChanged(int zen) { |
| mBubbles.onZenStateChanged(); |
| } |
| |
| @Override |
| public void onConfigChanged(ZenModeConfig config) { |
| mBubbles.onZenStateChanged(); |
| } |
| }); |
| |
| notifUserManager.addUserChangedListener( |
| new NotificationLockscreenUserManager.UserChangedListener() { |
| @Override |
| public void onUserChanged(int userId) { |
| mBubbles.onUserChanged(userId); |
| } |
| |
| @Override |
| public void onCurrentProfilesChanged(SparseArray<UserInfo> currentProfiles) { |
| mBubbles.onCurrentProfilesChanged(currentProfiles); |
| } |
| |
| @Override |
| public void onUserRemoved(int userId) { |
| mBubbles.onUserRemoved(userId); |
| } |
| |
| }); |
| |
| // Store callback in a field so it won't get GC'd |
| mStatusBarWindowCallback = |
| (keyguardShowing, keyguardOccluded, bouncerShowing, isDozing, panelExpanded) -> |
| mBubbles.onNotificationPanelExpandedChanged(panelExpanded); |
| notificationShadeWindowController.registerCallback(mStatusBarWindowCallback); |
| |
| mSysuiProxy = new Bubbles.SysuiProxy() { |
| @Override |
| public void isNotificationPanelExpand(Consumer<Boolean> callback) { |
| sysuiMainExecutor.execute(() -> { |
| callback.accept(mNotificationShadeWindowController.getPanelExpanded()); |
| }); |
| } |
| |
| @Override |
| public void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback) { |
| sysuiMainExecutor.execute(() -> { |
| final NotificationEntry entry = mCommonNotifCollection.getEntry(key); |
| callback.accept(entry == null ? null : notifToBubbleEntry(entry)); |
| }); |
| } |
| |
| @Override |
| public void getShouldRestoredEntries(ArraySet<String> savedBubbleKeys, |
| Consumer<List<BubbleEntry>> callback) { |
| sysuiMainExecutor.execute(() -> { |
| List<BubbleEntry> result = new ArrayList<>(); |
| final Collection<NotificationEntry> activeEntries = |
| mCommonNotifCollection.getAllNotifs(); |
| for (NotificationEntry entry : activeEntries) { |
| if (mNotifUserManager.isCurrentProfile(entry.getSbn().getUserId()) |
| && savedBubbleKeys.contains(entry.getKey()) |
| && mNotificationInterruptStateProvider.shouldBubbleUp(entry) |
| && entry.isBubble()) { |
| result.add(notifToBubbleEntry(entry)); |
| } |
| } |
| callback.accept(result); |
| }); |
| } |
| |
| @Override |
| public void setNotificationInterruption(String key) { |
| sysuiMainExecutor.execute(() -> { |
| final NotificationEntry entry = mCommonNotifCollection.getEntry(key); |
| if (entry != null |
| && entry.getImportance() >= NotificationManager.IMPORTANCE_HIGH) { |
| entry.setInterruption(); |
| } |
| }); |
| } |
| |
| @Override |
| public void requestNotificationShadeTopUi(boolean requestTopUi, String componentTag) { |
| sysuiMainExecutor.execute(() -> { |
| mNotificationShadeWindowController.setRequestTopUi(requestTopUi, componentTag); |
| }); |
| } |
| |
| @Override |
| public void notifyRemoveNotification(String key, int reason) { |
| sysuiMainExecutor.execute(() -> { |
| final NotificationEntry entry = mCommonNotifCollection.getEntry(key); |
| if (entry != null) { |
| for (NotifCallback cb : mCallbacks) { |
| cb.removeNotification(entry, getDismissedByUserStats(entry, true), |
| reason); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void notifyInvalidateNotifications(String reason) { |
| sysuiMainExecutor.execute(() -> { |
| for (NotifCallback cb : mCallbacks) { |
| cb.invalidateNotifications(reason); |
| } |
| }); |
| } |
| |
| @Override |
| public void notifyMaybeCancelSummary(String key) { |
| sysuiMainExecutor.execute(() -> { |
| final NotificationEntry entry = mCommonNotifCollection.getEntry(key); |
| if (entry != null) { |
| for (NotifCallback cb : mCallbacks) { |
| cb.maybeCancelSummary(entry); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void removeNotificationEntry(String key) { |
| sysuiMainExecutor.execute(() -> { |
| final NotificationEntry entry = mCommonNotifCollection.getEntry(key); |
| if (entry != null) { |
| mNotificationGroupManager.onEntryRemoved(entry); |
| } |
| }); |
| } |
| |
| @Override |
| public void updateNotificationBubbleButton(String key) { |
| sysuiMainExecutor.execute(() -> { |
| final NotificationEntry entry = mCommonNotifCollection.getEntry(key); |
| if (entry != null && entry.getRow() != null) { |
| entry.getRow().updateBubbleButton(); |
| } |
| }); |
| } |
| |
| @Override |
| public void updateNotificationSuppression(String key) { |
| sysuiMainExecutor.execute(() -> { |
| final NotificationEntry entry = mCommonNotifCollection.getEntry(key); |
| if (entry != null) { |
| mNotificationGroupManager.updateSuppression(entry); |
| } |
| }); |
| } |
| |
| @Override |
| public void onStackExpandChanged(boolean shouldExpand) { |
| sysuiMainExecutor.execute(() -> { |
| sysUiState.setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand) |
| .commitUpdate(mContext.getDisplayId()); |
| if (!shouldExpand) { |
| sysUiState.setFlag( |
| QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, |
| false).commitUpdate(mContext.getDisplayId()); |
| } |
| }); |
| } |
| |
| @Override |
| public void onManageMenuExpandChanged(boolean menuExpanded) { |
| sysuiMainExecutor.execute(() -> { |
| sysUiState.setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED, |
| menuExpanded).commitUpdate(mContext.getDisplayId()); |
| }); |
| } |
| |
| |
| @Override |
| public void onUnbubbleConversation(String key) { |
| sysuiMainExecutor.execute(() -> { |
| final NotificationEntry entry = mCommonNotifCollection.getEntry(key); |
| if (entry != null) { |
| onUserChangedBubble(entry, false /* shouldBubble */); |
| } |
| }); |
| } |
| }; |
| mBubbles.setSysuiProxy(mSysuiProxy); |
| } |
| |
| private void setupNotifPipeline() { |
| mNotifPipeline.addCollectionListener(new NotifCollectionListener() { |
| @Override |
| public void onEntryAdded(NotificationEntry entry) { |
| BubblesManager.this.onEntryAdded(entry); |
| } |
| |
| @Override |
| public void onEntryUpdated(NotificationEntry entry) { |
| BubblesManager.this.onEntryUpdated(entry); |
| } |
| |
| @Override |
| public void onEntryRemoved(NotificationEntry entry, |
| @NotifCollection.CancellationReason int reason) { |
| if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL) { |
| BubblesManager.this.onEntryRemoved(entry); |
| } |
| } |
| |
| @Override |
| public void onRankingUpdate(RankingMap rankingMap) { |
| BubblesManager.this.onRankingUpdate(rankingMap); |
| } |
| |
| @Override |
| public void onNotificationChannelModified( |
| String pkgName, |
| UserHandle user, |
| NotificationChannel channel, |
| int modificationType) { |
| BubblesManager.this.onNotificationChannelModified( |
| pkgName, |
| user, |
| channel, |
| modificationType); |
| } |
| }); |
| } |
| |
| void onEntryAdded(NotificationEntry entry) { |
| if (mNotificationInterruptStateProvider.shouldBubbleUp(entry) |
| && entry.isBubble()) { |
| mBubbles.onEntryAdded(notifToBubbleEntry(entry)); |
| } |
| } |
| |
| void onEntryUpdated(NotificationEntry entry) { |
| mBubbles.onEntryUpdated(notifToBubbleEntry(entry), |
| mNotificationInterruptStateProvider.shouldBubbleUp(entry)); |
| } |
| |
| void onEntryRemoved(NotificationEntry entry) { |
| mBubbles.onEntryRemoved(notifToBubbleEntry(entry)); |
| } |
| |
| void onRankingUpdate(RankingMap rankingMap) { |
| String[] orderedKeys = rankingMap.getOrderedKeys(); |
| HashMap<String, Pair<BubbleEntry, Boolean>> pendingOrActiveNotif = new HashMap<>(); |
| for (int i = 0; i < orderedKeys.length; i++) { |
| String key = orderedKeys[i]; |
| final NotificationEntry entry = mCommonNotifCollection.getEntry(key); |
| BubbleEntry bubbleEntry = entry != null |
| ? notifToBubbleEntry(entry) |
| : null; |
| boolean shouldBubbleUp = entry != null |
| ? mNotificationInterruptStateProvider.shouldBubbleUp(entry) |
| : false; |
| pendingOrActiveNotif.put(key, new Pair<>(bubbleEntry, shouldBubbleUp)); |
| } |
| mBubbles.onRankingUpdated(rankingMap, pendingOrActiveNotif); |
| } |
| |
| void onNotificationChannelModified( |
| String pkg, |
| UserHandle user, |
| NotificationChannel channel, |
| int modificationType) { |
| mBubbles.onNotificationChannelModified(pkg, user, channel, modificationType); |
| } |
| |
| /** |
| * Gets the DismissedByUserStats used by {@link NotificationEntryManager}. |
| * Will not be necessary when using the new notification pipeline's {@link NotifCollection}. |
| * Instead, this is taken care of by {@link BubbleCoordinator}. |
| */ |
| private DismissedByUserStats getDismissedByUserStats( |
| NotificationEntry entry, |
| boolean isVisible) { |
| return new DismissedByUserStats( |
| DISMISSAL_BUBBLE, |
| DISMISS_SENTIMENT_NEUTRAL, |
| mVisibilityProvider.obtain(entry, isVisible)); |
| } |
| |
| /** |
| * We intercept notification entries (including group summaries) dismissed by the user when |
| * there is an active bubble associated with it. We do this so that developers can still |
| * cancel it (and hence the bubbles associated with it). |
| * |
| * @return true if we want to intercept the dismissal of the entry, else false. |
| * @see Bubbles#handleDismissalInterception(BubbleEntry, List, IntConsumer, Executor) |
| */ |
| public boolean handleDismissalInterception(NotificationEntry entry) { |
| if (entry == null) { |
| return false; |
| } |
| |
| List<NotificationEntry> children = entry.getAttachedNotifChildren(); |
| List<BubbleEntry> bubbleChildren = null; |
| if (children != null) { |
| bubbleChildren = new ArrayList<>(); |
| for (int i = 0; i < children.size(); i++) { |
| bubbleChildren.add(notifToBubbleEntry(children.get(i))); |
| } |
| } |
| |
| return mBubbles.handleDismissalInterception(notifToBubbleEntry(entry), bubbleChildren, |
| // TODO : b/171847985 should re-work on notification side to make this more clear. |
| (int i) -> { |
| if (i >= 0) { |
| for (NotifCallback cb : mCallbacks) { |
| cb.removeNotification(children.get(i), |
| getDismissedByUserStats(children.get(i), true), |
| REASON_GROUP_SUMMARY_CANCELED); |
| } |
| } else { |
| mNotificationGroupManager.onEntryRemoved(entry); |
| } |
| }, mSysuiMainExecutor); |
| } |
| |
| /** |
| * Request the stack expand if needed, then select the specified Bubble as current. |
| * If no bubble exists for this entry, one is created. |
| * |
| * @param entry the notification for the bubble to be selected |
| */ |
| public void expandStackAndSelectBubble(NotificationEntry entry) { |
| mBubbles.expandStackAndSelectBubble(notifToBubbleEntry(entry)); |
| } |
| |
| /** |
| * Request the stack expand if needed, then select the specified Bubble as current. |
| * |
| * @param bubble the bubble to be selected |
| */ |
| public void expandStackAndSelectBubble(Bubble bubble) { |
| mBubbles.expandStackAndSelectBubble(bubble); |
| } |
| |
| /** |
| * @return a bubble that matches the provided shortcutId, if one exists. |
| */ |
| public Bubble getBubbleWithShortcutId(String shortcutId) { |
| return mBubbles.getBubbleWithShortcutId(shortcutId); |
| } |
| |
| /** See {@link NotifCallback}. */ |
| public void addNotifCallback(NotifCallback callback) { |
| mCallbacks.add(callback); |
| } |
| |
| /** |
| * When a notification is set as important, make it a bubble and expand the stack if |
| * it can bubble. |
| * |
| * @param entry the important notification. |
| */ |
| public void onUserSetImportantConversation(NotificationEntry entry) { |
| if (entry.getBubbleMetadata() == null) { |
| // No bubble metadata, nothing to do. |
| return; |
| } |
| try { |
| int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; |
| mBarService.onNotificationBubbleChanged(entry.getKey(), true, flags); |
| } catch (RemoteException e) { |
| Log.e(TAG, e.getMessage()); |
| } |
| mShadeController.collapsePanel(true); |
| if (entry.getRow() != null) { |
| entry.getRow().updateBubbleButton(); |
| } |
| } |
| |
| /** |
| * Called when a user has indicated that an active notification should be shown as a bubble. |
| * <p> |
| * This method will collapse the shade, create the bubble without a flyout or dot, and suppress |
| * the notification from appearing in the shade. |
| * |
| * @param entry the notification to change bubble state for. |
| * @param shouldBubble whether the notification should show as a bubble or not. |
| */ |
| public void onUserChangedBubble(@NonNull final NotificationEntry entry, boolean shouldBubble) { |
| NotificationChannel channel = entry.getChannel(); |
| final String appPkg = entry.getSbn().getPackageName(); |
| final int appUid = entry.getSbn().getUid(); |
| if (channel == null || appPkg == null) { |
| return; |
| } |
| |
| entry.setFlagBubble(shouldBubble); |
| |
| // Update the state in NotificationManagerService |
| try { |
| int flags = Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; |
| flags |= Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE; |
| mBarService.onNotificationBubbleChanged(entry.getKey(), shouldBubble, flags); |
| } catch (RemoteException e) { |
| } |
| |
| // Change the settings |
| channel = NotificationChannelHelper.createConversationChannelIfNeeded(mContext, |
| mNotificationManager, entry, channel); |
| channel.setAllowBubbles(shouldBubble); |
| try { |
| int currentPref = mNotificationManager.getBubblePreferenceForPackage(appPkg, appUid); |
| if (shouldBubble && currentPref == BUBBLE_PREFERENCE_NONE) { |
| mNotificationManager.setBubblesAllowed(appPkg, appUid, BUBBLE_PREFERENCE_SELECTED); |
| } |
| mNotificationManager.updateNotificationChannelForPackage(appPkg, appUid, channel); |
| } catch (RemoteException e) { |
| Log.e(TAG, e.getMessage()); |
| } |
| |
| if (shouldBubble) { |
| mShadeController.collapsePanel(true); |
| if (entry.getRow() != null) { |
| entry.getRow().updateBubbleButton(); |
| } |
| } |
| } |
| |
| @Override |
| public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { |
| mBubbles.dump(pw, args); |
| } |
| |
| /** Checks whether bubbles are enabled for this user, handles negative userIds. */ |
| public static boolean areBubblesEnabled(@NonNull Context context, @NonNull UserHandle user) { |
| if (user.getIdentifier() < 0) { |
| return Settings.Secure.getInt(context.getContentResolver(), |
| NOTIFICATION_BUBBLES, 0) == 1; |
| } else { |
| return Settings.Secure.getIntForUser(context.getContentResolver(), |
| NOTIFICATION_BUBBLES, 0, user.getIdentifier()) == 1; |
| } |
| } |
| |
| static BubbleEntry notifToBubbleEntry(NotificationEntry e) { |
| return new BubbleEntry(e.getSbn(), e.getRanking(), e.isDismissable(), |
| e.shouldSuppressNotificationDot(), e.shouldSuppressNotificationList(), |
| e.shouldSuppressPeek()); |
| } |
| |
| /** |
| * Callback for when the BubbleController wants to interact with the notification pipeline to: |
| * - Remove a previously bubbled notification |
| * - Update the notification shade since bubbled notification should/shouldn't be showing |
| */ |
| public interface NotifCallback { |
| /** |
| * Called when a bubbled notification that was hidden from the shade is now being removed |
| * This can happen when an app cancels a bubbled notification or when the user dismisses a |
| * bubble. |
| */ |
| void removeNotification(@NonNull NotificationEntry entry, |
| @NonNull DismissedByUserStats stats, int reason); |
| |
| /** |
| * Called when a bubbled notification has changed whether it should be |
| * filtered from the shade. |
| */ |
| void invalidateNotifications(@NonNull String reason); |
| |
| /** |
| * Called on a bubbled entry that has been removed when there are no longer |
| * bubbled entries in its group. |
| * |
| * Checks whether its group has any other (non-bubbled) children. If it doesn't, |
| * removes all remnants of the group's summary from the notification pipeline. |
| * TODO: (b/145659174) Only old pipeline needs this - delete post-migration. |
| */ |
| void maybeCancelSummary(@NonNull NotificationEntry entry); |
| } |
| } |