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