Notification: Reuse drawable in Header if Icon unchanged

Mitigates an issue where a LevelListDrawable would constantly
be reloaded even if unchanged. To avoid this, small icons are
now only reloaded if they no longer point to the same resource.

Note that StatusBarIconView already has this logic.

Change-Id: I6be436e5cef7b7ca91a28edc413b1aaa0f1007d5
Fixes: 30496073
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 4802b29..c4a9378 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3256,7 +3256,8 @@
          * Resets the notification header to its original state
          */
         private void resetNotificationHeader(RemoteViews contentView) {
-            contentView.setImageViewResource(R.id.icon, 0);
+            // Small icon doesn't need to be reset, as it's always set. Resetting would prevent
+            // re-using the drawable when the notification is updated.
             contentView.setBoolean(R.id.notification_header, "setExpanded", false);
             contentView.setTextViewText(R.id.app_name_text, null);
             contentView.setViewVisibility(R.id.chronometer, View.GONE);
diff --git a/core/java/com/android/internal/widget/CachingIconView.java b/core/java/com/android/internal/widget/CachingIconView.java
new file mode 100644
index 0000000..293b77b
--- /dev/null
+++ b/core/java/com/android/internal/widget/CachingIconView.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2016 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.internal.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.widget.ImageView;
+import android.widget.RemoteViews;
+
+import libcore.util.Objects;
+
+/**
+ * An ImageView for displaying an Icon. Avoids reloading the Icon when possible.
+ */
+@RemoteViews.RemoteView
+public class CachingIconView extends ImageView {
+
+    private String mLastPackage;
+    private int mLastResId;
+    private boolean mInternalSetDrawable;
+
+    public CachingIconView(Context context, @Nullable AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    @RemotableViewMethod(asyncImpl="setImageIconAsync")
+    public void setImageIcon(@Nullable Icon icon) {
+        if (!testAndSetCache(icon)) {
+            mInternalSetDrawable = true;
+            // This calls back to setImageDrawable, make sure we don't clear the cache there.
+            super.setImageIcon(icon);
+            mInternalSetDrawable = false;
+        }
+    }
+
+    @Override
+    public Runnable setImageIconAsync(@Nullable Icon icon) {
+        resetCache();
+        return super.setImageIconAsync(icon);
+    }
+
+    @Override
+    @RemotableViewMethod(asyncImpl="setImageResourceAsync")
+    public void setImageResource(@DrawableRes int resId) {
+        if (!testAndSetCache(resId)) {
+            mInternalSetDrawable = true;
+            // This calls back to setImageDrawable, make sure we don't clear the cache there.
+            super.setImageResource(resId);
+            mInternalSetDrawable = false;
+        }
+    }
+
+    @Override
+    public Runnable setImageResourceAsync(@DrawableRes int resId) {
+        resetCache();
+        return super.setImageResourceAsync(resId);
+    }
+
+    @Override
+    @RemotableViewMethod(asyncImpl="setImageURIAsync")
+    public void setImageURI(@Nullable Uri uri) {
+        resetCache();
+        super.setImageURI(uri);
+    }
+
+    @Override
+    public Runnable setImageURIAsync(@Nullable Uri uri) {
+        resetCache();
+        return super.setImageURIAsync(uri);
+    }
+
+    @Override
+    public void setImageDrawable(@Nullable Drawable drawable) {
+        if (!mInternalSetDrawable) {
+            // Only clear the cache if we were externally called.
+            resetCache();
+        }
+        super.setImageDrawable(drawable);
+    }
+
+    @Override
+    @RemotableViewMethod
+    public void setImageBitmap(Bitmap bm) {
+        resetCache();
+        super.setImageBitmap(bm);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        resetCache();
+    }
+
+    /**
+     * @return true if the currently set image is the same as {@param icon}
+     */
+    private synchronized boolean testAndSetCache(Icon icon) {
+        if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
+            String iconPackage = normalizeIconPackage(icon);
+
+            boolean isCached = mLastResId != 0
+                    && icon.getResId() == mLastResId
+                    && Objects.equal(iconPackage, mLastPackage);
+
+            mLastPackage = iconPackage;
+            mLastResId = icon.getResId();
+
+            return isCached;
+        } else {
+            resetCache();
+            return false;
+        }
+    }
+
+    /**
+     * @return true if the currently set image is the same as {@param resId}
+     */
+    private synchronized boolean testAndSetCache(int resId) {
+        boolean isCached;
+        if (resId == 0 || mLastResId == 0) {
+            isCached = false;
+        } else {
+            isCached = resId == mLastResId && null == mLastPackage;
+        }
+        mLastPackage = null;
+        mLastResId = resId;
+        return isCached;
+    }
+
+    /**
+     * Returns the normalized package name of {@param icon}.
+     * @return null if icon is null or if the icons package is null, empty or matches the current
+     *         context. Otherwise returns the icon's package context.
+     */
+    private String normalizeIconPackage(Icon icon) {
+        if (icon == null) {
+            return null;
+        }
+
+        String pkg = icon.getResPackage();
+        if (TextUtils.isEmpty(pkg)) {
+            return null;
+        }
+        if (pkg.equals(mContext.getPackageName())) {
+            return null;
+        }
+        return pkg;
+    }
+
+    private synchronized void resetCache() {
+        mLastResId = 0;
+        mLastPackage = null;
+    }
+}
diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml
index 38f671c..1f71a18 100644
--- a/core/res/res/layout/notification_template_header.xml
+++ b/core/res/res/layout/notification_template_header.xml
@@ -26,7 +26,7 @@
     android:paddingBottom="16dp"
     android:paddingStart="@dimen/notification_content_margin_start"
     android:paddingEnd="16dp">
-    <ImageView
+    <com.android.internal.widget.CachingIconView
         android:id="@+id/icon"
         android:layout_width="18dp"
         android:layout_height="18dp"