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