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