Merge "Enhanced notification details." into nyc-dev
diff --git a/res/layout/notification_log_row.xml b/res/layout/notification_log_row.xml
index d7f57c6..e76835e 100644
--- a/res/layout/notification_log_row.xml
+++ b/res/layout/notification_log_row.xml
@@ -27,11 +27,11 @@
     <RelativeLayout
         android:layout_width="match_parent"
         android:layout_height="@*android:dimen/status_bar_icon_size"
-        android:layout_marginBottom="4dp"
+        android:layout_marginBottom="6dp"
         >
 
         <ImageView
-            android:id="@android:id/icon"
+            android:id="@+id/icon"
             android:layout_width="@*android:dimen/status_bar_icon_size"
             android:layout_height="@*android:dimen/status_bar_icon_size"
             android:layout_centerVertical="true"
@@ -40,17 +40,18 @@
             android:layout_marginEnd="8dp"
             android:contentDescription="@null"
             android:adjustViewBounds="true"
+            android:tint="?android:attr/textColorPrimary"
             android:maxHeight="@*android:dimen/status_bar_icon_size"
             android:maxWidth="@*android:dimen/status_bar_icon_size"
             android:scaleType="fitCenter" />
 
         <TextView
-            android:id="@android:id/title"
+            android:id="@+id/title"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_centerVertical="true"
             android:layout_toStartOf="@+id/timestamp"
-            android:layout_toEndOf="@android:id/icon"
+            android:layout_toEndOf="@id/icon"
             android:ellipsize="end"
             android:singleLine="true"
             android:textColor="?android:attr/textColorPrimary"
@@ -75,24 +76,12 @@
             />
     </RelativeLayout>
 
-    <TextView
-        android:id="@+id/extra"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal"
-        android:layout_marginStart="30dp"
-        android:ellipsize="end"
-        android:singleLine="true"
-        android:textColor="?android:attr/textColorPrimary"
-        android:textAppearance="?android:attr/textAppearanceSmall"
-        android:textAlignment="viewStart"
-        />
-
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="@*android:dimen/status_bar_icon_size"
         android:orientation="horizontal"
         android:layout_marginStart="30dp"
+        android:layout_marginBottom="6dp"
         >
 
         <ImageView
@@ -120,4 +109,19 @@
             />
 
     </LinearLayout>
+
+    <TextView
+            android:id="@+id/extra"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:layout_marginStart="30dp"
+            android:layout_marginBottom="6dp"
+            android:singleLine="false"
+            android:textColor="?android:attr/textColorPrimary"
+            android:textSize="10sp"
+            android:fontFamily="monospace"
+            android:textAlignment="viewStart"
+    />
+
 </LinearLayout>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 8d48bbf..19a4218 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7180,4 +7180,56 @@
         <item>@*android:string/gigabyteShort</item>
     </string-array>
 
+    <!-- Notification log debug tool: missing title -->
+    <string name="notification_log_no_title">(none)</string>
+    <!-- Notification log debug tool: delimiter between header and field data -->
+    <string name="notification_log_details_delimiter">": "</string>
+    <!-- Notification log debug tool: header: package name -->
+    <string name="notification_log_details_package">pkg</string>
+    <!-- Notification log debug tool: header: notification key -->
+    <string name="notification_log_details_key">key</string>
+    <!-- Notification log debug tool: header: notification group -->
+    <string name="notification_log_details_group">group</string>
+    <!-- Notification log debug tool: header: notification group summary suffix -->
+    <string name="notification_log_details_group_summary"> (summary)</string>
+    <!-- Notification log debug tool: header: notification visibility -->
+    <string name="notification_log_details_visibility">visibility</string>
+    <!-- Notification log debug tool: header: notification public version -->
+    <string name="notification_log_details_public_version">publicVersion</string>
+    <!-- Notification log debug tool: header: notification priority -->
+    <string name="notification_log_details_priority">priority</string>
+    <!-- Notification log debug tool: header: notification importance -->
+    <string name="notification_log_details_importance">importance</string>
+    <!-- Notification log debug tool: header: notification importance explanation -->
+    <string name="notification_log_details_explanation">explanation</string>
+    <!-- Notification log debug tool: header: notification contentIntent field -->
+    <string name="notification_log_details_content_intent">intent</string>
+    <!-- Notification log debug tool: header: notification deleteIntent field -->
+    <string name="notification_log_details_delete_intent">delete intent</string>
+    <!-- Notification log debug tool: header: notification fullScreenIntent field -->
+    <string name="notification_log_details_full_screen_intent">full screen intent</string>
+    <!-- Notification log debug tool: header: notification actions list -->
+    <string name="notification_log_details_actions">actions</string>
+    <!-- Notification log debug tool: header: title -->
+    <string name="notification_log_details_title">title</string>
+    <!-- Notification log debug tool: header: notification action remoteinput -->
+    <string name="notification_log_details_remoteinput">remote inputs</string>
+    <!-- Notification log debug tool: header: notification contentView -->
+    <string name="notification_log_details_content_view">custom view</string>
+    <!-- Notification log debug tool: header: notification extras -->
+    <string name="notification_log_details_extras">extras</string>
+    <!-- Notification log debug tool: header: notification icon -->
+    <string name="notification_log_details_icon">icon</string>
+    <!-- Notification log debug tool: header: notification size -->
+    <string name="notification_log_details_parcel">parcel size</string>
+    <!-- Notification log debug tool: notification ashmem size -->
+    <string name="notification_log_details_ashmem">ashmem</string>
+    <!-- Notification log debug tool: header: notification sound info -->
+    <string name="notification_log_details_sound">sound</string>
+    <!-- Notification log debug tool: header: notification vibration info -->
+    <string name="notification_log_details_vibrate">vibrate</string>
+    <!-- Notification log debug tool: the word 'default' -->
+    <string name="notification_log_details_default">default</string>
+    <!-- Notification log debug tool: the word 'none' -->
+    <string name="notification_log_details_none">none</string>
 </resources>
diff --git a/src/com/android/settings/CopyablePreference.java b/src/com/android/settings/CopyablePreference.java
index b1f101d..03147c2 100644
--- a/src/com/android/settings/CopyablePreference.java
+++ b/src/com/android/settings/CopyablePreference.java
@@ -30,6 +30,10 @@
         super(context, attrs);
     }
 
+    public CopyablePreference(Context context) {
+        this(context, null);
+    }
+
     @Override
     public void onBindViewHolder(PreferenceViewHolder holder) {
         super.onBindViewHolder(holder);
@@ -45,10 +49,14 @@
         });
     }
 
-    public static void copyPreference(Context context, Preference pref) {
+    public CharSequence getCopyableText() {
+        return getSummary();
+    }
+
+    public static void copyPreference(Context context, CopyablePreference pref) {
         ClipboardManager cm =
                 (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
-        cm.setText(pref.getSummary());
+        cm.setText(pref.getCopyableText());
         Toast.makeText(context, com.android.internal.R.string.text_copied, Toast.LENGTH_SHORT)
                 .show();
     }
diff --git a/src/com/android/settings/notification/NotificationStation.java b/src/com/android/settings/notification/NotificationStation.java
index 44f0ffc..7c79e8b 100644
--- a/src/com/android/settings/notification/NotificationStation.java
+++ b/src/com/android/settings/notification/NotificationStation.java
@@ -16,28 +16,31 @@
 
 package com.android.settings.notification;
 
-import android.app.Activity;
-import android.app.ActivityManager;
+import android.app.*;
 import android.app.INotificationManager;
-import android.app.Notification;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentSender;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.UserHandle;
+import android.os.*;
 import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationListenerService.Ranking;
+import android.service.notification.NotificationListenerService.RankingMap;
 import android.service.notification.StatusBarNotification;
 import android.support.v7.preference.Preference;
 import android.support.v7.preference.PreferenceViewHolder;
 import android.support.v7.widget.RecyclerView;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
 import android.util.Log;
 import android.view.View;
 import android.widget.DateTimeView;
@@ -45,19 +48,20 @@
 import android.widget.TextView;
 
 import com.android.internal.logging.MetricsProto.MetricsEvent;
+import com.android.settings.CopyablePreference;
 import com.android.settings.R;
 import com.android.settings.SettingsPreferenceFragment;
 import com.android.settings.Utils;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
+import java.lang.StringBuilder;
+import java.util.*;
 
 public class NotificationStation extends SettingsPreferenceFragment {
     private static final String TAG = NotificationStation.class.getSimpleName();
 
     private static final boolean DEBUG = false;
+    private static final boolean DUMP_EXTRAS = true;
+    private static final boolean DUMP_PARCEL = true;
 
     private static class HistoricalNotificationInfo {
         public String pkg;
@@ -69,10 +73,12 @@
         public int user;
         public long timestamp;
         public boolean active;
+        public CharSequence extra;
     }
 
     private PackageManager mPm;
     private INotificationManager mNoMan;
+    private RankingMap mRanking;
 
     private Runnable mRefreshListRunnable = new Runnable() {
         @Override
@@ -83,19 +89,26 @@
 
     private NotificationListenerService mListener = new NotificationListenerService() {
         @Override
-        public void onNotificationPosted(StatusBarNotification notification) {
-            logd("onNotificationPosted: %s", notification);
+        public void onNotificationPosted(StatusBarNotification sbn, RankingMap ranking) {
+            logd("onNotificationPosted: %s", sbn.getNotification());
             final Handler h = getListView().getHandler();
+            mRanking = ranking;
             h.removeCallbacks(mRefreshListRunnable);
             h.postDelayed(mRefreshListRunnable, 100);
         }
 
         @Override
-        public void onNotificationRemoved(StatusBarNotification notification) {
+        public void onNotificationRemoved(StatusBarNotification notification, RankingMap ranking) {
             final Handler h = getListView().getHandler();
+            mRanking = ranking;
             h.removeCallbacks(mRefreshListRunnable);
             h.postDelayed(mRefreshListRunnable, 100);
         }
+
+        @Override
+        public void onNotificationRankingUpdate(RankingMap ranking) {
+            mRanking = ranking;
+        }
     };
 
     private Context mContext;
@@ -179,6 +192,40 @@
         }
     }
 
+    private static CharSequence bold(CharSequence cs) {
+        if (cs.length() == 0) return cs;
+        SpannableString ss = new SpannableString(cs);
+        ss.setSpan(new StyleSpan(Typeface.BOLD), 0, cs.length(), 0);
+        return ss;
+    }
+
+    private static String getTitleString(Notification n) {
+        String title = null;
+        if (n.extras != null) {
+            title = n.extras.getString(Notification.EXTRA_TITLE);
+            if (TextUtils.isEmpty(title)) {
+                title = n.extras.getString(Notification.EXTRA_TEXT);
+            }
+        }
+        if (TextUtils.isEmpty(title) && !TextUtils.isEmpty(n.tickerText)) {
+            title = n.tickerText.toString();
+        }
+        return title;
+    }
+
+    private static String formatPendingIntent(PendingIntent pi) {
+        final StringBuilder sb = new StringBuilder();
+        final IntentSender is = pi.getIntentSender();
+        sb.append("Intent(pkg=").append(is.getCreatorPackage());
+        try {
+            final boolean isActivity =
+                    ActivityManagerNative.getDefault().isIntentSenderAnActivity(is.getTarget());
+            if (isActivity) sb.append(" (activity)");
+        } catch (RemoteException ex) {}
+        sb.append(")");
+        return sb.toString();
+    }
+
     private List<HistoricalNotificationInfo> loadNotifications() {
         final int currentUserId = ActivityManager.getCurrentUser();
         try {
@@ -190,40 +237,189 @@
             List<HistoricalNotificationInfo> list
                     = new ArrayList<HistoricalNotificationInfo>(active.length + dismissed.length);
 
+            final Ranking rank = new Ranking();
+
             for (StatusBarNotification[] resultset
                     : new StatusBarNotification[][] { active, dismissed }) {
                 for (StatusBarNotification sbn : resultset) {
+                    if (sbn.getUserId() != UserHandle.USER_ALL & sbn.getUserId() != currentUserId) {
+                        continue;
+                    }
+
+                    final Notification n = sbn.getNotification();
                     final HistoricalNotificationInfo info = new HistoricalNotificationInfo();
                     info.pkg = sbn.getPackageName();
                     info.user = sbn.getUserId();
-                    info.icon = loadIconDrawable(info.pkg, info.user, sbn.getNotification().icon);
+                    info.icon = loadIconDrawable(info.pkg, info.user, n.icon);
                     info.pkgicon = loadPackageIconDrawable(info.pkg, info.user);
                     info.pkgname = loadPackageName(info.pkg);
-                    if (sbn.getNotification().extras != null) {
-                        info.title = sbn.getNotification().extras.getString(
-                                Notification.EXTRA_TITLE);
-                        if (info.title == null || "".equals(info.title)) {
-                            info.title = sbn.getNotification().extras.getString(
-                                    Notification.EXTRA_TEXT);
-                        }
-                    }
-                    if (info.title == null || "".equals(info.title)) {
-                        info.title = sbn.getNotification().tickerText;
-                    }
-                    // still nothing? come on, give us something!
-                    if (info.title == null || "".equals(info.title)) {
-                        info.title = info.pkgname;
+                    info.title = getTitleString(n);
+                    if (TextUtils.isEmpty(info.title)) {
+                        info.title = getString(R.string.notification_log_no_title);
                     }
                     info.timestamp = sbn.getPostTime();
-                    info.priority = sbn.getNotification().priority;
-                    logd("   [%d] %s: %s", info.timestamp, info.pkg, info.title);
+                    info.priority = n.priority;
 
                     info.active = (resultset == active);
 
-                    if (info.user == UserHandle.USER_ALL
-                            || info.user == currentUserId) {
-                        list.add(info);
+                    final SpannableStringBuilder sb = new SpannableStringBuilder();
+                    final String delim = getString(R.string.notification_log_details_delimiter);
+                    sb.append(bold(getString(R.string.notification_log_details_package)))
+                            .append(delim)
+                            .append(info.pkg)
+                            .append("\n")
+                            .append(bold(getString(R.string.notification_log_details_key)))
+                            .append(delim)
+                            .append(sbn.getKey());
+                    sb.append("\n")
+                            .append(bold(getString(R.string.notification_log_details_icon)))
+                            .append(delim)
+                            .append(n.getSmallIcon().toString());
+                    if (!TextUtils.isEmpty(n.getGroup())) {
+                        sb.append("\n")
+                                .append(bold(getString(R.string.notification_log_details_group)))
+                                .append(delim)
+                                .append(n.getGroup());
+                        if (n.isGroupSummary()) {
+                            sb.append(bold(
+                                    getString(R.string.notification_log_details_group_summary)));
+                        }
                     }
+                    sb.append("\n")
+                            .append(bold(getString(R.string.notification_log_details_sound)))
+                            .append(delim);
+                    if (0 != (n.defaults & Notification.DEFAULT_SOUND)) {
+                        sb.append(getString(R.string.notification_log_details_default));
+                    } else if (n.sound != null) {
+                        sb.append(n.sound.toString());
+                    } else {
+                        sb.append(getString(R.string.notification_log_details_none));
+                    }
+                    sb.append("\n")
+                            .append(bold(getString(R.string.notification_log_details_vibrate)))
+                            .append(delim);
+                    if (0 != (n.defaults & Notification.DEFAULT_VIBRATE)) {
+                        sb.append(getString(R.string.notification_log_details_default));
+                    } else if (n.vibrate != null) {
+                        for (int vi=0;vi<n.vibrate.length;vi++) {
+                            if (vi > 0) sb.append(',');
+                            sb.append(String.valueOf(n.vibrate[vi]));
+                        }
+                    } else {
+                        sb.append(getString(R.string.notification_log_details_none));
+                    }
+                    sb.append("\n")
+                            .append(bold(getString(R.string.notification_log_details_visibility)))
+                            .append(delim)
+                            .append(Notification.visibilityToString(n.visibility));
+                    if (n.publicVersion != null) {
+                        sb.append("\n")
+                                .append(bold(getString(
+                                        R.string.notification_log_details_public_version)))
+                                .append(delim)
+                                .append(getTitleString(n.publicVersion));
+                    }
+                    sb.append("\n")
+                            .append(bold(getString(R.string.notification_log_details_priority)))
+                            .append(delim)
+                            .append(Notification.priorityToString(n.priority));
+                    if (mRanking != null && mRanking.getRanking(sbn.getKey(), rank)) {
+                        sb.append("\n")
+                                .append(bold(getString(
+                                        R.string.notification_log_details_importance)))
+                                .append(delim)
+                                .append(Ranking.importanceToString(rank.getImportance()));
+                        if (rank.getImportanceExplanation() != null) {
+                            sb.append("\n")
+                                    .append(bold(getString(
+                                            R.string.notification_log_details_explanation)))
+                                    .append(delim)
+                                    .append(rank.getImportanceExplanation());
+                        }
+                    }
+                    if (n.contentIntent != null) {
+                        sb.append("\n")
+                                .append(bold(getString(
+                                        R.string.notification_log_details_content_intent)))
+                                .append(delim)
+                                .append(formatPendingIntent(n.contentIntent));
+                    }
+                    if (n.deleteIntent != null) {
+                        sb.append("\n")
+                                .append(bold(getString(
+                                        R.string.notification_log_details_delete_intent)))
+                                .append(delim)
+                                .append(formatPendingIntent(n.deleteIntent));
+                    }
+                    if (n.fullScreenIntent != null) {
+                        sb.append("\n")
+                                .append(bold(getString(
+                                        R.string.notification_log_details_full_screen_intent)))
+                                .append(delim)
+                                .append(formatPendingIntent(n.fullScreenIntent));
+                    }
+                    if (n.actions != null && n.actions.length > 0) {
+                        sb.append("\n")
+                                .append(bold(getString(R.string.notification_log_details_actions)));
+                        for (int ai=0; ai<n.actions.length; ai++) {
+                            final Notification.Action action = n.actions[ai];
+                            sb.append("\n  ").append(String.valueOf(ai)).append(' ')
+                                    .append(bold(getString(
+                                            R.string.notification_log_details_title)))
+                                    .append(delim)
+                                    .append(action.title)
+                                    .append("\n    ")
+                                    .append(bold(getString(
+                                            R.string.notification_log_details_content_intent)))
+                                    .append(delim)
+                                    .append(formatPendingIntent(action.actionIntent));
+                            if (action.getRemoteInputs() != null) {
+                                sb.append(' ')
+                                        .append(bold(getString(
+                                                R.string.notification_log_details_remoteinput)))
+                                        .append(delim)
+                                        .append(String.valueOf(action.getRemoteInputs().length));
+                            }
+                        }
+                    }
+                    if (n.contentView != null) {
+                        sb.append("\n")
+                                .append(bold(getString(
+                                        R.string.notification_log_details_content_view)))
+                                .append(delim)
+                                .append(n.contentView.toString());
+                    }
+
+                    if (DUMP_EXTRAS) {
+                        if (n.extras != null && n.extras.size() > 0) {
+                            sb.append("\n")
+                                    .append(bold(getString(
+                                            R.string.notification_log_details_extras)));
+                            for (String extraKey : n.extras.keySet()) {
+                                String val = String.valueOf(n.extras.get(extraKey));
+                                if (val.length() > 100) val = val.substring(0, 100) + "...";
+                                sb.append("\n  ").append(extraKey).append(delim).append(val);
+                            }
+                        }
+                    }
+                    if (DUMP_PARCEL) {
+                        final Parcel p = Parcel.obtain();
+                        n.writeToParcel(p, 0);
+                        sb.append("\n")
+                                .append(bold(getString(R.string.notification_log_details_parcel)))
+                                .append(delim)
+                                .append(String.valueOf(p.dataPosition()))
+                                .append(' ')
+                                .append(bold(getString(R.string.notification_log_details_ashmem)))
+                                .append(delim)
+                                .append(String.valueOf(p.getBlobAshmemSize()))
+                                .append("\n");
+                    }
+
+                    info.extra = sb;
+
+                    logd("   [%d] %s: %s", info.timestamp, info.pkg, info.title);
+                    list.add(info);
                 }
             }
 
@@ -293,7 +489,7 @@
         return null;
     }
 
-    private static class HistoricalNotificationPreference extends Preference {
+    private static class HistoricalNotificationPreference extends CopyablePreference {
         private final HistoricalNotificationInfo mInfo;
 
         public HistoricalNotificationPreference(Context context, HistoricalNotificationInfo info) {
@@ -304,27 +500,49 @@
 
         @Override
         public void onBindViewHolder(PreferenceViewHolder row) {
+            super.onBindViewHolder(row);
+
             if (mInfo.icon != null) {
-                ((ImageView) row.findViewById(android.R.id.icon)).setImageDrawable(mInfo.icon);
+                ((ImageView) row.findViewById(R.id.icon)).setImageDrawable(mInfo.icon);
             }
             if (mInfo.pkgicon != null) {
                 ((ImageView) row.findViewById(R.id.pkgicon)).setImageDrawable(mInfo.pkgicon);
             }
 
             ((DateTimeView) row.findViewById(R.id.timestamp)).setTime(mInfo.timestamp);
-            ((TextView) row.findViewById(android.R.id.title)).setText(mInfo.title);
+            ((TextView) row.findViewById(R.id.title)).setText(mInfo.title);
             ((TextView) row.findViewById(R.id.pkgname)).setText(mInfo.pkgname);
 
-            row.findViewById(R.id.extra).setVisibility(View.GONE);
+            final TextView extra = (TextView) row.findViewById(R.id.extra);
+            extra.setText(mInfo.extra);
+            extra.setVisibility(View.GONE);
+
+            row.itemView.setOnClickListener(
+                    new View.OnClickListener() {
+                        @Override
+                        public void onClick(View view) {
+                            extra.setVisibility(extra.getVisibility() == View.VISIBLE
+                                    ? View.GONE : View.VISIBLE);
+                        }
+                    });
+
             row.itemView.setAlpha(mInfo.active ? 1.0f : 0.5f);
         }
 
         @Override
+        public CharSequence getCopyableText() {
+            return new SpannableStringBuilder(mInfo.title)
+                    .append(" [").append(new Date(mInfo.timestamp).toString())
+                    .append("]\n").append(mInfo.pkgname)
+                    .append("\n").append(mInfo.extra);
+        }
+
+        @Override
         public void performClick() {
-            Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
-                    Uri.fromParts("package", mInfo.pkg, null));
-            intent.setComponent(intent.resolveActivity(getContext().getPackageManager()));
-            getContext().startActivity(intent);
+//            Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+//                    Uri.fromParts("package", mInfo.pkg, null));
+//            intent.setComponent(intent.resolveActivity(getContext().getPackageManager()));
+//            getContext().startActivity(intent);
         }
     }
 }