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