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