• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
2 * Copyright (C) 2019 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 package com.android.server.notification;
17 
18 import static android.app.Notification.FLAG_BUBBLE;
19 import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
20 import static android.app.NotificationChannel.ALLOW_BUBBLE_OFF;
21 import static android.app.NotificationManager.BUBBLE_PREFERENCE_ALL;
22 import static android.app.NotificationManager.BUBBLE_PREFERENCE_NONE;
23 import static android.app.NotificationManager.BUBBLE_PREFERENCE_SELECTED;
24 
25 import static com.android.internal.util.FrameworkStatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING;
26 import static com.android.internal.util.FrameworkStatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE;
27 
28 import android.app.ActivityManager;
29 import android.app.Notification;
30 import android.app.NotificationChannel;
31 import android.app.PendingIntent;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.pm.ActivityInfo;
35 import android.content.res.Resources;
36 import android.util.Slog;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.util.FrameworkStatsLog;
40 
41 /**
42  * Determines whether a bubble can be shown for this notification.
43  */
44 public class BubbleExtractor implements NotificationSignalExtractor {
45     private static final String TAG = "BubbleExtractor";
46     private static final boolean DBG = false;
47 
48     private ShortcutHelper mShortcutHelper;
49     private RankingConfig mConfig;
50     private ActivityManager mActivityManager;
51     private Context mContext;
52 
53     boolean mSupportsBubble;
54 
initialize(Context context, NotificationUsageStats usageStats)55     public void initialize(Context context, NotificationUsageStats usageStats) {
56         if (DBG) Slog.d(TAG, "Initializing  " + getClass().getSimpleName() + ".");
57         mContext = context;
58         mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
59 
60         mSupportsBubble = Resources.getSystem().getBoolean(
61                 com.android.internal.R.bool.config_supportsBubble);
62     }
63 
process(NotificationRecord record)64     public RankingReconsideration process(NotificationRecord record) {
65         if (record == null || record.getNotification() == null) {
66             if (DBG) Slog.d(TAG, "skipping empty notification");
67             return null;
68         }
69 
70         if (mConfig == null) {
71             if (DBG) Slog.d(TAG, "missing config");
72             return null;
73         }
74 
75         if (mShortcutHelper == null) {
76             if (DBG) Slog.d(TAG, "missing shortcut helper");
77             return null;
78         }
79 
80         boolean notifCanPresentAsBubble = canPresentAsBubble(record)
81                 && !mActivityManager.isLowRamDevice()
82                 && record.isConversation()
83                 && record.getShortcutInfo() != null
84                 && (record.getNotification().flags & FLAG_FOREGROUND_SERVICE) == 0;
85 
86         boolean userEnabledBubbles = mConfig.bubblesEnabled(record.getUser());
87         int appPreference =
88                 mConfig.getBubblePreference(
89                         record.getSbn().getPackageName(), record.getSbn().getUid());
90         NotificationChannel recordChannel = record.getChannel();
91         if (!userEnabledBubbles
92                 || appPreference == BUBBLE_PREFERENCE_NONE
93                 || !notifCanPresentAsBubble) {
94             record.setAllowBubble(false);
95             if (!notifCanPresentAsBubble) {
96                 // clear out bubble metadata since it can't be used
97                 record.getNotification().setBubbleMetadata(null);
98             }
99         } else if (recordChannel == null) {
100             // the app is allowed but there's no channel to check
101             record.setAllowBubble(true);
102         } else if (appPreference == BUBBLE_PREFERENCE_ALL) {
103             record.setAllowBubble(recordChannel.getAllowBubbles() != ALLOW_BUBBLE_OFF);
104         } else if (appPreference == BUBBLE_PREFERENCE_SELECTED) {
105             record.setAllowBubble(recordChannel.canBubble());
106         }
107         if (DBG) {
108             Slog.d(TAG, "record: " + record.getKey()
109                     + " appPref: " + appPreference
110                     + " canBubble: " + record.canBubble()
111                     + " canPresentAsBubble: " + notifCanPresentAsBubble
112                     + " flagRemoved: " + record.isFlagBubbleRemoved());
113         }
114 
115         final boolean applyFlag = record.canBubble() && !record.isFlagBubbleRemoved();
116         if (applyFlag) {
117             record.getNotification().flags |= FLAG_BUBBLE;
118         } else {
119             record.getNotification().flags &= ~FLAG_BUBBLE;
120         }
121         return null;
122     }
123 
124     @Override
setConfig(RankingConfig config)125     public void setConfig(RankingConfig config) {
126         mConfig = config;
127     }
128 
129     @Override
setZenHelper(ZenModeHelper helper)130     public void setZenHelper(ZenModeHelper helper) {
131     }
132 
setShortcutHelper(ShortcutHelper helper)133     public void setShortcutHelper(ShortcutHelper helper) {
134         mShortcutHelper = helper;
135     }
136 
137     @VisibleForTesting
setActivityManager(ActivityManager manager)138     public void setActivityManager(ActivityManager manager) {
139         mActivityManager = manager;
140     }
141 
142     /**
143      * @return whether there is valid information for the notification to bubble.
144      */
145     @VisibleForTesting
canPresentAsBubble(NotificationRecord r)146     boolean canPresentAsBubble(NotificationRecord r) {
147         if (!mSupportsBubble) {
148             return false;
149         }
150 
151         Notification notification = r.getNotification();
152         Notification.BubbleMetadata metadata = notification.getBubbleMetadata();
153         String pkg = r.getSbn().getPackageName();
154         if (metadata == null) {
155             return false;
156         }
157 
158         String shortcutId = metadata.getShortcutId();
159         String notificationShortcutId = r.getShortcutInfo() != null
160                 ? r.getShortcutInfo().getId()
161                 : null;
162         boolean shortcutValid = false;
163         if (notificationShortcutId != null && shortcutId != null) {
164             // NoMan already checks validity of shortcut, just check if they match.
165             shortcutValid = shortcutId.equals(notificationShortcutId);
166         } else if (shortcutId != null) {
167             shortcutValid =
168                     mShortcutHelper.getValidShortcutInfo(shortcutId, pkg, r.getUser()) != null;
169         }
170         if (metadata.getIntent() == null && !shortcutValid) {
171             // Should have a shortcut if intent is null
172             logBubbleError(r.getKey(),
173                     "couldn't find valid shortcut for bubble with shortcutId: " + shortcutId);
174             return false;
175         }
176         if (shortcutValid) {
177             // TODO: check the shortcut intent / ensure it can show in activity view
178             return true;
179         }
180         return canLaunchInTaskView(mContext, metadata.getIntent(), pkg);
181     }
182 
183     /**
184      * Whether an intent is properly configured to display in an {@link
185      * com.android.wm.shell.TaskView} for bubbling.
186      *
187      * @param context       the context to use.
188      * @param pendingIntent the pending intent of the bubble.
189      * @param packageName   the notification package name for this bubble.
190      */
191     // Keep checks in sync with BubbleController#canLaunchInTaskView.
192     @VisibleForTesting
canLaunchInTaskView(Context context, PendingIntent pendingIntent, String packageName)193     protected boolean canLaunchInTaskView(Context context, PendingIntent pendingIntent,
194             String packageName) {
195         if (pendingIntent == null) {
196             Slog.w(TAG, "Unable to create bubble -- no intent");
197             return false;
198         }
199 
200         Intent intent = pendingIntent.getIntent();
201         ActivityInfo info = intent != null
202                 ? intent.resolveActivityInfo(context.getPackageManager(), 0)
203                 : null;
204         if (info == null) {
205             FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED,
206                     packageName,
207                     BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_MISSING);
208             Slog.w(TAG, "Unable to send as bubble -- couldn't find activity info for intent: "
209                     + intent);
210             return false;
211         }
212         if (!ActivityInfo.isResizeableMode(info.resizeMode)) {
213             FrameworkStatsLog.write(FrameworkStatsLog.BUBBLE_DEVELOPER_ERROR_REPORTED,
214                     packageName,
215                     BUBBLE_DEVELOPER_ERROR_REPORTED__ERROR__ACTIVITY_INFO_NOT_RESIZABLE);
216             Slog.w(TAG, "Unable to send as bubble -- activity is not resizable for intent: "
217                     + intent);
218             return false;
219         }
220         return true;
221     }
222 
logBubbleError(String key, String failureMessage)223     private void logBubbleError(String key, String failureMessage) {
224         if (DBG) {
225             Slog.w(TAG, "Bubble notification: " + key + " failed: " + failureMessage);
226         }
227     }
228 }
229