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