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