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