Round rect clipping for notification contents

Enable round rect clipping on notifications for the following kind
of notifications:
- Big picture style.
- Big media narrow style.
- Custom notifications.

Bug: 16142505
Change-Id: I157650fe470636ed624a81557c08135827eac0cb
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 354e99d..139462d 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -42,6 +42,10 @@
     <!-- Whether to use cheap, less good looking shadows for recents -->
     <bool name="config_recents_fake_shadows">false</bool>
 
+    <!-- Whether to clip notification contents with a rounded rectangle. Might be expensive on
+         certain GPU's and thus can be turned off with only minimal visual impact. -->
+    <bool name="config_notifications_round_rect_clipping">true</bool>
+
     <!-- The theme to use for RecentsActivity. -->
     <item type="style" name="config_recents_activity_theme">@style/RecentsTheme.Wallpaper</item>
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigMediaNarrowViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigMediaNarrowViewWrapper.java
new file mode 100644
index 0000000..91e5404
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigMediaNarrowViewWrapper.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar;
+
+import android.content.Context;
+import android.view.View;
+
+/**
+ * Wraps a big media narrow notification template layout.
+ */
+public class NotificationBigMediaNarrowViewWrapper extends NotificationMediaViewWrapper {
+
+    protected NotificationBigMediaNarrowViewWrapper(Context ctx,
+            View view) {
+        super(ctx, view);
+    }
+
+    @Override
+    public boolean needsRoundRectClipping() {
+        return true;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigPictureViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigPictureViewWrapper.java
new file mode 100644
index 0000000..ffe0cd1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationBigPictureViewWrapper.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.statusbar;
+
+import android.content.Context;
+import android.view.View;
+
+/**
+ * Wraps a notification view inflated from a big picture style template.
+ */
+public class NotificationBigPictureViewWrapper extends NotificationTemplateViewWrapper {
+
+    protected NotificationBigPictureViewWrapper(Context ctx, View view) {
+        super(ctx, view);
+    }
+
+    @Override
+    public boolean needsRoundRectClipping() {
+        return true;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
index 110b14c..dec2fc7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationContentView.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar;
 
 import android.content.Context;
+import android.graphics.Outline;
 import android.graphics.Paint;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
@@ -24,6 +25,7 @@
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
 import android.view.ViewTreeObserver;
 import android.view.animation.Interpolator;
 import android.view.animation.LinearInterpolator;
@@ -46,21 +48,26 @@
     private final Rect mClipBounds = new Rect();
     private final int mSmallHeight;
     private final int mHeadsUpHeight;
+    private final int mRoundRectRadius;
     private final Interpolator mLinearInterpolator = new LinearInterpolator();
+    private final boolean mRoundRectClippingEnabled;
 
     private View mContractedChild;
     private View mExpandedChild;
     private View mHeadsUpChild;
 
     private NotificationViewWrapper mContractedWrapper;
+    private NotificationViewWrapper mExpandedWrapper;
+    private NotificationViewWrapper mHeadsUpWrapper;
     private int mClipTopAmount;
     private int mContentHeight;
+    private int mUnrestrictedContentHeight;
     private int mVisibleType = VISIBLE_TYPE_CONTRACTED;
     private boolean mDark;
     private final Paint mFadePaint = new Paint();
     private boolean mAnimate;
     private boolean mIsHeadsUp;
-    private ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener
+    private final ViewTreeObserver.OnPreDrawListener mEnableAnimationPredrawListener
             = new ViewTreeObserver.OnPreDrawListener() {
         @Override
         public boolean onPreDraw() {
@@ -70,12 +77,25 @@
         }
     };
 
+    private final ViewOutlineProvider mOutlineProvider = new ViewOutlineProvider() {
+        @Override
+        public void getOutline(View view, Outline outline) {
+            outline.setRoundRect(0, 0, view.getWidth(), mUnrestrictedContentHeight,
+                    mRoundRectRadius);
+        }
+    };
+
     public NotificationContentView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mFadePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
         mSmallHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height);
         mHeadsUpHeight = getResources().getDimensionPixelSize(R.dimen.notification_mid_height);
+        mRoundRectRadius = getResources().getDimensionPixelSize(
+                R.dimen.notification_material_rounded_rect_radius);
+        mRoundRectClippingEnabled = getResources().getBoolean(
+                R.bool.config_notifications_round_rect_clipping);
         reset(true);
+        setOutlineProvider(mOutlineProvider);
     }
 
     @Override
@@ -127,6 +147,7 @@
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
         updateClipping();
+        invalidateOutline();
     }
 
     @Override
@@ -177,6 +198,7 @@
         mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child);
         selectLayout(false /* animate */, true /* force */);
         mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
+        updateRoundRectClipping();
     }
 
     public void setExpandedChild(View child) {
@@ -186,7 +208,9 @@
         }
         addView(child);
         mExpandedChild = child;
+        mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child);
         selectLayout(false /* animate */, true /* force */);
+        updateRoundRectClipping();
     }
 
     public void setHeadsUpChild(View child) {
@@ -196,7 +220,9 @@
         }
         addView(child);
         mHeadsUpChild = child;
+        mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child);
         selectLayout(false /* animate */, true /* force */);
+        updateRoundRectClipping();
     }
 
     @Override
@@ -222,10 +248,11 @@
     }
 
     public void setContentHeight(int contentHeight) {
-        contentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());
-        mContentHeight = contentHeight;
+        mContentHeight = Math.max(Math.min(contentHeight, getHeight()), getMinHeight());;
+        mUnrestrictedContentHeight = Math.max(contentHeight, getMinHeight());
         selectLayout(mAnimate /* animate */, false /* force */);
         updateClipping();
+        invalidateOutline();
     }
 
     public int getContentHeight() {
@@ -250,6 +277,27 @@
         updateClipping();
     }
 
+    private void updateRoundRectClipping() {
+        boolean enabled = needsRoundRectClipping();
+        setClipToOutline(enabled);
+    }
+
+    private boolean needsRoundRectClipping() {
+        if (!mRoundRectClippingEnabled) {
+            return false;
+        }
+        boolean needsForContracted = mContractedChild != null
+                && mContractedChild.getVisibility() == View.VISIBLE
+                && mContractedWrapper.needsRoundRectClipping();
+        boolean needsForExpanded = mExpandedChild != null
+                && mExpandedChild.getVisibility() == View.VISIBLE
+                && mExpandedWrapper.needsRoundRectClipping();
+        boolean needsForHeadsUp = mExpandedChild != null
+                && mExpandedChild.getVisibility() == View.VISIBLE
+                && mExpandedWrapper.needsRoundRectClipping();
+        return needsForContracted || needsForExpanded || needsForHeadsUp;
+    }
+
     private void updateClipping() {
         mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight);
         setClipBounds(mClipBounds);
@@ -290,6 +338,7 @@
             mHeadsUpChild.setLayerType(LAYER_TYPE_NONE, null);
         }
         setLayerType(LAYER_TYPE_NONE, null);
+        updateRoundRectClipping();
     }
 
     private void runSwitchAnimation(int visibleType) {
@@ -315,6 +364,7 @@
                         updateViewVisibilities(mVisibleType);
                     }
                 });
+        updateRoundRectClipping();
     }
 
     /**
@@ -358,6 +408,10 @@
             mContractedWrapper.notifyContentUpdated();
             mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
         }
+        if (mExpandedChild != null) {
+            mExpandedWrapper.notifyContentUpdated();
+        }
+        updateRoundRectClipping();
     }
 
     public boolean isContentExpandable() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java
index 0702d7e..6fd341b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationCustomViewWrapper.java
@@ -41,4 +41,9 @@
             mInvertHelper.update(dark);
         }
     }
+
+    @Override
+    public boolean needsRoundRectClipping() {
+        return true;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java
index 44e8b85..b362a29 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewWrapper.java
@@ -25,15 +25,23 @@
  */
 public abstract class NotificationViewWrapper {
 
+    private static final String TAG_BIG_MEDIA_NARROW = "bigMediaNarrow";
+    private static final String TAG_MEDIA = "media";
+    private static final String TAG_BIG_PICTURE = "bigPicture";
+
     protected final View mView;
 
     public static NotificationViewWrapper wrap(Context ctx, View v) {
-
-        // TODO: Figure out a better way to find out which template the view is.
-        if (v.findViewById(com.android.internal.R.id.media_actions) != null) {
-            return new NotificationMediaViewWrapper(ctx, v);
-        } else if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) {
-            return new NotificationTemplateViewWrapper(ctx, v);
+        if (v.getId() == com.android.internal.R.id.status_bar_latest_event_content) {
+            if (TAG_BIG_MEDIA_NARROW.equals(v.getTag())) {
+                return new NotificationBigMediaNarrowViewWrapper(ctx, v);
+            } else if (TAG_MEDIA.equals(v.getTag())) {
+                return new NotificationMediaViewWrapper(ctx, v);
+            } else if (TAG_BIG_PICTURE.equals(v.getTag())) {
+                return new NotificationBigMediaNarrowViewWrapper(ctx, v);
+            } else {
+                return new NotificationTemplateViewWrapper(ctx, v);
+            }
         } else {
             return new NotificationCustomViewWrapper(v);
         }
@@ -56,4 +64,12 @@
      * Notifies this wrapper that the content of the view might have changed.
      */
     public void notifyContentUpdated() {}
+
+    /**
+     * @return true if this template might need to be clipped with a round rect to make it look
+     *         nice, false otherwise
+     */
+    public boolean needsRoundRectClipping() {
+        return false;
+    }
 }