Fixes touch ripples on media buttons.

This change makes touch ripples appear correctly on the action buttons
on media notifications.  This involves the following changes:

- NotificationViewWrapper.onReinflated() sets the notification content
root's background to transparent rather than null when transferring the
background color to the NotificationBackgroundView.  This gives the
ripples a surface to draw on.
- The RemoteViews for the media templates are changed to use a fixed
set of buttons instead of removing and re-adding them on every update.
This prevents the update caused by whatever action the tap invokes from
interrupting the ripples.
- A method is added to RemoteViews allowing the ripple color to be
specified.  This allows us to change the ripple color to match the
foreground color of the controls, which is necessary for the ripples to
show up against a dark background.

Test: manual
Change-Id: I907f9a1a75efb48da496133ad357fc5150de4410
Fixes: 35856702
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index b57a7d9..4f41da6 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -20,6 +20,7 @@
 
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
+import android.annotation.IdRes;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -37,6 +38,7 @@
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -7759,8 +7761,17 @@
      * @see Notification.Builder#setColorized(boolean)
      */
     public static class MediaStyle extends Style {
+        // Changing max media buttons requires also changing templates
+        // (notification_template_material_media and notification_template_material_big_media).
         static final int MAX_MEDIA_BUTTONS_IN_COMPACT = 3;
         static final int MAX_MEDIA_BUTTONS = 5;
+        @IdRes private static final int[] MEDIA_BUTTON_IDS = {
+                R.id.action0,
+                R.id.action1,
+                R.id.action2,
+                R.id.action3,
+                R.id.action4,
+        };
 
         private int[] mActionsToShowInCompact = null;
         private MediaSession.Token mToken;
@@ -7874,15 +7885,16 @@
             return false;
         }
 
-        private RemoteViews generateMediaActionButton(Action action, int color) {
+        private void bindMediaActionButton(RemoteViews container, @IdRes int buttonId,
+                Action action, int color) {
             final boolean tombstone = (action.actionIntent == null);
-            RemoteViews button = new BuilderRemoteViews(mBuilder.mContext.getApplicationInfo(),
-                    R.layout.notification_material_media_action);
-            button.setImageViewIcon(R.id.action0, action.getIcon());
+            container.setViewVisibility(buttonId, View.VISIBLE);
+            container.setImageViewIcon(buttonId, action.getIcon());
 
             // If the action buttons should not be tinted, then just use the default
             // notification color. Otherwise, just use the passed-in color.
-            Configuration currentConfig = mBuilder.mContext.getResources().getConfiguration();
+            Resources resources = mBuilder.mContext.getResources();
+            Configuration currentConfig = resources.getConfiguration();
             boolean inNightMode = (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK)
                     == Configuration.UI_MODE_NIGHT_YES;
             int tintColor = mBuilder.shouldTintActionButtons() || mBuilder.isColorized()
@@ -7890,13 +7902,21 @@
                     : ContrastColorUtil.resolveColor(mBuilder.mContext,
                             Notification.COLOR_DEFAULT, inNightMode);
 
-            button.setDrawableTint(R.id.action0, false, tintColor,
+            container.setDrawableTint(buttonId, false, tintColor,
                     PorterDuff.Mode.SRC_ATOP);
+
+            final TypedArray typedArray = mBuilder.mContext.obtainStyledAttributes(
+                    new int[]{ android.R.attr.colorControlHighlight });
+            int rippleAlpha = Color.alpha(typedArray.getColor(0, 0));
+            typedArray.recycle();
+            int rippleColor = Color.argb(rippleAlpha, Color.red(tintColor), Color.green(tintColor),
+                    Color.blue(tintColor));
+            container.setRippleDrawableColor(buttonId, ColorStateList.valueOf(rippleColor));
+
             if (!tombstone) {
-                button.setOnClickPendingIntent(R.id.action0, action.actionIntent);
+                container.setOnClickPendingIntent(buttonId, action.actionIntent);
             }
-            button.setContentDescription(R.id.action0, action.title);
-            return button;
+            container.setContentDescription(buttonId, action.title);
         }
 
         private RemoteViews makeMediaContentView() {
@@ -7905,21 +7925,20 @@
                     null /* result */);
 
             final int numActions = mBuilder.mActions.size();
-            final int N = mActionsToShowInCompact == null
+            final int numActionsToShow = mActionsToShowInCompact == null
                     ? 0
                     : Math.min(mActionsToShowInCompact.length, MAX_MEDIA_BUTTONS_IN_COMPACT);
-            view.removeAllViews(com.android.internal.R.id.media_actions);
-            if (N > 0) {
-                for (int i = 0; i < N; i++) {
-                    if (i >= numActions) {
-                        throw new IllegalArgumentException(String.format(
-                                "setShowActionsInCompactView: action %d out of bounds (max %d)",
-                                i, numActions - 1));
-                    }
-
+            if (numActionsToShow > numActions) {
+                throw new IllegalArgumentException(String.format(
+                        "setShowActionsInCompactView: action %d out of bounds (max %d)",
+                        numActions, numActions - 1));
+            }
+            for (int i = 0; i < MAX_MEDIA_BUTTONS_IN_COMPACT; i++) {
+                if (i < numActionsToShow) {
                     final Action action = mBuilder.mActions.get(mActionsToShowInCompact[i]);
-                    final RemoteViews button = generateMediaActionButton(action, getActionColor());
-                    view.addView(com.android.internal.R.id.media_actions, button);
+                    bindMediaActionButton(view, MEDIA_BUTTON_IDS[i], action, getActionColor());
+                } else {
+                    view.setViewVisibility(MEDIA_BUTTON_IDS[i], View.GONE);
                 }
             }
             handleImage(view);
@@ -7949,12 +7968,12 @@
             RemoteViews big = mBuilder.applyStandardTemplate(
                     R.layout.notification_template_material_big_media, false, null /* result */);
 
-            if (actionCount > 0) {
-                big.removeAllViews(com.android.internal.R.id.media_actions);
-                for (int i = 0; i < actionCount; i++) {
-                    final RemoteViews button = generateMediaActionButton(mBuilder.mActions.get(i),
+            for (int i = 0; i < MAX_MEDIA_BUTTONS; i++) {
+                if (i < actionCount) {
+                    bindMediaActionButton(big, MEDIA_BUTTON_IDS[i], mBuilder.mActions.get(i),
                             getActionColor());
-                    big.addView(com.android.internal.R.id.media_actions, button);
+                } else {
+                    big.setViewVisibility(MEDIA_BUTTON_IDS[i], View.GONE);
                 }
             }
             handleImage(big);
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 35ff6cc..8f17e96 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -42,6 +42,7 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
+import android.graphics.drawable.RippleDrawable;
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.os.Binder;
@@ -152,6 +153,7 @@
     private static final int SET_REMOTE_INPUTS_ACTION_TAG = 18;
     private static final int LAYOUT_PARAM_ACTION_TAG = 19;
     private static final int OVERRIDE_TEXT_COLORS_TAG = 20;
+    private static final int SET_RIPPLE_DRAWABLE_COLOR_TAG = 21;
 
     /**
      * Application that hosts the remote views.
@@ -1122,6 +1124,53 @@
         PorterDuff.Mode filterMode;
     }
 
+    /**
+     * Equivalent to calling
+     * {@link RippleDrawable#setColor(ColorStateList)},
+     * on the {@link Drawable} of a given view.
+     * <p>
+     * The operation will be performed on the {@link Drawable} returned by the
+     * target {@link View#getBackground()}.
+     * <p>
+     */
+    private class SetRippleDrawableColor extends Action {
+
+        ColorStateList mColorStateList;
+
+        SetRippleDrawableColor(int id, ColorStateList colorStateList) {
+            this.viewId = id;
+            this.mColorStateList = colorStateList;
+        }
+
+        SetRippleDrawableColor(Parcel parcel) {
+            viewId = parcel.readInt();
+            mColorStateList = parcel.readParcelable(null);
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(viewId);
+            dest.writeParcelable(mColorStateList, 0);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final View target = root.findViewById(viewId);
+            if (target == null) return;
+
+            // Pick the correct drawable to modify for this view
+            Drawable targetDrawable = target.getBackground();
+
+            if (targetDrawable instanceof RippleDrawable) {
+                ((RippleDrawable) targetDrawable.mutate()).setColor(mColorStateList);
+            }
+        }
+
+        @Override
+        public int getActionTag() {
+            return SET_RIPPLE_DRAWABLE_COLOR_TAG;
+        }
+    }
+
     private final class ViewContentNavigation extends Action {
         final boolean mNext;
 
@@ -2394,6 +2443,8 @@
                 return new LayoutParamAction(parcel);
             case OVERRIDE_TEXT_COLORS_TAG:
                 return new OverrideTextColorsAction(parcel);
+            case SET_RIPPLE_DRAWABLE_COLOR_TAG:
+                return new SetRippleDrawableColor(parcel);
             default:
                 throw new ActionException("Tag " + tag + " not found");
         }
@@ -2855,6 +2906,22 @@
 
     /**
      * @hide
+     * Equivalent to calling
+     * {@link RippleDrawable#setColor(ColorStateList)} on the {@link Drawable} of a given view,
+     * assuming it's a {@link RippleDrawable}.
+     * <p>
+     *
+     * @param viewId The id of the view that contains the target
+     *            {@link RippleDrawable}
+     * @param colorStateList Specify a color for a
+     *            {@link ColorStateList} for this drawable.
+     */
+    public void setRippleDrawableColor(int viewId, ColorStateList colorStateList) {
+        addAction(new SetRippleDrawableColor(viewId, colorStateList));
+    }
+
+    /**
+     * @hide
      * Equivalent to calling {@link android.widget.ProgressBar#setProgressTintList}.
      *
      * @param viewId The id of the view whose tint should change
diff --git a/core/res/res/layout/notification_material_media_action.xml b/core/res/res/layout/notification_material_media_action.xml
index 900ca2d..dd79a0b 100644
--- a/core/res/res/layout/notification_material_media_action.xml
+++ b/core/res/res/layout/notification_material_media_action.xml
@@ -18,7 +18,6 @@
 <ImageButton
     xmlns:android="http://schemas.android.com/apk/res/android"
     style="@android:style/Widget.Material.Button.Borderless.Small"
-    android:id="@+id/action0"
     android:layout_width="@dimen/media_notification_action_button_size"
     android:layout_height="@dimen/media_notification_action_button_size"
     android:paddingBottom="8dp"
@@ -28,4 +27,5 @@
     android:layout_marginEnd="2dp"
     android:gravity="center"
     android:background="@drawable/notification_material_media_action_background"
+    android:visibility="gone"
     />
diff --git a/core/res/res/layout/notification_template_material_big_media.xml b/core/res/res/layout/notification_template_material_big_media.xml
index b4e26483..5cb93eb 100644
--- a/core/res/res/layout/notification_template_material_big_media.xml
+++ b/core/res/res/layout/notification_template_material_big_media.xml
@@ -59,7 +59,26 @@
             android:orientation="horizontal"
             android:layoutDirection="ltr"
             style="@style/NotificationMediaActionContainer" >
-            <!-- media buttons will be added here -->
+            <include
+                layout="@layout/notification_material_media_action"
+                android:id="@+id/action0"
+            />
+            <include
+                layout="@layout/notification_material_media_action"
+                android:id="@+id/action1"
+            />
+            <include
+                layout="@layout/notification_material_media_action"
+                android:id="@+id/action2"
+            />
+            <include
+                layout="@layout/notification_material_media_action"
+                android:id="@+id/action3"
+            />
+            <include
+                layout="@layout/notification_material_media_action"
+                android:id="@+id/action4"
+            />
         </LinearLayout>
     </LinearLayout>
 </com.android.internal.widget.MediaNotificationView>
diff --git a/core/res/res/layout/notification_template_material_media.xml b/core/res/res/layout/notification_template_material_media.xml
index 3a0912b..01b0866 100644
--- a/core/res/res/layout/notification_template_material_media.xml
+++ b/core/res/res/layout/notification_template_material_media.xml
@@ -65,7 +65,18 @@
             android:layoutDirection="ltr"
             android:orientation="horizontal"
             >
-            <!-- media buttons will be added here -->
+            <include
+                layout="@layout/notification_material_media_action"
+                android:id="@+id/action0"
+            />
+            <include
+                layout="@layout/notification_material_media_action"
+                android:id="@+id/action1"
+            />
+            <include
+                layout="@layout/notification_material_media_action"
+                android:id="@+id/action2"
+            />
         </LinearLayout>
     </LinearLayout>
 </FrameLayout>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 6f28b2c..2ab3920 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -186,6 +186,8 @@
   <java-symbol type="id" name="action0" />
   <java-symbol type="id" name="action1" />
   <java-symbol type="id" name="action2" />
+  <java-symbol type="id" name="action3" />
+  <java-symbol type="id" name="action4" />
   <java-symbol type="id" name="big_picture" />
   <java-symbol type="id" name="big_text" />
   <java-symbol type="id" name="chronometer" />