1 /* 2 * Copyright (C) 2015 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.settings.notification; 17 18 import static android.app.NotificationManager.IMPORTANCE_NONE; 19 import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; 20 21 import android.app.INotificationManager; 22 import android.app.NotificationChannel; 23 import android.app.NotificationChannelGroup; 24 import android.app.role.RoleManager; 25 import android.app.usage.IUsageStatsManager; 26 import android.app.usage.UsageEvents; 27 import android.content.ComponentName; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.ApplicationInfo; 31 import android.content.pm.PackageInfo; 32 import android.content.pm.PackageManager; 33 import android.content.pm.ParceledListSlice; 34 import android.graphics.drawable.Drawable; 35 import android.os.RemoteException; 36 import android.os.ServiceManager; 37 import android.os.UserHandle; 38 import android.service.notification.NotifyingApp; 39 import android.text.format.DateUtils; 40 import android.util.IconDrawableFactory; 41 import android.util.Log; 42 43 import androidx.annotation.VisibleForTesting; 44 45 import com.android.settingslib.R; 46 import com.android.settingslib.Utils; 47 import com.android.settingslib.utils.StringUtil; 48 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Map; 53 54 public class NotificationBackend { 55 private static final String TAG = "NotificationBackend"; 56 57 static IUsageStatsManager sUsageStatsManager = IUsageStatsManager.Stub.asInterface( 58 ServiceManager.getService(Context.USAGE_STATS_SERVICE)); 59 private static final int DAYS_TO_CHECK = 7; 60 static INotificationManager sINM = INotificationManager.Stub.asInterface( 61 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 62 loadAppRow(Context context, PackageManager pm, ApplicationInfo app)63 public AppRow loadAppRow(Context context, PackageManager pm, ApplicationInfo app) { 64 final AppRow row = new AppRow(); 65 row.pkg = app.packageName; 66 row.uid = app.uid; 67 try { 68 row.label = app.loadLabel(pm); 69 } catch (Throwable t) { 70 Log.e(TAG, "Error loading application label for " + row.pkg, t); 71 row.label = row.pkg; 72 } 73 row.icon = IconDrawableFactory.newInstance(context).getBadgedIcon(app); 74 row.banned = getNotificationsBanned(row.pkg, row.uid); 75 row.showBadge = canShowBadge(row.pkg, row.uid); 76 row.allowBubbles = canBubble(row.pkg, row.uid); 77 row.userId = UserHandle.getUserId(row.uid); 78 row.blockedChannelCount = getBlockedChannelCount(row.pkg, row.uid); 79 row.channelCount = getChannelCount(row.pkg, row.uid); 80 recordAggregatedUsageEvents(context, row); 81 return row; 82 } 83 isBlockable(Context context, ApplicationInfo info)84 public boolean isBlockable(Context context, ApplicationInfo info) { 85 final boolean blocked = getNotificationsBanned(info.packageName, info.uid); 86 final boolean systemApp = isSystemApp(context, info); 87 return !systemApp || (systemApp && blocked); 88 } 89 loadAppRow(Context context, PackageManager pm, RoleManager roleManager, PackageInfo app)90 public AppRow loadAppRow(Context context, PackageManager pm, 91 RoleManager roleManager, PackageInfo app) { 92 final AppRow row = loadAppRow(context, pm, app.applicationInfo); 93 recordCanBeBlocked(context, pm, roleManager, app, row); 94 return row; 95 } 96 recordCanBeBlocked(Context context, PackageManager pm, RoleManager rm, PackageInfo app, AppRow row)97 void recordCanBeBlocked(Context context, PackageManager pm, RoleManager rm, PackageInfo app, 98 AppRow row) { 99 row.systemApp = Utils.isSystemPackage(context.getResources(), pm, app); 100 List<String> roles = rm.getHeldRolesFromController(app.packageName); 101 if (roles.contains(RoleManager.ROLE_DIALER) 102 || roles.contains(RoleManager.ROLE_EMERGENCY)) { 103 row.systemApp = true; 104 } 105 final String[] nonBlockablePkgs = context.getResources().getStringArray( 106 com.android.internal.R.array.config_nonBlockableNotificationPackages); 107 markAppRowWithBlockables(nonBlockablePkgs, row, app.packageName); 108 } 109 markAppRowWithBlockables(String[] nonBlockablePkgs, AppRow row, String packageName)110 @VisibleForTesting static void markAppRowWithBlockables(String[] nonBlockablePkgs, AppRow row, 111 String packageName) { 112 if (nonBlockablePkgs != null) { 113 int N = nonBlockablePkgs.length; 114 for (int i = 0; i < N; i++) { 115 String pkg = nonBlockablePkgs[i]; 116 if (pkg == null) { 117 continue; 118 } else if (pkg.contains(":")) { 119 // handled by NotificationChannel.isImportanceLockedByOEM() 120 continue; 121 } else if (packageName.equals(nonBlockablePkgs[i])) { 122 row.systemApp = row.lockedImportance = true; 123 } 124 } 125 } 126 } 127 isSystemApp(Context context, ApplicationInfo app)128 public boolean isSystemApp(Context context, ApplicationInfo app) { 129 try { 130 PackageInfo info = context.getPackageManager().getPackageInfo( 131 app.packageName, PackageManager.GET_SIGNATURES); 132 RoleManager rm = context.getSystemService(RoleManager.class); 133 final AppRow row = new AppRow(); 134 recordCanBeBlocked(context, context.getPackageManager(), rm, info, row); 135 return row.systemApp; 136 } catch (PackageManager.NameNotFoundException e) { 137 e.printStackTrace(); 138 } 139 return false; 140 } 141 getNotificationsBanned(String pkg, int uid)142 public boolean getNotificationsBanned(String pkg, int uid) { 143 try { 144 final boolean enabled = sINM.areNotificationsEnabledForPackage(pkg, uid); 145 return !enabled; 146 } catch (Exception e) { 147 Log.w(TAG, "Error calling NoMan", e); 148 return false; 149 } 150 } 151 setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled)152 public boolean setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled) { 153 try { 154 if (onlyHasDefaultChannel(pkg, uid)) { 155 NotificationChannel defaultChannel = 156 getChannel(pkg, uid, NotificationChannel.DEFAULT_CHANNEL_ID); 157 defaultChannel.setImportance(enabled ? IMPORTANCE_UNSPECIFIED : IMPORTANCE_NONE); 158 updateChannel(pkg, uid, defaultChannel); 159 } 160 sINM.setNotificationsEnabledForPackage(pkg, uid, enabled); 161 return true; 162 } catch (Exception e) { 163 Log.w(TAG, "Error calling NoMan", e); 164 return false; 165 } 166 } 167 canShowBadge(String pkg, int uid)168 public boolean canShowBadge(String pkg, int uid) { 169 try { 170 return sINM.canShowBadge(pkg, uid); 171 } catch (Exception e) { 172 Log.w(TAG, "Error calling NoMan", e); 173 return false; 174 } 175 } 176 setShowBadge(String pkg, int uid, boolean showBadge)177 public boolean setShowBadge(String pkg, int uid, boolean showBadge) { 178 try { 179 sINM.setShowBadge(pkg, uid, showBadge); 180 return true; 181 } catch (Exception e) { 182 Log.w(TAG, "Error calling NoMan", e); 183 return false; 184 } 185 } 186 canBubble(String pkg, int uid)187 public boolean canBubble(String pkg, int uid) { 188 try { 189 return sINM.areBubblesAllowedForPackage(pkg, uid); 190 } catch (Exception e) { 191 Log.w(TAG, "Error calling NoMan", e); 192 return false; 193 } 194 } 195 setAllowBubbles(String pkg, int uid, boolean allow)196 public boolean setAllowBubbles(String pkg, int uid, boolean allow) { 197 try { 198 sINM.setBubblesAllowed(pkg, uid, allow); 199 return true; 200 } catch (Exception e) { 201 Log.w(TAG, "Error calling NoMan", e); 202 return false; 203 } 204 } 205 206 getChannel(String pkg, int uid, String channelId)207 public NotificationChannel getChannel(String pkg, int uid, String channelId) { 208 if (channelId == null) { 209 return null; 210 } 211 try { 212 return sINM.getNotificationChannelForPackage(pkg, uid, channelId, true); 213 } catch (Exception e) { 214 Log.w(TAG, "Error calling NoMan", e); 215 return null; 216 } 217 } 218 getGroup(String pkg, int uid, String groupId)219 public NotificationChannelGroup getGroup(String pkg, int uid, String groupId) { 220 if (groupId == null) { 221 return null; 222 } 223 try { 224 return sINM.getNotificationChannelGroupForPackage(groupId, pkg, uid); 225 } catch (Exception e) { 226 Log.w(TAG, "Error calling NoMan", e); 227 return null; 228 } 229 } 230 getGroups(String pkg, int uid)231 public ParceledListSlice<NotificationChannelGroup> getGroups(String pkg, int uid) { 232 try { 233 return sINM.getNotificationChannelGroupsForPackage(pkg, uid, false); 234 } catch (Exception e) { 235 Log.w(TAG, "Error calling NoMan", e); 236 return ParceledListSlice.emptyList(); 237 } 238 } 239 240 /** 241 * Returns all notification channels associated with the package and uid that will bypass DND 242 */ getNotificationChannelsBypassingDnd(String pkg, int uid)243 public ParceledListSlice<NotificationChannel> getNotificationChannelsBypassingDnd(String pkg, 244 int uid) { 245 try { 246 return sINM.getNotificationChannelsBypassingDnd(pkg, uid); 247 } catch (Exception e) { 248 Log.w(TAG, "Error calling NoMan", e); 249 return ParceledListSlice.emptyList(); 250 } 251 } 252 updateChannel(String pkg, int uid, NotificationChannel channel)253 public void updateChannel(String pkg, int uid, NotificationChannel channel) { 254 try { 255 sINM.updateNotificationChannelForPackage(pkg, uid, channel); 256 } catch (Exception e) { 257 Log.w(TAG, "Error calling NoMan", e); 258 } 259 } 260 updateChannelGroup(String pkg, int uid, NotificationChannelGroup group)261 public void updateChannelGroup(String pkg, int uid, NotificationChannelGroup group) { 262 try { 263 sINM.updateNotificationChannelGroupForPackage(pkg, uid, group); 264 } catch (Exception e) { 265 Log.w(TAG, "Error calling NoMan", e); 266 } 267 } 268 getDeletedChannelCount(String pkg, int uid)269 public int getDeletedChannelCount(String pkg, int uid) { 270 try { 271 return sINM.getDeletedChannelCount(pkg, uid); 272 } catch (Exception e) { 273 Log.w(TAG, "Error calling NoMan", e); 274 return 0; 275 } 276 } 277 getBlockedChannelCount(String pkg, int uid)278 public int getBlockedChannelCount(String pkg, int uid) { 279 try { 280 return sINM.getBlockedChannelCount(pkg, uid); 281 } catch (Exception e) { 282 Log.w(TAG, "Error calling NoMan", e); 283 return 0; 284 } 285 } 286 onlyHasDefaultChannel(String pkg, int uid)287 public boolean onlyHasDefaultChannel(String pkg, int uid) { 288 try { 289 return sINM.onlyHasDefaultChannel(pkg, uid); 290 } catch (Exception e) { 291 Log.w(TAG, "Error calling NoMan", e); 292 return false; 293 } 294 } 295 getChannelCount(String pkg, int uid)296 public int getChannelCount(String pkg, int uid) { 297 try { 298 return sINM.getNumNotificationChannelsForPackage(pkg, uid, false); 299 } catch (Exception e) { 300 Log.w(TAG, "Error calling NoMan", e); 301 return 0; 302 } 303 } 304 getNumAppsBypassingDnd(int uid)305 public int getNumAppsBypassingDnd(int uid) { 306 try { 307 return sINM.getAppsBypassingDndCount(uid); 308 } catch (Exception e) { 309 Log.w(TAG, "Error calling NoMan", e); 310 return 0; 311 } 312 } 313 getBlockedAppCount()314 public int getBlockedAppCount() { 315 try { 316 return sINM.getBlockedAppCount(UserHandle.myUserId()); 317 } catch (Exception e) { 318 Log.w(TAG, "Error calling NoMan", e); 319 return 0; 320 } 321 } 322 shouldHideSilentStatusBarIcons(Context context)323 public boolean shouldHideSilentStatusBarIcons(Context context) { 324 try { 325 return sINM.shouldHideSilentStatusIcons(context.getPackageName()); 326 } catch (Exception e) { 327 Log.w(TAG, "Error calling NoMan", e); 328 return false; 329 } 330 } 331 setHideSilentStatusIcons(boolean hide)332 public void setHideSilentStatusIcons(boolean hide) { 333 try { 334 sINM.setHideSilentStatusIcons(hide); 335 } catch (Exception e) { 336 Log.w(TAG, "Error calling NoMan", e); 337 } 338 } 339 allowAssistantAdjustment(String capability, boolean allowed)340 public void allowAssistantAdjustment(String capability, boolean allowed) { 341 try { 342 if (allowed) { 343 sINM.allowAssistantAdjustment(capability); 344 } else { 345 sINM.disallowAssistantAdjustment(capability); 346 } 347 } catch (Exception e) { 348 Log.w(TAG, "Error calling NoMan", e); 349 } 350 } 351 getAssistantAdjustments(String pkg)352 public List<String> getAssistantAdjustments(String pkg) { 353 try { 354 return sINM.getAllowedAssistantAdjustments(pkg); 355 } catch (Exception e) { 356 Log.w(TAG, "Error calling NoMan", e); 357 } 358 return new ArrayList<>(); 359 } 360 showSilentInStatusBar(String pkg)361 public boolean showSilentInStatusBar(String pkg) { 362 try { 363 return !sINM.shouldHideSilentStatusIcons(pkg); 364 } catch (Exception e) { 365 Log.w(TAG, "Error calling NoMan", e); 366 } 367 return false; 368 } 369 recordAggregatedUsageEvents(Context context, AppRow appRow)370 protected void recordAggregatedUsageEvents(Context context, AppRow appRow) { 371 long now = System.currentTimeMillis(); 372 long startTime = now - (DateUtils.DAY_IN_MILLIS * DAYS_TO_CHECK); 373 UsageEvents events = null; 374 try { 375 events = sUsageStatsManager.queryEventsForPackageForUser( 376 startTime, now, appRow.userId, appRow.pkg, context.getPackageName()); 377 } catch (RemoteException e) { 378 e.printStackTrace(); 379 } 380 recordAggregatedUsageEvents(events, appRow); 381 } 382 recordAggregatedUsageEvents(UsageEvents events, AppRow appRow)383 protected void recordAggregatedUsageEvents(UsageEvents events, AppRow appRow) { 384 appRow.sentByChannel = new HashMap<>(); 385 appRow.sentByApp = new NotificationsSentState(); 386 if (events != null) { 387 UsageEvents.Event event = new UsageEvents.Event(); 388 while (events.hasNextEvent()) { 389 events.getNextEvent(event); 390 391 if (event.getEventType() == UsageEvents.Event.NOTIFICATION_INTERRUPTION) { 392 String channelId = event.mNotificationChannelId; 393 if (channelId != null) { 394 NotificationsSentState stats = appRow.sentByChannel.get(channelId); 395 if (stats == null) { 396 stats = new NotificationsSentState(); 397 appRow.sentByChannel.put(channelId, stats); 398 } 399 if (event.getTimeStamp() > stats.lastSent) { 400 stats.lastSent = event.getTimeStamp(); 401 appRow.sentByApp.lastSent = event.getTimeStamp(); 402 } 403 stats.sentCount++; 404 appRow.sentByApp.sentCount++; 405 calculateAvgSentCounts(stats); 406 } 407 } 408 409 } 410 calculateAvgSentCounts(appRow.sentByApp); 411 } 412 } 413 getSentSummary(Context context, NotificationsSentState state, boolean sortByRecency)414 public static CharSequence getSentSummary(Context context, NotificationsSentState state, 415 boolean sortByRecency) { 416 if (state == null) { 417 return null; 418 } 419 if (sortByRecency) { 420 if (state.lastSent == 0) { 421 return context.getString(R.string.notifications_sent_never); 422 } 423 return StringUtil.formatRelativeTime( 424 context, System.currentTimeMillis() - state.lastSent, true); 425 } else { 426 if (state.avgSentDaily > 0) { 427 return context.getResources().getQuantityString(R.plurals.notifications_sent_daily, 428 state.avgSentDaily, state.avgSentDaily); 429 } 430 return context.getResources().getQuantityString(R.plurals.notifications_sent_weekly, 431 state.avgSentWeekly, state.avgSentWeekly); 432 } 433 } 434 calculateAvgSentCounts(NotificationsSentState stats)435 private void calculateAvgSentCounts(NotificationsSentState stats) { 436 if (stats != null) { 437 stats.avgSentDaily = Math.round((float) stats.sentCount / DAYS_TO_CHECK); 438 if (stats.sentCount < DAYS_TO_CHECK) { 439 stats.avgSentWeekly = stats.sentCount; 440 } 441 } 442 } 443 getAllowedNotificationAssistant()444 public ComponentName getAllowedNotificationAssistant() { 445 try { 446 return sINM.getAllowedNotificationAssistant(); 447 } catch (Exception e) { 448 Log.w(TAG, "Error calling NoMan", e); 449 return null; 450 } 451 } 452 setNotificationAssistantGranted(ComponentName cn)453 public boolean setNotificationAssistantGranted(ComponentName cn) { 454 try { 455 sINM.setNotificationAssistantAccessGranted(cn, true); 456 if (cn == null) { 457 return sINM.getAllowedNotificationAssistant() == null; 458 } else { 459 return cn.equals(sINM.getAllowedNotificationAssistant()); 460 } 461 } catch (Exception e) { 462 Log.w(TAG, "Error calling NoMan", e); 463 return false; 464 } 465 } 466 467 /** 468 * NotificationsSentState contains how often an app sends notifications and how recently it sent 469 * one. 470 */ 471 public static class NotificationsSentState { 472 public int avgSentDaily = 0; 473 public int avgSentWeekly = 0; 474 public long lastSent = 0; 475 public int sentCount = 0; 476 } 477 478 static class Row { 479 public String section; 480 } 481 482 public static class AppRow extends Row { 483 public String pkg; 484 public int uid; 485 public Drawable icon; 486 public CharSequence label; 487 public Intent settingsIntent; 488 public boolean banned; 489 public boolean first; // first app in section 490 public boolean systemApp; 491 public boolean lockedImportance; 492 public boolean showBadge; 493 public boolean allowBubbles; 494 public int userId; 495 public int blockedChannelCount; 496 public int channelCount; 497 public Map<String, NotificationsSentState> sentByChannel; 498 public NotificationsSentState sentByApp; 499 } 500 } 501