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.history;
18 
19 import static android.provider.Settings.EXTRA_APP_PACKAGE;
20 import static android.provider.Settings.EXTRA_CHANNEL_ID;
21 
22 import android.app.Activity;
23 import android.app.ActivityManager;
24 import android.app.INotificationManager;
25 import android.app.Notification;
26 import android.app.NotificationChannel;
27 import android.app.PendingIntent;
28 import android.app.settings.SettingsEnums;
29 import android.content.ComponentName;
30 import android.content.Context;
31 import android.content.Intent;
32 import android.content.IntentSender;
33 import android.content.pm.ApplicationInfo;
34 import android.content.pm.PackageManager;
35 import android.graphics.PorterDuff;
36 import android.graphics.Typeface;
37 import android.graphics.drawable.Drawable;
38 import android.os.Bundle;
39 import android.os.Parcel;
40 import android.os.RemoteException;
41 import android.os.ServiceManager;
42 import android.os.UserHandle;
43 import android.provider.Settings;
44 import android.service.notification.NotificationListenerService;
45 import android.service.notification.NotificationListenerService.Ranking;
46 import android.service.notification.NotificationListenerService.RankingMap;
47 import android.service.notification.StatusBarNotification;
48 import android.text.SpannableString;
49 import android.text.SpannableStringBuilder;
50 import android.text.TextUtils;
51 import android.text.style.StyleSpan;
52 import android.util.Log;
53 import android.view.View;
54 import android.view.ViewGroup;
55 import android.widget.DateTimeView;
56 import android.widget.ImageView;
57 import android.widget.TextView;
58 
59 import androidx.preference.Preference;
60 import androidx.preference.PreferenceViewHolder;
61 import androidx.recyclerview.widget.RecyclerView;
62 
63 import com.android.settings.R;
64 import com.android.settings.SettingsPreferenceFragment;
65 import com.android.settings.Utils;
66 
67 import java.util.ArrayList;
68 import java.util.Comparator;
69 import java.util.LinkedList;
70 import java.util.List;
71 
72 public class NotificationStation extends SettingsPreferenceFragment {
73     private static final String TAG = NotificationStation.class.getSimpleName();
74 
75     private static final boolean DEBUG = false;
76     private static final boolean DUMP_EXTRAS = true;
77     private static final boolean DUMP_PARCEL = true;
78 
79     private static class HistoricalNotificationInfo {
80         public String key;
81         public NotificationChannel channel;
82         // Historical notifications don't have Ranking information. for most fields that's ok
83         // but we need channel id to launch settings.
84         public String channelId;
85         public String pkg;
86         public Drawable pkgicon;
87         public CharSequence pkgname;
88         public Drawable icon;
89         public boolean badged;
90         public CharSequence title;
91         public CharSequence text;
92         public int priority;
93         public int user;
94         public long timestamp;
95         public boolean active;
96         public CharSequence notificationExtra;
97         public CharSequence rankingExtra;
98         public boolean alerted;
99         public boolean visuallyInterruptive;
100 
updateFrom(HistoricalNotificationInfo updatedInfo)101         public void updateFrom(HistoricalNotificationInfo updatedInfo) {
102             this.channel = updatedInfo.channel;
103             this.icon = updatedInfo.icon;
104             this.title = updatedInfo.title;
105             this.text = updatedInfo.text;
106             this.priority = updatedInfo.priority;
107             this.timestamp = updatedInfo.timestamp;
108             this.active = updatedInfo.active;
109             this.alerted = updatedInfo.alerted;
110             this.visuallyInterruptive = updatedInfo.visuallyInterruptive;
111             this.notificationExtra = updatedInfo.notificationExtra;
112             this.rankingExtra = updatedInfo.rankingExtra;
113         }
114     }
115 
116     private PackageManager mPm;
117     private INotificationManager mNoMan;
118     private RankingMap mRanking;
119     private LinkedList<HistoricalNotificationInfo> mNotificationInfos;
120 
121     private final NotificationListenerService mListener = new NotificationListenerService() {
122         @Override
123         public void onNotificationPosted(StatusBarNotification sbn, RankingMap ranking) {
124             logd("onNotificationPosted: %s, with update for %d", sbn.getNotification(),
125                     ranking == null ? 0 : ranking.getOrderedKeys().length);
126             mRanking = ranking;
127             if (sbn.getNotification().isGroupSummary()) {
128                 return;
129             }
130             addOrUpdateNotification(sbn);
131         }
132 
133         @Override
134         public void onNotificationRemoved(StatusBarNotification sbn, RankingMap ranking) {
135             logd("onNotificationRankingUpdate with update for %d",
136                     ranking == null ? 0 : ranking.getOrderedKeys().length);
137             mRanking = ranking;
138             if (sbn.getNotification().isGroupSummary()) {
139                 return;
140             }
141             markNotificationAsDismissed(sbn);
142         }
143 
144         @Override
145         public void onNotificationRankingUpdate(RankingMap ranking) {
146             logd("onNotificationRankingUpdate with update for %d",
147                     ranking == null ? 0 : ranking.getOrderedKeys().length);
148             mRanking = ranking;
149             updateNotificationsFromRanking();
150         }
151 
152         @Override
153         public void onListenerConnected() {
154             mRanking = getCurrentRanking();
155             logd("onListenerConnected with update for %d",
156                     mRanking == null ? 0 : mRanking.getOrderedKeys().length);
157             populateNotifications();
158         }
159     };
160 
161     private Context mContext;
162 
163     private final Comparator<HistoricalNotificationInfo> mNotificationSorter
164             = (lhs, rhs) -> Long.compare(rhs.timestamp, lhs.timestamp);
165 
166     @Override
onAttach(Activity activity)167     public void onAttach(Activity activity) {
168         logd("onAttach(%s)", activity.getClass().getSimpleName());
169         super.onAttach(activity);
170         mContext = activity;
171         mPm = mContext.getPackageManager();
172         mNoMan = INotificationManager.Stub.asInterface(
173                 ServiceManager.getService(Context.NOTIFICATION_SERVICE));
174         mNotificationInfos = new LinkedList<>();
175     }
176 
177     @Override
onDetach()178     public void onDetach() {
179         logd("onDetach()");
180         super.onDetach();
181     }
182 
183     @Override
onPause()184     public void onPause() {
185         try {
186             mListener.unregisterAsSystemService();
187         } catch (RemoteException e) {
188             Log.e(TAG, "Cannot unregister listener", e);
189         }
190         super.onPause();
191     }
192 
193     @Override
getMetricsCategory()194     public int getMetricsCategory() {
195         return SettingsEnums.NOTIFICATION_STATION;
196     }
197 
198     @Override
onActivityCreated(Bundle savedInstanceState)199     public void onActivityCreated(Bundle savedInstanceState) {
200         logd("onActivityCreated(%s)", savedInstanceState);
201         super.onActivityCreated(savedInstanceState);
202 
203         RecyclerView listView = getListView();
204         Utils.forceCustomPadding(listView, false /* non additive padding */);
205     }
206 
207     @Override
onResume()208     public void onResume() {
209         logd("onResume()");
210         super.onResume();
211         try {
212             mListener.registerAsSystemService(mContext, new ComponentName(mContext.getPackageName(),
213                     this.getClass().getCanonicalName()), ActivityManager.getCurrentUser());
214         } catch (RemoteException e) {
215             Log.e(TAG, "Cannot register listener", e);
216         }
217     }
218 
219     /**
220      * Adds all current and historical notifications when the NLS connects.
221      */
populateNotifications()222     private void populateNotifications() {
223         loadNotifications();
224         final int N = mNotificationInfos.size();
225         logd("adding %d infos", N);
226         if (getPreferenceScreen() == null) {
227             setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext()));
228         }
229         getPreferenceScreen().removeAll();
230         for (int i = 0; i < N; i++) {
231             getPreferenceScreen().addPreference(new HistoricalNotificationPreference(
232                     getPrefContext(), mNotificationInfos.get(i), i));
233         }
234     }
235 
236     /**
237      * Finds and dims the given notification in the preferences list.
238      */
markNotificationAsDismissed(StatusBarNotification sbn)239     private void markNotificationAsDismissed(StatusBarNotification sbn) {
240         final int N = mNotificationInfos.size();
241         for (int i = 0; i < N; i++) {
242             final HistoricalNotificationInfo info = mNotificationInfos.get(i);
243             if (TextUtils.equals(info.key, sbn.getKey())) {
244                 info.active = false;
245                 ((HistoricalNotificationPreference) getPreferenceScreen().findPreference(
246                         sbn.getKey())).updatePreference(info);
247                break;
248             }
249         }
250     }
251 
252     /**
253      * Either updates a notification with its latest information or (if it's something the user
254      * would consider a new notification) adds a new entry at the start of the list.
255      */
addOrUpdateNotification(StatusBarNotification sbn)256     private void addOrUpdateNotification(StatusBarNotification sbn) {
257         HistoricalNotificationInfo newInfo = createFromSbn(sbn, true);
258         boolean needsAdd = true;
259         final int N = mNotificationInfos.size();
260         for (int i = 0; i < N; i++) {
261             final HistoricalNotificationInfo info = mNotificationInfos.get(i);
262             if (TextUtils.equals(info.key, sbn.getKey()) && info.active
263                     && !newInfo.alerted && !newInfo.visuallyInterruptive) {
264                 info.updateFrom(newInfo);
265 
266                 ((HistoricalNotificationPreference) getPreferenceScreen().findPreference(
267                         sbn.getKey())).updatePreference(info);
268                 needsAdd = false;
269                 break;
270             }
271         }
272         if (needsAdd) {
273             mNotificationInfos.addFirst(newInfo);
274             getPreferenceScreen().addPreference(new HistoricalNotificationPreference(
275                     getPrefContext(), mNotificationInfos.peekFirst(),
276                     -1 * mNotificationInfos.size()));
277         }
278     }
279 
280     /**
281      * Updates all notifications in the list based on new information in the ranking.
282      */
updateNotificationsFromRanking()283     private void updateNotificationsFromRanking() {
284         Ranking rank = new Ranking();
285         for (int i = 0; i < getPreferenceScreen().getPreferenceCount(); i++) {
286             final HistoricalNotificationPreference p =
287                     (HistoricalNotificationPreference) getPreferenceScreen().getPreference(i);
288             final HistoricalNotificationInfo info = mNotificationInfos.get(i);
289             mRanking.getRanking(p.getKey(), rank);
290 
291             updateFromRanking(info);
292             ((HistoricalNotificationPreference) getPreferenceScreen().findPreference(
293                     info.key)).updatePreference(info);
294         }
295     }
296 
logd(String msg, Object... args)297     private static void logd(String msg, Object... args) {
298         if (DEBUG) {
299             Log.d(TAG, args == null || args.length == 0 ? msg : String.format(msg, args));
300         }
301     }
302 
bold(CharSequence cs)303     private static CharSequence bold(CharSequence cs) {
304         if (cs.length() == 0) return cs;
305         SpannableString ss = new SpannableString(cs);
306         ss.setSpan(new StyleSpan(Typeface.BOLD), 0, cs.length(), 0);
307         return ss;
308     }
309 
getTitleString(Notification n)310     private static String getTitleString(Notification n) {
311         CharSequence title = null;
312         if (n.extras != null) {
313             title = n.extras.getCharSequence(Notification.EXTRA_TITLE);
314         }
315         return title == null? null : String.valueOf(title);
316     }
317 
318     /**
319      * Returns the appropriate substring for this notification based on the style of notification.
320      */
getTextString(Context appContext, Notification n)321     private static String getTextString(Context appContext, Notification n) {
322         CharSequence text = null;
323         if (n.extras != null) {
324             text = n.extras.getCharSequence(Notification.EXTRA_TEXT);
325 
326             Notification.Builder nb = Notification.Builder.recoverBuilder(appContext, n);
327 
328             if (nb.getStyle() instanceof Notification.BigTextStyle) {
329                 text = ((Notification.BigTextStyle) nb.getStyle()).getBigText();
330             } else if (nb.getStyle() instanceof Notification.MessagingStyle) {
331                 Notification.MessagingStyle ms = (Notification.MessagingStyle) nb.getStyle();
332                 final List<Notification.MessagingStyle.Message> messages = ms.getMessages();
333                 if (messages != null && messages.size() > 0) {
334                     text = messages.get(messages.size() - 1).getText();
335                 }
336             }
337 
338             if (TextUtils.isEmpty(text)) {
339                 text = n.extras.getCharSequence(Notification.EXTRA_TEXT);
340             }
341         }
342         return text == null ? null : String.valueOf(text);
343     }
344 
loadIcon(HistoricalNotificationInfo info, StatusBarNotification sbn)345     private Drawable loadIcon(HistoricalNotificationInfo info, StatusBarNotification sbn) {
346         Drawable draw = sbn.getNotification().getSmallIcon().loadDrawableAsUser(
347                 sbn.getPackageContext(mContext), info.user);
348         if (draw == null) {
349             return null;
350         }
351         draw.mutate();
352         draw.setColorFilter(sbn.getNotification().color, PorterDuff.Mode.SRC_ATOP);
353         return draw;
354     }
355 
formatPendingIntent(PendingIntent pi)356     private static String formatPendingIntent(PendingIntent pi) {
357         final StringBuilder sb = new StringBuilder();
358         final IntentSender is = pi.getIntentSender();
359         sb.append("Intent(pkg=").append(is.getCreatorPackage());
360         try {
361             final boolean isActivity =
362                     ActivityManager.getService().isIntentSenderAnActivity(is.getTarget());
363             if (isActivity) sb.append(" (activity)");
364         } catch (RemoteException ex) {}
365         sb.append(")");
366         return sb.toString();
367     }
368 
369     /**
370      * Reads all current and past notifications (up to the system limit, since the device was
371      * booted), stores the data we need to present them, and sorts them chronologically for display.
372      */
loadNotifications()373     private void loadNotifications() {
374         try {
375             StatusBarNotification[] active = mNoMan.getActiveNotificationsWithAttribution(
376                     mContext.getPackageName(), mContext.getAttributionTag());
377             StatusBarNotification[] dismissed = mNoMan.getHistoricalNotificationsWithAttribution(
378                     mContext.getPackageName(), mContext.getAttributionTag(), 50, false);
379 
380             List<HistoricalNotificationInfo> list
381                     = new ArrayList<>(active.length + dismissed.length);
382 
383             for (StatusBarNotification[] resultSet
384                     : new StatusBarNotification[][] { active, dismissed }) {
385                 for (StatusBarNotification sbn : resultSet) {
386                     if (sbn.getNotification().isGroupSummary()) {
387                         continue;
388                     }
389                     final HistoricalNotificationInfo info = createFromSbn(sbn, resultSet == active);
390                     logd("   [%d] %s: %s", info.timestamp, info.pkg, info.title);
391                     list.add(info);
392                 }
393             }
394 
395             // notifications are given to us in the same order as the shade; sorted by inferred
396             // priority. Resort chronologically for our display.
397             list.sort(mNotificationSorter);
398             mNotificationInfos = new LinkedList<>(list);
399 
400         } catch (RemoteException e) {
401             Log.e(TAG, "Cannot load Notifications: ", e);
402         }
403     }
404 
createFromSbn(StatusBarNotification sbn, boolean active)405     private HistoricalNotificationInfo createFromSbn(StatusBarNotification sbn, boolean active) {
406         final Notification n = sbn.getNotification();
407         final HistoricalNotificationInfo info = new HistoricalNotificationInfo();
408         info.pkg = sbn.getPackageName();
409         info.user = sbn.getUserId() == UserHandle.USER_ALL
410                 ? UserHandle.USER_SYSTEM : sbn.getUserId();
411         info.badged = info.user != ActivityManager.getCurrentUser();
412         info.icon = loadIcon(info, sbn);
413         if (info.icon == null) {
414             info.icon = loadPackageIconDrawable(info.pkg, info.user);
415         }
416         info.pkgname = loadPackageName(info.pkg);
417         info.title = getTitleString(n);
418         info.text = getTextString(sbn.getPackageContext(mContext), n);
419         info.timestamp = sbn.getPostTime();
420         info.priority = n.priority;
421         info.key = sbn.getKey();
422         info.channelId = sbn.getNotification().getChannelId();
423 
424         info.active = active;
425         info.notificationExtra = generateExtraText(sbn, info);
426 
427         updateFromRanking(info);
428 
429         return info;
430     }
431 
updateFromRanking(HistoricalNotificationInfo info)432     private void updateFromRanking(HistoricalNotificationInfo info) {
433         Ranking rank = new Ranking();
434         if (mRanking == null) {
435             return;
436         }
437         mRanking.getRanking(info.key, rank);
438         info.alerted = rank.getLastAudiblyAlertedMillis() > 0;
439         info.visuallyInterruptive = rank.visuallyInterruptive();
440         info.channel = rank.getChannel();
441         info.rankingExtra = generateRankingExtraText(info);
442     }
443 
444     /**
445      * Generates a string of debug information for this notification based on the RankingMap
446      */
generateRankingExtraText(HistoricalNotificationInfo info)447     private CharSequence generateRankingExtraText(HistoricalNotificationInfo info) {
448         final SpannableStringBuilder sb = new SpannableStringBuilder();
449         final String delim = getString(R.string.notification_log_details_delimiter);
450 
451         Ranking rank = new Ranking();
452         if (mRanking != null && mRanking.getRanking(info.key, rank)) {
453             if (info.active && info.alerted) {
454                 sb.append("\n")
455                         .append(bold(getString(R.string.notification_log_details_alerted)));
456             }
457             sb.append("\n")
458                     .append(bold(getString(R.string.notification_log_channel)))
459                     .append(delim)
460                     .append(info.channel.toString());
461             sb.append("\n")
462                     .append(bold("getShortcutInfo"))
463                     .append(delim)
464                     .append(String.valueOf(rank.getShortcutInfo()));
465             sb.append("\n")
466                     .append(bold("isConversation"))
467                     .append(delim)
468                     .append(rank.isConversation() ? "true" : "false");
469             sb.append("\n")
470                     .append(bold("isBubble"))
471                     .append(delim)
472                     .append(rank.isBubble() ? "true" : "false");
473             if (info.active) {
474                 sb.append("\n")
475                         .append(bold(getString(
476                                 R.string.notification_log_details_importance)))
477                         .append(delim)
478                         .append(Ranking.importanceToString(rank.getImportance()));
479                 if (rank.getImportanceExplanation() != null) {
480                     sb.append("\n")
481                             .append(bold(getString(
482                                     R.string.notification_log_details_explanation)))
483                             .append(delim)
484                             .append(rank.getImportanceExplanation());
485                 }
486                 sb.append("\n")
487                         .append(bold(getString(
488                                 R.string.notification_log_details_badge)))
489                         .append(delim)
490                         .append(Boolean.toString(rank.canShowBadge()));
491             }
492         } else {
493             if (mRanking == null) {
494                 sb.append("\n")
495                         .append(bold(getString(
496                                 R.string.notification_log_details_ranking_null)));
497             } else {
498                 sb.append("\n")
499                         .append(bold(getString(
500                                 R.string.notification_log_details_ranking_none)));
501             }
502         }
503 
504         return sb;
505     }
506 
507     /**
508      * Generates a string of debug information for this notification
509      */
generateExtraText(StatusBarNotification sbn, HistoricalNotificationInfo info)510     private CharSequence generateExtraText(StatusBarNotification sbn,
511                                            HistoricalNotificationInfo info) {
512         final Notification n = sbn.getNotification();
513         final SpannableStringBuilder sb = new SpannableStringBuilder();
514         final String delim = getString(R.string.notification_log_details_delimiter);
515         sb.append(bold(getString(R.string.notification_log_details_package)))
516                 .append(delim)
517                 .append(info.pkg)
518                 .append("\n")
519                 .append(bold(getString(R.string.notification_log_details_key)))
520                 .append(delim)
521                 .append(sbn.getKey());
522         sb.append("\n")
523                 .append(bold(getString(R.string.notification_log_details_icon)))
524                 .append(delim)
525                 .append(String.valueOf(n.getSmallIcon()));
526         sb.append("\n")
527                 .append(bold("postTime"))
528                 .append(delim)
529                 .append(String.valueOf(sbn.getPostTime()));
530         if (n.getTimeoutAfter() != 0) {
531             sb.append("\n")
532                     .append(bold("timeoutAfter"))
533                     .append(delim)
534                     .append(String.valueOf(n.getTimeoutAfter()));
535         }
536         if (sbn.isGroup()) {
537             sb.append("\n")
538                     .append(bold(getString(R.string.notification_log_details_group)))
539                     .append(delim)
540                     .append(String.valueOf(sbn.getGroupKey()));
541             if (n.isGroupSummary()) {
542                 sb.append(bold(
543                         getString(R.string.notification_log_details_group_summary)));
544             }
545         }
546         if (n.publicVersion != null) {
547             sb.append("\n")
548                     .append(bold(getString(
549                             R.string.notification_log_details_public_version)))
550                     .append(delim)
551                     .append(getTitleString(n.publicVersion));
552         }
553 
554         if (n.contentIntent != null) {
555             sb.append("\n")
556                     .append(bold(getString(
557                             R.string.notification_log_details_content_intent)))
558                     .append(delim)
559                     .append(formatPendingIntent(n.contentIntent));
560         }
561         if (n.deleteIntent != null) {
562             sb.append("\n")
563                     .append(bold(getString(
564                             R.string.notification_log_details_delete_intent)))
565                     .append(delim)
566                     .append(formatPendingIntent(n.deleteIntent));
567         }
568         if (n.fullScreenIntent != null) {
569             sb.append("\n")
570                     .append(bold(getString(
571                             R.string.notification_log_details_full_screen_intent)))
572                     .append(delim)
573                     .append(formatPendingIntent(n.fullScreenIntent));
574         }
575         if (n.actions != null && n.actions.length > 0) {
576             sb.append("\n")
577                     .append(bold(getString(R.string.notification_log_details_actions)));
578             for (int ai=0; ai<n.actions.length; ai++) {
579                 final Notification.Action action = n.actions[ai];
580                 sb.append("\n  ").append(String.valueOf(ai)).append(' ')
581                         .append(bold(getString(
582                                 R.string.notification_log_details_title)))
583                         .append(delim)
584                         .append(action.title);
585                 if (action.actionIntent != null) {
586                     sb.append("\n    ")
587                             .append(bold(getString(
588                                     R.string.notification_log_details_content_intent)))
589                             .append(delim)
590                             .append(formatPendingIntent(action.actionIntent));
591                 }
592                 if (action.getRemoteInputs() != null) {
593                     sb.append("\n    ")
594                             .append(bold(getString(
595                                     R.string.notification_log_details_remoteinput)))
596                             .append(delim)
597                             .append(String.valueOf(action.getRemoteInputs().length));
598                 }
599             }
600         }
601         if (n.contentView != null) {
602             sb.append("\n")
603                     .append(bold(getString(
604                             R.string.notification_log_details_content_view)))
605                     .append(delim)
606                     .append(n.contentView.toString());
607         }
608         if (n.getBubbleMetadata() != null) {
609             sb.append("\n")
610                     .append(bold("bubbleMetadata"))
611                     .append(delim)
612                     .append(String.valueOf(n.getBubbleMetadata()));
613         }
614         if (n.getShortcutId() != null) {
615             sb.append("\n")
616                     .append(bold("shortcutId"))
617                     .append(delim)
618                     .append(String.valueOf(n.getShortcutId()));
619         }
620 
621         if (DUMP_EXTRAS) {
622             if (n.extras != null && n.extras.size() > 0) {
623                 sb.append("\n")
624                         .append(bold(getString(
625                                 R.string.notification_log_details_extras)));
626                 for (String extraKey : n.extras.keySet()) {
627                     String val = String.valueOf(n.extras.get(extraKey));
628                     if (val.length() > 100) val = val.substring(0, 100) + "...";
629                     sb.append("\n  ").append(extraKey).append(delim).append(val);
630                 }
631             }
632         }
633         if (DUMP_PARCEL) {
634             final Parcel p = Parcel.obtain();
635             n.writeToParcel(p, 0);
636             sb.append("\n")
637                     .append(bold(getString(R.string.notification_log_details_parcel)))
638                     .append(delim)
639                     .append(String.valueOf(p.dataPosition()))
640                     .append(' ')
641                     .append(bold(getString(R.string.notification_log_details_ashmem)))
642                     .append(delim)
643                     .append(String.valueOf(p.getBlobAshmemSize()))
644                     .append("\n");
645         }
646         return sb;
647     }
648 
loadPackageIconDrawable(String pkg, int userId)649     private Drawable loadPackageIconDrawable(String pkg, int userId) {
650         Drawable icon = null;
651         try {
652             icon = mPm.getApplicationIcon(pkg);
653         } catch (PackageManager.NameNotFoundException e) {
654             Log.e(TAG, "Cannot get application icon", e);
655         }
656 
657         return icon;
658     }
659 
loadPackageName(String pkg)660     private CharSequence loadPackageName(String pkg) {
661         try {
662             ApplicationInfo info = mPm.getApplicationInfo(pkg,
663                     PackageManager.MATCH_ANY_USER);
664             if (info != null) return mPm.getApplicationLabel(info);
665         } catch (PackageManager.NameNotFoundException e) {
666             Log.e(TAG, "Cannot load package name", e);
667         }
668         return pkg;
669     }
670 
671     private static class HistoricalNotificationPreference extends Preference {
672         private final HistoricalNotificationInfo mInfo;
673         private static long sLastExpandedTimestamp; // quick hack to keep things from collapsing
674         public ViewGroup mItemView; // hack to update prefs fast;
675         private Context mContext;
676 
HistoricalNotificationPreference(Context context, HistoricalNotificationInfo info, int order)677         public HistoricalNotificationPreference(Context context, HistoricalNotificationInfo info,
678                 int order) {
679             super(context);
680             setLayoutResource(R.layout.notification_log_row);
681             setOrder(order);
682             setKey(info.key);
683             mInfo = info;
684             mContext = context;
685         }
686 
687         @Override
onBindViewHolder(PreferenceViewHolder row)688         public void onBindViewHolder(PreferenceViewHolder row) {
689             super.onBindViewHolder(row);
690 
691             mItemView = (ViewGroup) row.itemView;
692 
693             updatePreference(mInfo);
694 
695             row.findViewById(R.id.timestamp).setOnLongClickListener(v -> {
696                 final View extras = row.findViewById(R.id.extra);
697                 extras.setVisibility(extras.getVisibility() == View.VISIBLE
698                         ? View.GONE : View.VISIBLE);
699                 sLastExpandedTimestamp = mInfo.timestamp;
700                 return false;
701             });
702         }
703 
updatePreference(HistoricalNotificationInfo info)704         public void updatePreference(HistoricalNotificationInfo info) {
705             if (mItemView == null) {
706                 return;
707             }
708             if (info.icon != null) {
709                 ((ImageView) mItemView.findViewById(R.id.icon)).setImageDrawable(mInfo.icon);
710             }
711             ((TextView) mItemView.findViewById(R.id.pkgname)).setText(mInfo.pkgname);
712             ((DateTimeView) mItemView.findViewById(R.id.timestamp)).setTime(info.timestamp);
713             if (!TextUtils.isEmpty(info.title)) {
714                 ((TextView) mItemView.findViewById(R.id.title)).setText(info.title);
715                 mItemView.findViewById(R.id.title).setVisibility(View.VISIBLE);
716             } else {
717                 mItemView.findViewById(R.id.title).setVisibility(View.GONE);
718             }
719             if (!TextUtils.isEmpty(info.text)) {
720                 ((TextView) mItemView.findViewById(R.id.text)).setText(info.text);
721                 mItemView.findViewById(R.id.text).setVisibility(View.VISIBLE);
722             } else {
723                 mItemView.findViewById(R.id.text).setVisibility(View.GONE);
724             }
725             if (info.icon != null) {
726                 ((ImageView) mItemView.findViewById(R.id.icon)).setImageDrawable(info.icon);
727             }
728 
729             ImageView profileBadge = mItemView.findViewById(R.id.profile_badge);
730             Drawable profile = mContext.getPackageManager().getUserBadgeForDensity(
731                     UserHandle.of(info.user), -1);
732             profileBadge.setImageDrawable(profile);
733             profileBadge.setVisibility(info.badged ? View.VISIBLE : View.GONE);
734 
735             ((DateTimeView) mItemView.findViewById(R.id.timestamp)).setTime(mInfo.timestamp);
736 
737             ((TextView) mItemView.findViewById(R.id.notification_extra))
738                     .setText(mInfo.notificationExtra);
739             ((TextView) mItemView.findViewById(R.id.ranking_extra))
740                     .setText(mInfo.rankingExtra);
741 
742             mItemView.findViewById(R.id.extra).setVisibility(
743                     mInfo.timestamp == sLastExpandedTimestamp ? View.VISIBLE : View.GONE);
744 
745             mItemView.setAlpha(mInfo.active ? 1.0f : 0.5f);
746 
747             mItemView.findViewById(R.id.alerted_icon).setVisibility(
748                     mInfo.alerted ? View.VISIBLE : View.GONE);
749         }
750 
751         @Override
performClick()752         public void performClick() {
753             Intent intent =  new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
754                     .putExtra(EXTRA_APP_PACKAGE, mInfo.pkg)
755                     .putExtra(EXTRA_CHANNEL_ID,
756                             mInfo.channel != null ? mInfo.channel.getId() : mInfo.channelId);
757             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
758             getContext().startActivity(intent);
759         }
760     }
761 }
762