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