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