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