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
17package com.android.settings.notification;
18
19import android.app.*;
20import android.app.INotificationManager;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.IntentSender;
24import android.content.pm.ApplicationInfo;
25import android.content.pm.PackageManager;
26import android.content.res.Resources;
27import android.graphics.Typeface;
28import android.graphics.drawable.Drawable;
29import android.os.*;
30import android.service.notification.NotificationListenerService;
31import android.service.notification.NotificationListenerService.Ranking;
32import android.service.notification.NotificationListenerService.RankingMap;
33import android.service.notification.StatusBarNotification;
34import android.support.v7.preference.Preference;
35import android.support.v7.preference.PreferenceViewHolder;
36import android.support.v7.widget.RecyclerView;
37import android.text.SpannableString;
38import android.text.SpannableStringBuilder;
39import android.text.TextUtils;
40import android.text.style.StyleSpan;
41import android.util.Log;
42import android.view.View;
43import android.widget.DateTimeView;
44import android.widget.ImageView;
45import android.widget.TextView;
46
47import com.android.internal.logging.MetricsProto.MetricsEvent;
48import com.android.settings.CopyablePreference;
49import com.android.settings.R;
50import com.android.settings.SettingsPreferenceFragment;
51import com.android.settings.Utils;
52
53import java.lang.StringBuilder;
54import java.util.*;
55
56public 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
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
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
152    public void onDetach() {
153        logd("onDetach()");
154        mHandler.removeCallbacks(mRefreshListRunnable);
155        mHandler = null;
156        super.onDetach();
157    }
158
159    @Override
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
170    protected int getMetricsCategory() {
171        return MetricsEvent.NOTIFICATION_STATION;
172    }
173
174    @Override
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
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
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
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
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
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
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
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
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
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
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
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
534        public HistoricalNotificationPreference(Context context, HistoricalNotificationInfo info) {
535            super(context);
536            setLayoutResource(R.layout.notification_log_row);
537            mInfo = info;
538        }
539
540        @Override
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
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
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