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