Merge \"Mark app pending intents in notification extras\" into nyc-dev
am: b2cd9c95bf

Change-Id: Idb515255f224c23d60a513713d9e4f93decd7a9c
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index eaea989..05f49c5 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -39,6 +39,7 @@
 import android.media.session.MediaSession;
 import android.net.Uri;
 import android.os.BadParcelableException;
+import android.os.BaseBundle;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Parcel;
@@ -53,6 +54,7 @@
 import android.text.style.CharacterStyle;
 import android.text.style.RelativeSizeSpan;
 import android.text.style.TextAppearanceSpan;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.Gravity;
@@ -63,6 +65,7 @@
 import android.widget.RemoteViews;
 
 import com.android.internal.R;
+import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.NotificationColorUtil;
 
 import java.lang.annotation.Retention;
@@ -70,6 +73,7 @@
 import java.lang.reflect.Constructor;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -758,6 +762,16 @@
     public Bundle extras = new Bundle();
 
     /**
+     * All pending intents in the notification extras (notification extras, actions extras,
+     * and remote input extras) as the system needs to be able to access them but touching
+     * the extras bundle in the system process is not safe because the bundle may contain
+     * custom parcelable objects.
+     *
+     * @hide
+     */
+    public ArraySet<PendingIntent> extrasPendingIntents;
+
+    /**
      * {@link #extras} key: this is the title of the notification,
      * as supplied to {@link Builder#setContentTitle(CharSequence)}.
      */
@@ -1573,7 +1587,16 @@
     /**
      * Unflatten the notification from a parcel.
      */
-    public Notification(Parcel parcel)
+    @SuppressWarnings("unchecked")
+    public Notification(Parcel parcel) {
+        // IMPORTANT: Add unmarshaling code in readFromParcel as the pending
+        // intents in extras are always written as the last entry.
+        readFromParcelImpl(parcel);
+        // Must be read last!
+        extrasPendingIntents = (ArraySet<PendingIntent>) parcel.readArraySet(null);
+    }
+
+    private void readFromParcelImpl(Parcel parcel)
     {
         int version = parcel.readInt();
 
@@ -1728,6 +1751,10 @@
             }
         }
 
+        if (!ArrayUtils.isEmpty(extrasPendingIntents)) {
+            that.extrasPendingIntents = new ArraySet<>(extrasPendingIntents);
+        }
+
         if (this.actions != null) {
             that.actions = new Action[this.actions.length];
             for(int i=0; i<this.actions.length; i++) {
@@ -1843,8 +1870,40 @@
     /**
      * Flatten this notification into a parcel.
      */
-    public void writeToParcel(Parcel parcel, int flags)
-    {
+    public void writeToParcel(Parcel parcel, int flags) {
+        // We need to mark all pending intents getting into the notification
+        // system as being put there to later allow the notification ranker
+        // to launch them and by doing so add the app to the battery saver white
+        // list for a short period of time. The problem is that the system
+        // cannot look into the extras as there may be parcelables there that
+        // the platform does not know how to handle. To go around that we have
+        // an explicit list of the pending intents in the extras bundle.
+        final boolean collectPendingIntents = (extrasPendingIntents == null);
+        if (collectPendingIntents) {
+            PendingIntent.setOnMarshaledListener(
+                    (PendingIntent intent, Parcel out, int outFlags) -> {
+                if (parcel == out) {
+                    if (extrasPendingIntents == null) {
+                        extrasPendingIntents = new ArraySet<>();
+                    }
+                    extrasPendingIntents.add(intent);
+                }
+            });
+        }
+        try {
+            // IMPORTANT: Add marshaling code in writeToParcelImpl as we
+            // want to intercept all pending events written to the pacel.
+            writeToParcelImpl(parcel, flags);
+            // Must be written last!
+            parcel.writeArraySet(extrasPendingIntents);
+        } finally {
+            if (collectPendingIntents) {
+                PendingIntent.setOnMarshaledListener(null);
+            }
+        }
+    }
+
+    private void writeToParcelImpl(Parcel parcel, int flags) {
         parcel.writeInt(1);
 
         parcel.writeLong(when);
diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java
index cb15392..cfa242b 100644
--- a/core/java/android/app/PendingIntent.java
+++ b/core/java/android/app/PendingIntent.java
@@ -242,6 +242,36 @@
     }
 
     /**
+     * Listener for observing when pending intents are written to a parcel.
+     *
+     * @hide
+     */
+    public interface OnMarshaledListener {
+        /**
+         * Called when a pending intent is written to a parcel.
+         *
+         * @param intent The pending intent.
+         * @param parcel The parcel to which it was written.
+         * @param flags The parcel flags when it was written.
+         */
+        void onMarshaled(PendingIntent intent, Parcel parcel, int flags);
+    }
+
+    private static final ThreadLocal<OnMarshaledListener> sOnMarshaledListener
+            = new ThreadLocal<>();
+
+    /**
+     * Registers an listener for pending intents being written to a parcel.
+     *
+     * @param listener The listener, null to clear.
+     *
+     * @hide
+     */
+    public static void setOnMarshaledListener(OnMarshaledListener listener) {
+        sOnMarshaledListener.set(listener);
+    }
+
+    /**
      * Retrieve a PendingIntent that will start a new activity, like calling
      * {@link Context#startActivity(Intent) Context.startActivity(Intent)}.
      * Note that the activity will be started outside of the context of an
@@ -1016,6 +1046,11 @@
 
     public void writeToParcel(Parcel out, int flags) {
         out.writeStrongBinder(mTarget.asBinder());
+        OnMarshaledListener listener = sOnMarshaledListener.get();
+        if (listener != null) {
+            listener.onMarshaled(this, out, flags);
+        }
+
     }
 
     public static final Parcelable.Creator<PendingIntent> CREATOR
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 2631247..74dcc07 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -17,8 +17,10 @@
 package android.os;
 
 import android.annotation.IntegerRes;
+import android.annotation.Nullable;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.Size;
 import android.util.SizeF;
@@ -734,6 +736,21 @@
     }
 
     /**
+     * Write an array set to the parcel.
+     *
+     * @param val The array set to write.
+     *
+     * @hide
+     */
+    public void writeArraySet(@Nullable ArraySet<? extends Object> val) {
+        final int size = (val != null) ? val.size() : -1;
+        writeInt(size);
+        for (int i = 0; i < size; i++) {
+            writeValue(val.valueAt(i));
+        }
+    }
+
+    /**
      * Flatten a Bundle into the parcel at the current dataPosition(),
      * growing dataCapacity() if needed.
      */
@@ -2735,6 +2752,26 @@
         readArrayMapInternal(outVal, N, loader);
     }
 
+    /**
+     * Reads an array set.
+     *
+     * @param loader The class loader to use.
+     *
+     * @hide
+     */
+    public @Nullable ArraySet<? extends Object> readArraySet(ClassLoader loader) {
+        final int size = readInt();
+        if (size < 0) {
+            return null;
+        }
+        ArraySet<Object> result = new ArraySet<>(size);
+        for (int i = 0; i < size; i++) {
+            Object value = readValue(loader);
+            result.append(value);
+        }
+        return result;
+    }
+
     private void readListInternal(List outVal, int N,
         ClassLoader loader) {
         while (N > 0) {
diff --git a/core/java/android/util/ArraySet.java b/core/java/android/util/ArraySet.java
index 9e9314f..d39e91f 100644
--- a/core/java/android/util/ArraySet.java
+++ b/core/java/android/util/ArraySet.java
@@ -390,6 +390,32 @@
     }
 
     /**
+     * Special fast path for appending items to the end of the array without validation.
+     * The array must already be large enough to contain the item.
+     * @hide
+     */
+    public void append(E value) {
+        final int index = mSize;
+        final int hash = value == null ? 0
+                : (mIdentityHashCode ? System.identityHashCode(value) : value.hashCode());
+        if (index >= mHashes.length) {
+            throw new IllegalStateException("Array is full");
+        }
+        if (index > 0 && mHashes[index - 1] > hash) {
+            RuntimeException e = new RuntimeException("here");
+            e.fillInStackTrace();
+            Log.w(TAG, "New hash " + hash
+                    + " is before end of array hash " + mHashes[index - 1]
+                    + " at index " + index, e);
+            add(value);
+            return;
+        }
+        mSize = index + 1;
+        mHashes[index] = hash;
+        mArray[index] = value;
+    }
+
+    /**
      * Perform a {@link #add(Object)} of all values in <var>array</var>
      * @param array The array whose contents are to be retrieved.
      */
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 59de263..9209d3d 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2598,7 +2598,6 @@
         final long duration = LocalServices.getService(DeviceIdleController.LocalService.class)
                 .getNotificationWhitelistDuration();
 
-        int size = 0;
         if (notification.contentIntent != null) {
             am.setPendingIntentWhitelistDuration(notification.contentIntent.getTarget(), duration);
         }
@@ -2615,45 +2614,19 @@
                     continue;
                 }
                 am.setPendingIntentWhitelistDuration(action.actionIntent.getTarget(), duration);
-                setPendingIntentWhitelistDuration(am, duration, action.getExtras());
-                final RemoteInput[] remoteInputs = action.getRemoteInputs();
-                if (remoteInputs != null) {
-                    for (RemoteInput remoteInput : remoteInputs) {
-                        setPendingIntentWhitelistDuration(am, duration, remoteInput.getExtras());
-                    }
-                }
             }
         }
-    }
-
-    private static void setPendingIntentWhitelistDuration(ActivityManagerInternal am, long duration,
-            Bundle extras) {
-        for (String key : extras.keySet()) {
-            final Object value = extras.get(key);
-            if (value instanceof Parcelable) {
-                setPendingIntentWhitelistDuration(am, duration, (Parcelable) value);
-            } else if (value instanceof Parcelable[]) {
-                for (Parcelable parcelable : (Parcelable[]) value) {
-                    setPendingIntentWhitelistDuration(am, duration, parcelable);
-                }
-            } else if (value instanceof List) {
-                for (Object element : (List <?>) value) {
-                    if (element instanceof Parcelable) {
-                        setPendingIntentWhitelistDuration(am, duration, (Parcelable) element);
-                    }
+        if (notification.extrasPendingIntents != null) {
+            final int intentCount = notification.extrasPendingIntents.size();
+            for (int i = 0; i < intentCount; i++) {
+                PendingIntent pendingIntent = notification.extrasPendingIntents.valueAt(i);
+                if (pendingIntent != null) {
+                    am.setPendingIntentWhitelistDuration(pendingIntent.getTarget(), duration);
                 }
             }
         }
     }
 
-    private static void setPendingIntentWhitelistDuration(ActivityManagerInternal am, long duration,
-            Parcelable parcelable) {
-        if (parcelable instanceof PendingIntent) {
-            am.setPendingIntentWhitelistDuration(((PendingIntent) parcelable).getTarget(),
-                    duration);
-        }
-    }
-
     private class EnqueueNotificationRunnable implements Runnable {
         private final NotificationRecord r;
         private final int userId;