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