1 /* 2 * Copyright (C) 2012 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 17 package com.android.settings.notification; 18 19 import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; 20 21 import android.app.Activity; 22 import android.app.ActivityManager; 23 import android.app.INotificationManager; 24 import android.app.Notification; 25 import android.app.NotificationChannel; 26 import android.app.PendingIntent; 27 import android.app.settings.SettingsEnums; 28 import android.content.ComponentName; 29 import android.content.Context; 30 import android.content.IntentSender; 31 import android.content.pm.ApplicationInfo; 32 import android.content.pm.PackageManager; 33 import android.content.res.Resources; 34 import android.graphics.Typeface; 35 import android.graphics.drawable.Drawable; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.Parcel; 39 import android.os.RemoteException; 40 import android.os.ServiceManager; 41 import android.os.UserHandle; 42 import android.service.notification.NotificationListenerService; 43 import android.service.notification.NotificationListenerService.Ranking; 44 import android.service.notification.NotificationListenerService.RankingMap; 45 import android.service.notification.StatusBarNotification; 46 import android.text.SpannableString; 47 import android.text.SpannableStringBuilder; 48 import android.text.TextUtils; 49 import android.text.style.StyleSpan; 50 import android.util.Log; 51 import android.view.View; 52 import android.widget.DateTimeView; 53 import android.widget.ImageView; 54 import android.widget.TextView; 55 56 import androidx.preference.Preference; 57 import androidx.preference.PreferenceViewHolder; 58 import androidx.recyclerview.widget.RecyclerView; 59 60 import com.android.settings.R; 61 import com.android.settings.SettingsPreferenceFragment; 62 import com.android.settings.Utils; 63 64 import java.util.ArrayList; 65 import java.util.Collections; 66 import java.util.Comparator; 67 import java.util.List; 68 69 public class NotificationStation extends SettingsPreferenceFragment { 70 private static final String TAG = NotificationStation.class.getSimpleName(); 71 72 private static final boolean DEBUG = false; 73 private static final boolean DUMP_EXTRAS = true; 74 private static final boolean DUMP_PARCEL = true; 75 private Handler mHandler; 76 77 private static class HistoricalNotificationInfo { 78 public String key; 79 public String channel; 80 public String pkg; 81 public Drawable pkgicon; 82 public CharSequence pkgname; 83 public Drawable icon; 84 public CharSequence title; 85 public int priority; 86 public int user; 87 public long timestamp; 88 public boolean active; 89 public CharSequence extra; 90 } 91 92 private PackageManager mPm; 93 private INotificationManager mNoMan; 94 private RankingMap mRanking; 95 96 private Runnable mRefreshListRunnable = new Runnable() { 97 @Override 98 public void run() { 99 refreshList(); 100 } 101 }; 102 103 private final NotificationListenerService mListener = new NotificationListenerService() { 104 @Override 105 public void onNotificationPosted(StatusBarNotification sbn, RankingMap ranking) { 106 logd("onNotificationPosted: %s, with update for %d", sbn.getNotification(), 107 ranking == null ? 0 : ranking.getOrderedKeys().length); 108 mRanking = ranking; 109 scheduleRefreshList(); 110 } 111 112 @Override 113 public void onNotificationRemoved(StatusBarNotification notification, RankingMap ranking) { 114 logd("onNotificationRankingUpdate with update for %d", 115 ranking == null ? 0 : ranking.getOrderedKeys().length); 116 mRanking = ranking; 117 scheduleRefreshList(); 118 } 119 120 @Override 121 public void onNotificationRankingUpdate(RankingMap ranking) { 122 logd("onNotificationRankingUpdate with update for %d", 123 ranking == null ? 0 : ranking.getOrderedKeys().length); 124 mRanking = ranking; 125 scheduleRefreshList(); 126 } 127 128 @Override 129 public void onListenerConnected() { 130 mRanking = getCurrentRanking(); 131 logd("onListenerConnected with update for %d", 132 mRanking == null ? 0 : mRanking.getOrderedKeys().length); 133 scheduleRefreshList(); 134 } 135 }; 136 scheduleRefreshList()137 private void scheduleRefreshList() { 138 if (mHandler != null) { 139 mHandler.removeCallbacks(mRefreshListRunnable); 140 mHandler.postDelayed(mRefreshListRunnable, 100); 141 } 142 } 143 144 private Context mContext; 145 146 private final Comparator<HistoricalNotificationInfo> mNotificationSorter 147 = new Comparator<HistoricalNotificationInfo>() { 148 @Override 149 public int compare(HistoricalNotificationInfo lhs, 150 HistoricalNotificationInfo rhs) { 151 return Long.compare(rhs.timestamp, lhs.timestamp); 152 } 153 }; 154 155 @Override onAttach(Activity activity)156 public void onAttach(Activity activity) { 157 logd("onAttach(%s)", activity.getClass().getSimpleName()); 158 super.onAttach(activity); 159 mHandler = new Handler(activity.getMainLooper()); 160 mContext = activity; 161 mPm = mContext.getPackageManager(); 162 mNoMan = INotificationManager.Stub.asInterface( 163 ServiceManager.getService(Context.NOTIFICATION_SERVICE)); 164 } 165 166 @Override onDetach()167 public void onDetach() { 168 logd("onDetach()"); 169 mHandler.removeCallbacks(mRefreshListRunnable); 170 mHandler = null; 171 super.onDetach(); 172 } 173 174 @Override onPause()175 public void onPause() { 176 try { 177 mListener.unregisterAsSystemService(); 178 } catch (RemoteException e) { 179 Log.e(TAG, "Cannot unregister listener", e); 180 } 181 super.onPause(); 182 } 183 184 @Override getMetricsCategory()185 public int getMetricsCategory() { 186 return SettingsEnums.NOTIFICATION_STATION; 187 } 188 189 @Override onActivityCreated(Bundle savedInstanceState)190 public void onActivityCreated(Bundle savedInstanceState) { 191 logd("onActivityCreated(%s)", savedInstanceState); 192 super.onActivityCreated(savedInstanceState); 193 194 RecyclerView listView = getListView(); 195 Utils.forceCustomPadding(listView, false /* non additive padding */); 196 } 197 198 @Override onResume()199 public void onResume() { 200 logd("onResume()"); 201 super.onResume(); 202 try { 203 mListener.registerAsSystemService(mContext, new ComponentName(mContext.getPackageName(), 204 this.getClass().getCanonicalName()), ActivityManager.getCurrentUser()); 205 } catch (RemoteException e) { 206 Log.e(TAG, "Cannot register listener", e); 207 } 208 refreshList(); 209 } 210 refreshList()211 private void refreshList() { 212 List<HistoricalNotificationInfo> infos = loadNotifications(); 213 if (infos != null) { 214 final int N = infos.size(); 215 logd("adding %d infos", N); 216 Collections.sort(infos, mNotificationSorter); 217 if (getPreferenceScreen() == null) { 218 setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext())); 219 } 220 getPreferenceScreen().removeAll(); 221 for (int i = 0; i < N; i++) { 222 getPreferenceScreen().addPreference( 223 new HistoricalNotificationPreference(getPrefContext(), infos.get(i))); 224 } 225 } 226 } 227 logd(String msg, Object... args)228 private static void logd(String msg, Object... args) { 229 if (DEBUG) { 230 Log.d(TAG, args == null || args.length == 0 ? msg : String.format(msg, args)); 231 } 232 } 233 bold(CharSequence cs)234 private static CharSequence bold(CharSequence cs) { 235 if (cs.length() == 0) return cs; 236 SpannableString ss = new SpannableString(cs); 237 ss.setSpan(new StyleSpan(Typeface.BOLD), 0, cs.length(), 0); 238 return ss; 239 } 240 getTitleString(Notification n)241 private static String getTitleString(Notification n) { 242 CharSequence title = null; 243 if (n.extras != null) { 244 title = n.extras.getCharSequence(Notification.EXTRA_TITLE); 245 if (TextUtils.isEmpty(title)) { 246 title = n.extras.getCharSequence(Notification.EXTRA_TEXT); 247 } 248 } 249 if (TextUtils.isEmpty(title) && !TextUtils.isEmpty(n.tickerText)) { 250 title = n.tickerText; 251 } 252 return String.valueOf(title); 253 } 254 formatPendingIntent(PendingIntent pi)255 private static String formatPendingIntent(PendingIntent pi) { 256 final StringBuilder sb = new StringBuilder(); 257 final IntentSender is = pi.getIntentSender(); 258 sb.append("Intent(pkg=").append(is.getCreatorPackage()); 259 try { 260 final boolean isActivity = 261 ActivityManager.getService().isIntentSenderAnActivity(is.getTarget()); 262 if (isActivity) sb.append(" (activity)"); 263 } catch (RemoteException ex) {} 264 sb.append(")"); 265 return sb.toString(); 266 } 267 loadNotifications()268 private List<HistoricalNotificationInfo> loadNotifications() { 269 final int currentUserId = ActivityManager.getCurrentUser(); 270 try { 271 StatusBarNotification[] active = mNoMan.getActiveNotifications( 272 mContext.getPackageName()); 273 StatusBarNotification[] dismissed = mNoMan.getHistoricalNotifications( 274 mContext.getPackageName(), 50); 275 276 List<HistoricalNotificationInfo> list 277 = new ArrayList<HistoricalNotificationInfo>(active.length + dismissed.length); 278 279 for (StatusBarNotification[] resultset 280 : new StatusBarNotification[][] { active, dismissed }) { 281 for (StatusBarNotification sbn : resultset) { 282 if (sbn.getUserId() != UserHandle.USER_ALL & sbn.getUserId() != currentUserId) { 283 continue; 284 } 285 286 final Notification n = sbn.getNotification(); 287 final HistoricalNotificationInfo info = new HistoricalNotificationInfo(); 288 info.pkg = sbn.getPackageName(); 289 info.user = sbn.getUserId(); 290 info.icon = loadIconDrawable(info.pkg, info.user, n.icon); 291 info.pkgicon = loadPackageIconDrawable(info.pkg, info.user); 292 info.pkgname = loadPackageName(info.pkg); 293 info.title = getTitleString(n); 294 if (TextUtils.isEmpty(info.title)) { 295 info.title = getString(R.string.notification_log_no_title); 296 } 297 info.timestamp = sbn.getPostTime(); 298 info.priority = n.priority; 299 info.channel = n.getChannelId(); 300 info.key = sbn.getKey(); 301 302 info.active = (resultset == active); 303 304 info.extra = generateExtraText(sbn, info); 305 306 logd(" [%d] %s: %s", info.timestamp, info.pkg, info.title); 307 list.add(info); 308 } 309 } 310 311 return list; 312 } catch (RemoteException e) { 313 Log.e(TAG, "Cannot load Notifications: ", e); 314 } 315 return null; 316 } 317 generateExtraText(StatusBarNotification sbn, HistoricalNotificationInfo info)318 private CharSequence generateExtraText(StatusBarNotification sbn, 319 HistoricalNotificationInfo info) { 320 final Ranking rank = new Ranking(); 321 322 final Notification n = sbn.getNotification(); 323 final SpannableStringBuilder sb = new SpannableStringBuilder(); 324 final String delim = getString(R.string.notification_log_details_delimiter); 325 sb.append(bold(getString(R.string.notification_log_details_package))) 326 .append(delim) 327 .append(info.pkg) 328 .append("\n") 329 .append(bold(getString(R.string.notification_log_details_key))) 330 .append(delim) 331 .append(sbn.getKey()); 332 sb.append("\n") 333 .append(bold(getString(R.string.notification_log_details_icon))) 334 .append(delim) 335 .append(String.valueOf(n.getSmallIcon())); 336 sb.append("\n") 337 .append(bold("channelId")) 338 .append(delim) 339 .append(String.valueOf(n.getChannelId())); 340 sb.append("\n") 341 .append(bold("postTime")) 342 .append(delim) 343 .append(String.valueOf(sbn.getPostTime())); 344 if (n.getTimeoutAfter() != 0) { 345 sb.append("\n") 346 .append(bold("timeoutAfter")) 347 .append(delim) 348 .append(String.valueOf(n.getTimeoutAfter())); 349 } 350 if (sbn.isGroup()) { 351 sb.append("\n") 352 .append(bold(getString(R.string.notification_log_details_group))) 353 .append(delim) 354 .append(String.valueOf(sbn.getGroupKey())); 355 if (n.isGroupSummary()) { 356 sb.append(bold( 357 getString(R.string.notification_log_details_group_summary))); 358 } 359 } 360 if (info.active) { 361 // mRanking only applies to active notifications 362 if (mRanking != null && mRanking.getRanking(sbn.getKey(), rank)) { 363 if (rank.getLastAudiblyAlertedMillis() > 0) { 364 sb.append("\n") 365 .append(bold(getString(R.string.notification_log_details_alerted))); 366 } 367 } 368 } 369 try { 370 NotificationChannel channel = mNoMan.getNotificationChannelForPackage( 371 sbn.getPackageName(), sbn.getUid(), n.getChannelId(), false); 372 sb.append("\n") 373 .append(bold(getString(R.string.notification_log_details_sound))) 374 .append(delim); 375 if (channel == null || channel.getImportance() == IMPORTANCE_UNSPECIFIED) { 376 377 if (0 != (n.defaults & Notification.DEFAULT_SOUND)) { 378 sb.append(getString(R.string.notification_log_details_default)); 379 } else if (n.sound != null) { 380 sb.append(n.sound.toString()); 381 } else { 382 sb.append(getString(R.string.notification_log_details_none)); 383 } 384 } else { 385 sb.append(String.valueOf(channel.getSound())); 386 } 387 sb.append("\n") 388 .append(bold(getString(R.string.notification_log_details_vibrate))) 389 .append(delim); 390 if (channel == null || channel.getImportance() == IMPORTANCE_UNSPECIFIED) { 391 if (0 != (n.defaults & Notification.DEFAULT_VIBRATE)) { 392 sb.append(getString(R.string.notification_log_details_default)); 393 } else if (n.vibrate != null) { 394 sb.append(getString(R.string.notification_log_details_vibrate_pattern)); 395 } else { 396 sb.append(getString(R.string.notification_log_details_none)); 397 } 398 } else { 399 if (channel.getVibrationPattern() != null) { 400 sb.append(getString(R.string.notification_log_details_vibrate_pattern)); 401 } else { 402 sb.append(getString(R.string.notification_log_details_none)); 403 } 404 } 405 } catch (RemoteException e) { 406 Log.d(TAG, "cannot read channel info", e); 407 } 408 sb.append("\n") 409 .append(bold(getString(R.string.notification_log_details_visibility))) 410 .append(delim) 411 .append(Notification.visibilityToString(n.visibility)); 412 if (n.publicVersion != null) { 413 sb.append("\n") 414 .append(bold(getString( 415 R.string.notification_log_details_public_version))) 416 .append(delim) 417 .append(getTitleString(n.publicVersion)); 418 } 419 sb.append("\n") 420 .append(bold(getString(R.string.notification_log_details_priority))) 421 .append(delim) 422 .append(Notification.priorityToString(n.priority)); 423 if (info.active) { 424 // mRanking only applies to active notifications 425 if (mRanking != null && mRanking.getRanking(sbn.getKey(), rank)) { 426 sb.append("\n") 427 .append(bold(getString( 428 R.string.notification_log_details_importance))) 429 .append(delim) 430 .append(Ranking.importanceToString(rank.getImportance())); 431 if (rank.getImportanceExplanation() != null) { 432 sb.append("\n") 433 .append(bold(getString( 434 R.string.notification_log_details_explanation))) 435 .append(delim) 436 .append(rank.getImportanceExplanation()); 437 } 438 sb.append("\n") 439 .append(bold(getString( 440 R.string.notification_log_details_badge))) 441 .append(delim) 442 .append(Boolean.toString(rank.canShowBadge())); 443 } else { 444 if (mRanking == null) { 445 sb.append("\n") 446 .append(bold(getString( 447 R.string.notification_log_details_ranking_null))); 448 } else { 449 sb.append("\n") 450 .append(bold(getString( 451 R.string.notification_log_details_ranking_none))); 452 } 453 } 454 } 455 if (n.contentIntent != null) { 456 sb.append("\n") 457 .append(bold(getString( 458 R.string.notification_log_details_content_intent))) 459 .append(delim) 460 .append(formatPendingIntent(n.contentIntent)); 461 } 462 if (n.deleteIntent != null) { 463 sb.append("\n") 464 .append(bold(getString( 465 R.string.notification_log_details_delete_intent))) 466 .append(delim) 467 .append(formatPendingIntent(n.deleteIntent)); 468 } 469 if (n.fullScreenIntent != null) { 470 sb.append("\n") 471 .append(bold(getString( 472 R.string.notification_log_details_full_screen_intent))) 473 .append(delim) 474 .append(formatPendingIntent(n.fullScreenIntent)); 475 } 476 if (n.actions != null && n.actions.length > 0) { 477 sb.append("\n") 478 .append(bold(getString(R.string.notification_log_details_actions))); 479 for (int ai=0; ai<n.actions.length; ai++) { 480 final Notification.Action action = n.actions[ai]; 481 sb.append("\n ").append(String.valueOf(ai)).append(' ') 482 .append(bold(getString( 483 R.string.notification_log_details_title))) 484 .append(delim) 485 .append(action.title); 486 if (action.actionIntent != null) { 487 sb.append("\n ") 488 .append(bold(getString( 489 R.string.notification_log_details_content_intent))) 490 .append(delim) 491 .append(formatPendingIntent(action.actionIntent)); 492 } 493 if (action.getRemoteInputs() != null) { 494 sb.append("\n ") 495 .append(bold(getString( 496 R.string.notification_log_details_remoteinput))) 497 .append(delim) 498 .append(String.valueOf(action.getRemoteInputs().length)); 499 } 500 } 501 } 502 if (n.contentView != null) { 503 sb.append("\n") 504 .append(bold(getString( 505 R.string.notification_log_details_content_view))) 506 .append(delim) 507 .append(n.contentView.toString()); 508 } 509 510 if (DUMP_EXTRAS) { 511 if (n.extras != null && n.extras.size() > 0) { 512 sb.append("\n") 513 .append(bold(getString( 514 R.string.notification_log_details_extras))); 515 for (String extraKey : n.extras.keySet()) { 516 String val = String.valueOf(n.extras.get(extraKey)); 517 if (val.length() > 100) val = val.substring(0, 100) + "..."; 518 sb.append("\n ").append(extraKey).append(delim).append(val); 519 } 520 } 521 } 522 if (DUMP_PARCEL) { 523 final Parcel p = Parcel.obtain(); 524 n.writeToParcel(p, 0); 525 sb.append("\n") 526 .append(bold(getString(R.string.notification_log_details_parcel))) 527 .append(delim) 528 .append(String.valueOf(p.dataPosition())) 529 .append(' ') 530 .append(bold(getString(R.string.notification_log_details_ashmem))) 531 .append(delim) 532 .append(String.valueOf(p.getBlobAshmemSize())) 533 .append("\n"); 534 } 535 return sb; 536 } 537 getResourcesForUserPackage(String pkg, int userId)538 private Resources getResourcesForUserPackage(String pkg, int userId) { 539 Resources r = null; 540 541 if (pkg != null) { 542 try { 543 if (userId == UserHandle.USER_ALL) { 544 userId = UserHandle.USER_SYSTEM; 545 } 546 r = mPm.getResourcesForApplicationAsUser(pkg, userId); 547 } catch (PackageManager.NameNotFoundException ex) { 548 Log.e(TAG, "Icon package not found: " + pkg, ex); 549 return null; 550 } 551 } else { 552 r = mContext.getResources(); 553 } 554 return r; 555 } 556 loadPackageIconDrawable(String pkg, int userId)557 private Drawable loadPackageIconDrawable(String pkg, int userId) { 558 Drawable icon = null; 559 try { 560 icon = mPm.getApplicationIcon(pkg); 561 } catch (PackageManager.NameNotFoundException e) { 562 Log.e(TAG, "Cannot get application icon", e); 563 } 564 565 return icon; 566 } 567 loadPackageName(String pkg)568 private CharSequence loadPackageName(String pkg) { 569 try { 570 ApplicationInfo info = mPm.getApplicationInfo(pkg, 571 PackageManager.MATCH_ANY_USER); 572 if (info != null) return mPm.getApplicationLabel(info); 573 } catch (PackageManager.NameNotFoundException e) { 574 Log.e(TAG, "Cannot load package name", e); 575 } 576 return pkg; 577 } 578 loadIconDrawable(String pkg, int userId, int resId)579 private Drawable loadIconDrawable(String pkg, int userId, int resId) { 580 Resources r = getResourcesForUserPackage(pkg, userId); 581 582 if (resId == 0) { 583 return null; 584 } 585 586 try { 587 return r.getDrawable(resId, null); 588 } catch (RuntimeException e) { 589 Log.w(TAG, "Icon not found in " 590 + (pkg != null ? resId : "<system>") 591 + ": " + Integer.toHexString(resId), e); 592 } 593 594 return null; 595 } 596 597 private static class HistoricalNotificationPreference extends Preference { 598 private final HistoricalNotificationInfo mInfo; 599 private static long sLastExpandedTimestamp; // quick hack to keep things from collapsing 600 HistoricalNotificationPreference(Context context, HistoricalNotificationInfo info)601 public HistoricalNotificationPreference(Context context, HistoricalNotificationInfo info) { 602 super(context); 603 setLayoutResource(R.layout.notification_log_row); 604 mInfo = info; 605 } 606 607 @Override onBindViewHolder(PreferenceViewHolder row)608 public void onBindViewHolder(PreferenceViewHolder row) { 609 super.onBindViewHolder(row); 610 611 if (mInfo.icon != null) { 612 ((ImageView) row.findViewById(R.id.icon)).setImageDrawable(mInfo.icon); 613 } 614 if (mInfo.pkgicon != null) { 615 ((ImageView) row.findViewById(R.id.pkgicon)).setImageDrawable(mInfo.pkgicon); 616 } 617 618 ((DateTimeView) row.findViewById(R.id.timestamp)).setTime(mInfo.timestamp); 619 ((TextView) row.findViewById(R.id.title)).setText(mInfo.title); 620 ((TextView) row.findViewById(R.id.pkgname)).setText(mInfo.pkgname); 621 622 final TextView extra = (TextView) row.findViewById(R.id.extra); 623 extra.setText(mInfo.extra); 624 extra.setVisibility(mInfo.timestamp == sLastExpandedTimestamp 625 ? View.VISIBLE : View.GONE); 626 627 row.itemView.setOnClickListener( 628 new View.OnClickListener() { 629 @Override 630 public void onClick(View view) { 631 extra.setVisibility(extra.getVisibility() == View.VISIBLE 632 ? View.GONE : View.VISIBLE); 633 sLastExpandedTimestamp = mInfo.timestamp; 634 } 635 }); 636 637 row.itemView.setAlpha(mInfo.active ? 1.0f : 0.5f); 638 } 639 640 @Override performClick()641 public void performClick() { 642 // Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 643 // Uri.fromParts("package", mInfo.pkg, null)); 644 // intent.setComponent(intent.resolveActivity(getContext().getPackageManager())); 645 // getContext().startActivity(intent); 646 } 647 } 648 } 649