1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.systemui.statusbar.notification.row; 18 19 import static android.service.notification.NotificationListenerService.Ranking 20 .USER_SENTIMENT_NEGATIVE; 21 22 import android.content.Context; 23 import android.metrics.LogMaker; 24 import android.util.Log; 25 26 import androidx.annotation.VisibleForTesting; 27 28 import com.android.internal.logging.MetricsLogger; 29 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 30 import com.android.systemui.Dependency; 31 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; 32 import com.android.systemui.statusbar.notification.NotificationEntryManager; 33 import com.android.systemui.statusbar.notification.logging.NotificationCounters; 34 35 import java.util.Collections; 36 import java.util.HashSet; 37 import java.util.Set; 38 39 import javax.inject.Inject; 40 import javax.inject.Singleton; 41 42 /** 43 * Manager for the notification blocking helper - tracks and helps create the blocking helper 44 * affordance. 45 */ 46 @Singleton 47 public class NotificationBlockingHelperManager { 48 /** Enables debug logging and always makes the blocking helper show up after a dismiss. */ 49 private static final boolean DEBUG = false; 50 private static final String TAG = "BlockingHelper"; 51 52 private final Context mContext; 53 /** Row that the blocking helper will be shown in (via {@link NotificationGuts}. */ 54 private ExpandableNotificationRow mBlockingHelperRow; 55 private Set<String> mNonBlockablePkgs; 56 57 /** 58 * Whether the notification shade/stack is expanded - used to determine blocking helper 59 * eligibility. 60 */ 61 private boolean mIsShadeExpanded; 62 63 private MetricsLogger mMetricsLogger = new MetricsLogger(); 64 65 @Inject NotificationBlockingHelperManager(Context context)66 public NotificationBlockingHelperManager(Context context) { 67 mContext = context; 68 mNonBlockablePkgs = new HashSet<>(); 69 Collections.addAll(mNonBlockablePkgs, mContext.getResources().getStringArray( 70 com.android.internal.R.array.config_nonBlockableNotificationPackages)); 71 } 72 73 /** 74 * Potentially shows the blocking helper, represented via the {@link NotificationInfo} menu 75 * item, in the current row if user sentiment is negative. 76 * 77 * @param row row to render the blocking helper in 78 * @param menuRow menu used to generate the {@link NotificationInfo} view that houses the 79 * blocking helper UI 80 * @return whether we're showing a blocking helper in the given notification row 81 */ perhapsShowBlockingHelper( ExpandableNotificationRow row, NotificationMenuRowPlugin menuRow)82 boolean perhapsShowBlockingHelper( 83 ExpandableNotificationRow row, NotificationMenuRowPlugin menuRow) { 84 // We only show the blocking helper if: 85 // - User sentiment is negative (DEBUG flag can bypass) 86 // - The notification shade is fully expanded (guarantees we're not touching a HUN). 87 // - The row is blockable (i.e. not non-blockable) 88 // - The dismissed row is a valid group (>1 or 0 children from the same channel) 89 // or the only child in the group 90 if ((row.getEntry().userSentiment == USER_SENTIMENT_NEGATIVE || DEBUG) 91 && mIsShadeExpanded 92 && !row.getIsNonblockable() 93 && ((!row.isChildInGroup() || row.isOnlyChildInGroup()) 94 && row.getNumUniqueChannels() <= 1)) { 95 // Dismiss any current blocking helper before continuing forward (only one can be shown 96 // at a given time). 97 dismissCurrentBlockingHelper(); 98 99 if (DEBUG) { 100 Log.d(TAG, "Manager.perhapsShowBlockingHelper: Showing new blocking helper"); 101 } 102 NotificationGutsManager manager = Dependency.get(NotificationGutsManager.class); 103 104 // Enable blocking helper on the row before moving forward so everything in the guts is 105 // correctly prepped. 106 mBlockingHelperRow = row; 107 mBlockingHelperRow.setBlockingHelperShowing(true); 108 109 // Log triggering of blocking helper by the system. This log line 110 // should be emitted before the "display" log line. 111 mMetricsLogger.write( 112 getLogMaker().setSubtype(MetricsEvent.BLOCKING_HELPER_TRIGGERED_BY_SYSTEM)); 113 114 // We don't care about the touch origin (x, y) since we're opening guts without any 115 // explicit user interaction. 116 manager.openGuts(mBlockingHelperRow, 0, 0, menuRow.getLongpressMenuItem(mContext)); 117 118 Dependency.get(MetricsLogger.class) 119 .count(NotificationCounters.BLOCKING_HELPER_SHOWN, 1); 120 return true; 121 } 122 return false; 123 } 124 125 /** 126 * Dismiss the currently showing blocking helper, if any, through a notification update. 127 * 128 * @return whether the blocking helper was dismissed 129 */ dismissCurrentBlockingHelper()130 boolean dismissCurrentBlockingHelper() { 131 if (!isBlockingHelperRowNull()) { 132 if (DEBUG) { 133 Log.d(TAG, "Manager.dismissCurrentBlockingHelper: Dismissing current helper"); 134 } 135 if (!mBlockingHelperRow.isBlockingHelperShowing()) { 136 Log.e(TAG, "Manager.dismissCurrentBlockingHelper: " 137 + "Non-null row is not showing a blocking helper"); 138 } 139 140 mBlockingHelperRow.setBlockingHelperShowing(false); 141 if (mBlockingHelperRow.isAttachedToWindow()) { 142 Dependency.get(NotificationEntryManager.class).updateNotifications(); 143 } 144 mBlockingHelperRow = null; 145 return true; 146 } 147 return false; 148 } 149 150 /** 151 * Update the expansion status of the notification shade/stack. 152 * 153 * @param expandedHeight how much the shade is expanded ({code 0} indicating it's collapsed) 154 */ setNotificationShadeExpanded(float expandedHeight)155 public void setNotificationShadeExpanded(float expandedHeight) { 156 mIsShadeExpanded = expandedHeight > 0.0f; 157 } 158 159 /** 160 * Returns whether the given package name is in the list of non-blockable packages. 161 */ isNonblockable(String packageName, String channelName)162 public boolean isNonblockable(String packageName, String channelName) { 163 return mNonBlockablePkgs.contains(packageName) 164 || mNonBlockablePkgs.contains(makeChannelKey(packageName, channelName)); 165 } 166 getLogMaker()167 private LogMaker getLogMaker() { 168 return mBlockingHelperRow.getStatusBarNotification() 169 .getLogMaker() 170 .setCategory(MetricsEvent.NOTIFICATION_BLOCKING_HELPER); 171 } 172 173 // Format must stay in sync with frameworks/base/core/res/res/values/config.xml 174 // config_nonBlockableNotificationPackages makeChannelKey(String pkg, String channel)175 private String makeChannelKey(String pkg, String channel) { 176 return pkg + ":" + channel; 177 } 178 179 @VisibleForTesting isBlockingHelperRowNull()180 boolean isBlockingHelperRowNull() { 181 return mBlockingHelperRow == null; 182 } 183 184 @VisibleForTesting setBlockingHelperRowForTest(ExpandableNotificationRow blockingHelperRowForTest)185 void setBlockingHelperRowForTest(ExpandableNotificationRow blockingHelperRowForTest) { 186 mBlockingHelperRow = blockingHelperRowForTest; 187 } 188 189 @VisibleForTesting setNonBlockablePkgs(String[] pkgsAndChannels)190 void setNonBlockablePkgs(String[] pkgsAndChannels) { 191 mNonBlockablePkgs = new HashSet<>(); 192 Collections.addAll(mNonBlockablePkgs, pkgsAndChannels); 193 } 194 } 195