• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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