NotificationStation.java revision 7a038ebb1cebcf60c54ff588770d882775abb4df
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.Intent;
24import android.content.IntentSender;
25import android.content.pm.ApplicationInfo;
26import android.content.pm.PackageManager;
27import android.content.res.Resources;
28import android.graphics.Typeface;
29import android.graphics.drawable.Drawable;
30import android.net.Uri;
31import android.os.*;
32import android.service.notification.NotificationListenerService;
33import android.service.notification.NotificationListenerService.Ranking;
34import android.service.notification.NotificationListenerService.RankingMap;
35import android.service.notification.StatusBarNotification;
36import android.support.v7.preference.Preference;
37import android.support.v7.preference.PreferenceViewHolder;
38import android.support.v7.widget.RecyclerView;
39import android.text.Spannable;
40import android.text.SpannableString;
41import android.text.SpannableStringBuilder;
42import android.text.TextUtils;
43import android.text.style.StyleSpan;
44import android.util.Log;
45import android.view.View;
46import android.widget.DateTimeView;
47import android.widget.ImageView;
48import android.widget.TextView;
49
50import com.android.internal.logging.MetricsProto.MetricsEvent;
51import com.android.settings.CopyablePreference;
52import com.android.settings.R;
53import com.android.settings.SettingsPreferenceFragment;
54import com.android.settings.Utils;
55
56import java.lang.StringBuilder;
57import java.util.*;
58
59public class NotificationStation extends SettingsPreferenceFragment {
60    private static final String TAG = NotificationStation.class.getSimpleName();
61
62    private static final boolean DEBUG = false;
63    private static final boolean DUMP_EXTRAS = true;
64    private static final boolean DUMP_PARCEL = true;
65
66    private static class HistoricalNotificationInfo {
67        public String pkg;
68        public Drawable pkgicon;
69        public CharSequence pkgname;
70        public Drawable icon;
71        public CharSequence title;
72        public int priority;
73        public int user;
74        public long timestamp;
75        public boolean active;
76        public CharSequence extra;
77    }
78
79    private PackageManager mPm;
80    private INotificationManager mNoMan;
81    private RankingMap mRanking;
82
83    private Runnable mRefreshListRunnable = new Runnable() {
84        @Override
85        public void run() {
86            refreshList();
87        }
88    };
89
90    private NotificationListenerService mListener = new NotificationListenerService() {
91        @Override
92        public void onNotificationPosted(StatusBarNotification sbn, RankingMap ranking) {
93            logd("onNotificationPosted: %s", sbn.getNotification());
94            final Handler h = getListView().getHandler();
95            mRanking = ranking;
96            h.removeCallbacks(mRefreshListRunnable);
97            h.postDelayed(mRefreshListRunnable, 100);
98        }
99
100        @Override
101        public void onNotificationRemoved(StatusBarNotification notification, RankingMap ranking) {
102            final Handler h = getListView().getHandler();
103            mRanking = ranking;
104            h.removeCallbacks(mRefreshListRunnable);
105            h.postDelayed(mRefreshListRunnable, 100);
106        }
107
108        @Override
109        public void onNotificationRankingUpdate(RankingMap ranking) {
110            mRanking = ranking;
111        }
112    };
113
114    private Context mContext;
115
116    private final Comparator<HistoricalNotificationInfo> mNotificationSorter
117            = new Comparator<HistoricalNotificationInfo>() {
118                @Override
119                public int compare(HistoricalNotificationInfo lhs,
120                                   HistoricalNotificationInfo rhs) {
121                    return (int)(rhs.timestamp - lhs.timestamp);
122                }
123            };
124
125    @Override
126    public void onAttach(Activity activity) {
127        logd("onAttach(%s)", activity.getClass().getSimpleName());
128        super.onAttach(activity);
129        mContext = activity;
130        mPm = mContext.getPackageManager();
131        mNoMan = INotificationManager.Stub.asInterface(
132                ServiceManager.getService(Context.NOTIFICATION_SERVICE));
133        try {
134            mListener.registerAsSystemService(mContext, new ComponentName(mContext.getPackageName(),
135                    this.getClass().getCanonicalName()), ActivityManager.getCurrentUser());
136        } catch (RemoteException e) {
137            Log.e(TAG, "Cannot register listener", e);
138        }
139    }
140
141    @Override
142    public void onDetach() {
143        try {
144            mListener.unregisterAsSystemService();
145        } catch (RemoteException e) {
146            Log.e(TAG, "Cannot unregister listener", e);
147        }
148        super.onDetach();
149    }
150
151    @Override
152    protected int getMetricsCategory() {
153        return MetricsEvent.NOTIFICATION_STATION;
154    }
155
156    @Override
157    public void onActivityCreated(Bundle savedInstanceState) {
158        logd("onActivityCreated(%s)", savedInstanceState);
159        super.onActivityCreated(savedInstanceState);
160
161        RecyclerView listView = getListView();
162        Utils.forceCustomPadding(listView, false /* non additive padding */);
163    }
164
165    @Override
166    public void onResume() {
167        logd("onResume()");
168        super.onResume();
169        refreshList();
170    }
171
172    private void refreshList() {
173        List<HistoricalNotificationInfo> infos = loadNotifications();
174        if (infos != null) {
175            final int N = infos.size();
176            logd("adding %d infos", N);
177            Collections.sort(infos, mNotificationSorter);
178            if (getPreferenceScreen() == null) {
179                setPreferenceScreen(getPreferenceManager().createPreferenceScreen(getContext()));
180            }
181            getPreferenceScreen().removeAll();
182            for (int i = 0; i < N; i++) {
183                getPreferenceScreen().addPreference(
184                        new HistoricalNotificationPreference(getPrefContext(), infos.get(i)));
185            }
186        }
187    }
188
189    private static void logd(String msg, Object... args) {
190        if (DEBUG) {
191            Log.d(TAG, args == null || args.length == 0 ? msg : String.format(msg, args));
192        }
193    }
194
195    private static CharSequence bold(CharSequence cs) {
196        if (cs.length() == 0) return cs;
197        SpannableString ss = new SpannableString(cs);
198        ss.setSpan(new StyleSpan(Typeface.BOLD), 0, cs.length(), 0);
199        return ss;
200    }
201
202    private static String getTitleString(Notification n) {
203        String title = null;
204        if (n.extras != null) {
205            title = n.extras.getString(Notification.EXTRA_TITLE);
206            if (TextUtils.isEmpty(title)) {
207                title = n.extras.getString(Notification.EXTRA_TEXT);
208            }
209        }
210        if (TextUtils.isEmpty(title) && !TextUtils.isEmpty(n.tickerText)) {
211            title = n.tickerText.toString();
212        }
213        return title;
214    }
215
216    private static String formatPendingIntent(PendingIntent pi) {
217        final StringBuilder sb = new StringBuilder();
218        final IntentSender is = pi.getIntentSender();
219        sb.append("Intent(pkg=").append(is.getCreatorPackage());
220        try {
221            final boolean isActivity =
222                    ActivityManagerNative.getDefault().isIntentSenderAnActivity(is.getTarget());
223            if (isActivity) sb.append(" (activity)");
224        } catch (RemoteException ex) {}
225        sb.append(")");
226        return sb.toString();
227    }
228
229    private List<HistoricalNotificationInfo> loadNotifications() {
230        final int currentUserId = ActivityManager.getCurrentUser();
231        try {
232            StatusBarNotification[] active = mNoMan.getActiveNotifications(
233                    mContext.getPackageName());
234            StatusBarNotification[] dismissed = mNoMan.getHistoricalNotifications(
235                    mContext.getPackageName(), 50);
236
237            List<HistoricalNotificationInfo> list
238                    = new ArrayList<HistoricalNotificationInfo>(active.length + dismissed.length);
239
240            final Ranking rank = new Ranking();
241
242            for (StatusBarNotification[] resultset
243                    : new StatusBarNotification[][] { active, dismissed }) {
244                for (StatusBarNotification sbn : resultset) {
245                    if (sbn.getUserId() != UserHandle.USER_ALL & sbn.getUserId() != currentUserId) {
246                        continue;
247                    }
248
249                    final Notification n = sbn.getNotification();
250                    final HistoricalNotificationInfo info = new HistoricalNotificationInfo();
251                    info.pkg = sbn.getPackageName();
252                    info.user = sbn.getUserId();
253                    info.icon = loadIconDrawable(info.pkg, info.user, n.icon);
254                    info.pkgicon = loadPackageIconDrawable(info.pkg, info.user);
255                    info.pkgname = loadPackageName(info.pkg);
256                    info.title = getTitleString(n);
257                    if (TextUtils.isEmpty(info.title)) {
258                        info.title = getString(R.string.notification_log_no_title);
259                    }
260                    info.timestamp = sbn.getPostTime();
261                    info.priority = n.priority;
262
263                    info.active = (resultset == active);
264
265                    final SpannableStringBuilder sb = new SpannableStringBuilder();
266                    final String delim = getString(R.string.notification_log_details_delimiter);
267                    sb.append(bold(getString(R.string.notification_log_details_package)))
268                            .append(delim)
269                            .append(info.pkg)
270                            .append("\n")
271                            .append(bold(getString(R.string.notification_log_details_key)))
272                            .append(delim)
273                            .append(sbn.getKey());
274                    sb.append("\n")
275                            .append(bold(getString(R.string.notification_log_details_icon)))
276                            .append(delim)
277                            .append(n.getSmallIcon().toString());
278                    if (!TextUtils.isEmpty(n.getGroup())) {
279                        sb.append("\n")
280                                .append(bold(getString(R.string.notification_log_details_group)))
281                                .append(delim)
282                                .append(n.getGroup());
283                        if (n.isGroupSummary()) {
284                            sb.append(bold(
285                                    getString(R.string.notification_log_details_group_summary)));
286                        }
287                    }
288                    sb.append("\n")
289                            .append(bold(getString(R.string.notification_log_details_sound)))
290                            .append(delim);
291                    if (0 != (n.defaults & Notification.DEFAULT_SOUND)) {
292                        sb.append(getString(R.string.notification_log_details_default));
293                    } else if (n.sound != null) {
294                        sb.append(n.sound.toString());
295                    } else {
296                        sb.append(getString(R.string.notification_log_details_none));
297                    }
298                    sb.append("\n")
299                            .append(bold(getString(R.string.notification_log_details_vibrate)))
300                            .append(delim);
301                    if (0 != (n.defaults & Notification.DEFAULT_VIBRATE)) {
302                        sb.append(getString(R.string.notification_log_details_default));
303                    } else if (n.vibrate != null) {
304                        for (int vi=0;vi<n.vibrate.length;vi++) {
305                            if (vi > 0) sb.append(',');
306                            sb.append(String.valueOf(n.vibrate[vi]));
307                        }
308                    } else {
309                        sb.append(getString(R.string.notification_log_details_none));
310                    }
311                    sb.append("\n")
312                            .append(bold(getString(R.string.notification_log_details_visibility)))
313                            .append(delim)
314                            .append(Notification.visibilityToString(n.visibility));
315                    if (n.publicVersion != null) {
316                        sb.append("\n")
317                                .append(bold(getString(
318                                        R.string.notification_log_details_public_version)))
319                                .append(delim)
320                                .append(getTitleString(n.publicVersion));
321                    }
322                    sb.append("\n")
323                            .append(bold(getString(R.string.notification_log_details_priority)))
324                            .append(delim)
325                            .append(Notification.priorityToString(n.priority));
326                    if (mRanking != null && mRanking.getRanking(sbn.getKey(), rank)) {
327                        sb.append("\n")
328                                .append(bold(getString(
329                                        R.string.notification_log_details_importance)))
330                                .append(delim)
331                                .append(Ranking.importanceToString(rank.getImportance()));
332                        if (rank.getImportanceExplanation() != null) {
333                            sb.append("\n")
334                                    .append(bold(getString(
335                                            R.string.notification_log_details_explanation)))
336                                    .append(delim)
337                                    .append(rank.getImportanceExplanation());
338                        }
339                    }
340                    if (n.contentIntent != null) {
341                        sb.append("\n")
342                                .append(bold(getString(
343                                        R.string.notification_log_details_content_intent)))
344                                .append(delim)
345                                .append(formatPendingIntent(n.contentIntent));
346                    }
347                    if (n.deleteIntent != null) {
348                        sb.append("\n")
349                                .append(bold(getString(
350                                        R.string.notification_log_details_delete_intent)))
351                                .append(delim)
352                                .append(formatPendingIntent(n.deleteIntent));
353                    }
354                    if (n.fullScreenIntent != null) {
355                        sb.append("\n")
356                                .append(bold(getString(
357                                        R.string.notification_log_details_full_screen_intent)))
358                                .append(delim)
359                                .append(formatPendingIntent(n.fullScreenIntent));
360                    }
361                    if (n.actions != null && n.actions.length > 0) {
362                        sb.append("\n")
363                                .append(bold(getString(R.string.notification_log_details_actions)));
364                        for (int ai=0; ai<n.actions.length; ai++) {
365                            final Notification.Action action = n.actions[ai];
366                            sb.append("\n  ").append(String.valueOf(ai)).append(' ')
367                                    .append(bold(getString(
368                                            R.string.notification_log_details_title)))
369                                    .append(delim)
370                                    .append(action.title)
371                                    .append("\n    ")
372                                    .append(bold(getString(
373                                            R.string.notification_log_details_content_intent)))
374                                    .append(delim)
375                                    .append(formatPendingIntent(action.actionIntent));
376                            if (action.getRemoteInputs() != null) {
377                                sb.append(' ')
378                                        .append(bold(getString(
379                                                R.string.notification_log_details_remoteinput)))
380                                        .append(delim)
381                                        .append(String.valueOf(action.getRemoteInputs().length));
382                            }
383                        }
384                    }
385                    if (n.contentView != null) {
386                        sb.append("\n")
387                                .append(bold(getString(
388                                        R.string.notification_log_details_content_view)))
389                                .append(delim)
390                                .append(n.contentView.toString());
391                    }
392
393                    if (DUMP_EXTRAS) {
394                        if (n.extras != null && n.extras.size() > 0) {
395                            sb.append("\n")
396                                    .append(bold(getString(
397                                            R.string.notification_log_details_extras)));
398                            for (String extraKey : n.extras.keySet()) {
399                                String val = String.valueOf(n.extras.get(extraKey));
400                                if (val.length() > 100) val = val.substring(0, 100) + "...";
401                                sb.append("\n  ").append(extraKey).append(delim).append(val);
402                            }
403                        }
404                    }
405                    if (DUMP_PARCEL) {
406                        final Parcel p = Parcel.obtain();
407                        n.writeToParcel(p, 0);
408                        sb.append("\n")
409                                .append(bold(getString(R.string.notification_log_details_parcel)))
410                                .append(delim)
411                                .append(String.valueOf(p.dataPosition()))
412                                .append(' ')
413                                .append(bold(getString(R.string.notification_log_details_ashmem)))
414                                .append(delim)
415                                .append(String.valueOf(p.getBlobAshmemSize()))
416                                .append("\n");
417                    }
418
419                    info.extra = sb;
420
421                    logd("   [%d] %s: %s", info.timestamp, info.pkg, info.title);
422                    list.add(info);
423                }
424            }
425
426            return list;
427        } catch (RemoteException e) {
428            Log.e(TAG, "Cannot load Notifications: ", e);
429        }
430        return null;
431    }
432
433    private Resources getResourcesForUserPackage(String pkg, int userId) {
434        Resources r = null;
435
436        if (pkg != null) {
437            try {
438                if (userId == UserHandle.USER_ALL) {
439                    userId = UserHandle.USER_SYSTEM;
440                }
441                r = mPm.getResourcesForApplicationAsUser(pkg, userId);
442            } catch (PackageManager.NameNotFoundException ex) {
443                Log.e(TAG, "Icon package not found: " + pkg, ex);
444                return null;
445            }
446        } else {
447            r = mContext.getResources();
448        }
449        return r;
450    }
451
452    private Drawable loadPackageIconDrawable(String pkg, int userId) {
453        Drawable icon = null;
454        try {
455            icon = mPm.getApplicationIcon(pkg);
456        } catch (PackageManager.NameNotFoundException e) {
457            Log.e(TAG, "Cannot get application icon", e);
458        }
459
460        return icon;
461    }
462
463    private CharSequence loadPackageName(String pkg) {
464        try {
465            ApplicationInfo info = mPm.getApplicationInfo(pkg,
466                    PackageManager.GET_UNINSTALLED_PACKAGES);
467            if (info != null) return mPm.getApplicationLabel(info);
468        } catch (PackageManager.NameNotFoundException e) {
469            Log.e(TAG, "Cannot load package name", e);
470        }
471        return pkg;
472    }
473
474    private Drawable loadIconDrawable(String pkg, int userId, int resId) {
475        Resources r = getResourcesForUserPackage(pkg, userId);
476
477        if (resId == 0) {
478            return null;
479        }
480
481        try {
482            return r.getDrawable(resId, null);
483        } catch (RuntimeException e) {
484            Log.w(TAG, "Icon not found in "
485                    + (pkg != null ? resId : "<system>")
486                    + ": " + Integer.toHexString(resId), e);
487        }
488
489        return null;
490    }
491
492    private static class HistoricalNotificationPreference extends CopyablePreference {
493        private final HistoricalNotificationInfo mInfo;
494
495        public HistoricalNotificationPreference(Context context, HistoricalNotificationInfo info) {
496            super(context);
497            setLayoutResource(R.layout.notification_log_row);
498            mInfo = info;
499        }
500
501        @Override
502        public void onBindViewHolder(PreferenceViewHolder row) {
503            super.onBindViewHolder(row);
504
505            if (mInfo.icon != null) {
506                ((ImageView) row.findViewById(R.id.icon)).setImageDrawable(mInfo.icon);
507            }
508            if (mInfo.pkgicon != null) {
509                ((ImageView) row.findViewById(R.id.pkgicon)).setImageDrawable(mInfo.pkgicon);
510            }
511
512            ((DateTimeView) row.findViewById(R.id.timestamp)).setTime(mInfo.timestamp);
513            ((TextView) row.findViewById(R.id.title)).setText(mInfo.title);
514            ((TextView) row.findViewById(R.id.pkgname)).setText(mInfo.pkgname);
515
516            final TextView extra = (TextView) row.findViewById(R.id.extra);
517            extra.setText(mInfo.extra);
518            extra.setVisibility(View.GONE);
519
520            row.itemView.setOnClickListener(
521                    new View.OnClickListener() {
522                        @Override
523                        public void onClick(View view) {
524                            extra.setVisibility(extra.getVisibility() == View.VISIBLE
525                                    ? View.GONE : View.VISIBLE);
526                        }
527                    });
528
529            row.itemView.setAlpha(mInfo.active ? 1.0f : 0.5f);
530        }
531
532        @Override
533        public CharSequence getCopyableText() {
534            return new SpannableStringBuilder(mInfo.title)
535                    .append(" [").append(new Date(mInfo.timestamp).toString())
536                    .append("]\n").append(mInfo.pkgname)
537                    .append("\n").append(mInfo.extra);
538        }
539
540        @Override
541        public void performClick() {
542//            Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
543//                    Uri.fromParts("package", mInfo.pkg, null));
544//            intent.setComponent(intent.resolveActivity(getContext().getPackageManager()));
545//            getContext().startActivity(intent);
546        }
547    }
548}
549