From 5c2d84675b239bc04ae98c75526e5b81897ee183 Mon Sep 17 00:00:00 2001 From: Jorim Jaggi Date: Fri, 21 Mar 2014 17:37:00 +0100 Subject: [PATCH] Move legacy notification processing to Notification.Builder Bug: 13485610 Change-Id: I5466d3dbc328c77876dc701c17e7a5a06777dbbe --- core/java/android/app/Notification.java | 118 +++++++-- core/java/android/widget/RemoteViews.java | 94 +++++++ .../android/internal/util}/ImageUtils.java | 6 +- .../internal/util/LegacyNotificationUtil.java | 193 ++++++++++++++ .../drawable/notification_icon_legacy_bg.xml | 0 .../notification_icon_legacy_bg_inset.xml | 0 .../layout/notification_quantum_action.xml | 2 +- .../notification_quantum_action_list.xml | 2 +- .../notification_quantum_action_tombstone.xml | 2 +- .../notification_template_quantum_base.xml | 2 +- ...notification_template_quantum_big_base.xml | 4 +- ...notification_template_quantum_big_text.xml | 6 +- .../notification_template_quantum_inbox.xml | 6 +- core/res/res/values/colors.xml | 3 + core/res/res/values/symbols.xml | 2 + packages/SystemUI/res/values/colors.xml | 2 - .../systemui/statusbar/BaseStatusBar.java | 236 ++---------------- 17 files changed, 419 insertions(+), 259 deletions(-) rename {packages/SystemUI/src/com/android/systemui => core/java/com/android/internal/util}/ImageUtils.java (96%) create mode 100644 core/java/com/android/internal/util/LegacyNotificationUtil.java rename {packages/SystemUI => core/res}/res/drawable/notification_icon_legacy_bg.xml (100%) rename {packages/SystemUI => core/res}/res/drawable/notification_icon_legacy_bg_inset.xml (100%) diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 13e74da569bfa..36d26359675af 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -17,12 +17,14 @@ package android.app; import com.android.internal.R; +import com.android.internal.util.LegacyNotificationUtil; import android.annotation.IntDef; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.PorterDuff; import android.media.AudioManager; import android.net.Uri; import android.os.BadParcelableException; @@ -652,13 +654,6 @@ public class Notification implements Parcelable */ public static final String EXTRA_AS_HEADS_UP = "headsup"; - /** - * Extra added from {@link Notification.Builder} to indicate that the remote views were inflated - * from the builder, as opposed to being created directly from the application. - * @hide - */ - public static final String EXTRA_BUILDER_REMOTE_VIEWS = "android.builderRemoteViews"; - /** * Allow certain system-generated notifications to appear before the device is provisioned. * Only available to notifications coming from the android package. @@ -1315,6 +1310,7 @@ public class Notification implements Parcelable private int mVisibility = VISIBILITY_PRIVATE; private Notification mPublicVersion = null; private boolean mQuantumTheme; + private final LegacyNotificationUtil mLegacyNotificationUtil; /** * Constructs a new Builder with the defaults: @@ -1345,6 +1341,10 @@ public class Notification implements Parcelable // TODO: Decide on targetSdk from calling app whether to use quantum theme. mQuantumTheme = true; + + // TODO: Decide on targetSdk from calling app whether to instantiate the processor at + // all. + mLegacyNotificationUtil = LegacyNotificationUtil.getInstance(); } /** @@ -1846,42 +1846,50 @@ public class Notification implements Parcelable boolean showLine3 = false; boolean showLine2 = false; int smallIconImageViewId = R.id.icon; - if (mLargeIcon != null) { - contentView.setImageViewBitmap(R.id.icon, mLargeIcon); - smallIconImageViewId = R.id.right_icon; - } if (!mQuantumTheme && mPriority < PRIORITY_LOW) { contentView.setInt(R.id.icon, "setBackgroundResource", R.drawable.notification_template_icon_low_bg); contentView.setInt(R.id.status_bar_latest_event_content, "setBackgroundResource", R.drawable.notification_bg_low); } + if (mLargeIcon != null) { + contentView.setImageViewBitmap(R.id.icon, mLargeIcon); + processLegacyLargeIcon(mLargeIcon, contentView); + smallIconImageViewId = R.id.right_icon; + } if (mSmallIcon != 0) { contentView.setImageViewResource(smallIconImageViewId, mSmallIcon); contentView.setViewVisibility(smallIconImageViewId, View.VISIBLE); + if (mLargeIcon != null) { + processLegacySmallIcon(mSmallIcon, smallIconImageViewId, contentView); + } else { + processLegacyLargeIcon(mSmallIcon, contentView); + } + } else { contentView.setViewVisibility(smallIconImageViewId, View.GONE); } if (mContentTitle != null) { - contentView.setTextViewText(R.id.title, mContentTitle); + contentView.setTextViewText(R.id.title, processLegacyText(mContentTitle)); } if (mContentText != null) { - contentView.setTextViewText(R.id.text, mContentText); + contentView.setTextViewText(R.id.text, processLegacyText(mContentText)); showLine3 = true; } if (mContentInfo != null) { - contentView.setTextViewText(R.id.info, mContentInfo); + contentView.setTextViewText(R.id.info, processLegacyText(mContentInfo)); contentView.setViewVisibility(R.id.info, View.VISIBLE); showLine3 = true; } else if (mNumber > 0) { final int tooBig = mContext.getResources().getInteger( R.integer.status_bar_notification_info_maxnum); if (mNumber > tooBig) { - contentView.setTextViewText(R.id.info, mContext.getResources().getString( - R.string.status_bar_notification_info_overflow)); + contentView.setTextViewText(R.id.info, processLegacyText( + mContext.getResources().getString( + R.string.status_bar_notification_info_overflow))); } else { NumberFormat f = NumberFormat.getIntegerInstance(); - contentView.setTextViewText(R.id.info, f.format(mNumber)); + contentView.setTextViewText(R.id.info, processLegacyText(f.format(mNumber))); } contentView.setViewVisibility(R.id.info, View.VISIBLE); showLine3 = true; @@ -1891,9 +1899,9 @@ public class Notification implements Parcelable // Need to show three lines? if (mSubText != null) { - contentView.setTextViewText(R.id.text, mSubText); + contentView.setTextViewText(R.id.text, processLegacyText(mSubText)); if (mContentText != null) { - contentView.setTextViewText(R.id.text2, mContentText); + contentView.setTextViewText(R.id.text2, processLegacyText(mContentText)); contentView.setViewVisibility(R.id.text2, View.VISIBLE); showLine2 = true; } else { @@ -2001,14 +2009,77 @@ public class Notification implements Parcelable tombstone ? getActionTombstoneLayoutResource() : getActionLayoutResource()); button.setTextViewCompoundDrawablesRelative(R.id.action0, action.icon, 0, 0, 0); - button.setTextViewText(R.id.action0, action.title); + button.setTextViewText(R.id.action0, processLegacyText(action.title)); if (!tombstone) { button.setOnClickPendingIntent(R.id.action0, action.actionIntent); } button.setContentDescription(R.id.action0, action.title); + processLegacyAction(action, button); return button; } + /** + * @return Whether we are currently building a notification from a legacy (an app that + * doesn't create quantum notifications by itself) app. + */ + private boolean isLegacy() { + return mLegacyNotificationUtil != null; + } + + private void processLegacyAction(Action action, RemoteViews button) { + if (isLegacy()) { + if (mLegacyNotificationUtil.isGrayscale(mContext, action.icon)) { + button.setTextViewCompoundDrawablesRelativeColorFilter(R.id.action0, 0, + mContext.getResources().getColor( + R.color.notification_action_legacy_color_filter), + PorterDuff.Mode.MULTIPLY); + } + } + } + + private CharSequence processLegacyText(CharSequence charSequence) { + if (isLegacy()) { + return mLegacyNotificationUtil.invertCharSequenceColors(charSequence); + } else { + return charSequence; + } + } + + private void processLegacyLargeIcon(int largeIconId, RemoteViews contentView) { + if (isLegacy()) { + processLegacyLargeIcon( + mLegacyNotificationUtil.isGrayscale(mContext, largeIconId), + contentView); + } + } + + private void processLegacyLargeIcon(Bitmap largeIcon, RemoteViews contentView) { + if (isLegacy()) { + processLegacyLargeIcon( + mLegacyNotificationUtil.isGrayscale(largeIcon), + contentView); + } + } + + private void processLegacyLargeIcon(boolean isGrayscale, RemoteViews contentView) { + if (isLegacy() && isGrayscale) { + contentView.setInt(R.id.icon, "setBackgroundResource", + R.drawable.notification_icon_legacy_bg_inset); + } + } + + private void processLegacySmallIcon(int smallIconDrawableId, int smallIconImageViewId, + RemoteViews contentView) { + if (isLegacy()) { + if (mLegacyNotificationUtil.isGrayscale(mContext, smallIconDrawableId)) { + contentView.setDrawableParameters(smallIconImageViewId, false, -1, + mContext.getResources().getColor( + R.color.notification_action_legacy_color_filter), + PorterDuff.Mode.MULTIPLY, -1); + } + } + } + /** * Apply the unstyled operations and return a new {@link Notification} object. * @hide @@ -2075,7 +2146,6 @@ public class Notification implements Parcelable extras.putBoolean(EXTRA_PROGRESS_INDETERMINATE, mProgressIndeterminate); extras.putBoolean(EXTRA_SHOW_CHRONOMETER, mUseChronometer); extras.putBoolean(EXTRA_SHOW_WHEN, mShowWhen); - extras.putBoolean(EXTRA_BUILDER_REMOTE_VIEWS, mContentView == null); if (mLargeIcon != null) { extras.putParcelable(EXTRA_LARGE_ICON, mLargeIcon); } @@ -2226,7 +2296,7 @@ public class Notification implements Parcelable mSummaryTextSet ? mSummaryText : mBuilder.mSubText; if (overflowText != null) { - contentView.setTextViewText(R.id.text, overflowText); + contentView.setTextViewText(R.id.text, mBuilder.processLegacyText(overflowText)); contentView.setViewVisibility(R.id.overflow_divider, View.VISIBLE); contentView.setViewVisibility(R.id.line3, View.VISIBLE); } else { @@ -2437,7 +2507,7 @@ public class Notification implements Parcelable contentView.setViewPadding(R.id.line1, 0, 0, 0, 0); } - contentView.setTextViewText(R.id.big_text, mBigText); + contentView.setTextViewText(R.id.big_text, mBuilder.processLegacyText(mBigText)); contentView.setViewVisibility(R.id.big_text, View.VISIBLE); contentView.setViewVisibility(R.id.text2, View.GONE); @@ -2542,7 +2612,7 @@ public class Notification implements Parcelable CharSequence str = mTexts.get(i); if (str != null && !str.equals("")) { contentView.setViewVisibility(rowIds[i], View.VISIBLE); - contentView.setTextViewText(rowIds[i], str); + contentView.setTextViewText(rowIds[i], mBuilder.processLegacyText(str)); } i++; } diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 0d3df517490dd..f7d20b532754f 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -1515,6 +1515,75 @@ public class RemoteViews implements Parcelable, Filter { public final static int TAG = 14; } + /** + * Helper action to set a color filter on a compound drawable on a TextView. Supports relative + * (s/t/e/b) or cardinal (l/t/r/b) arrangement. + */ + private class TextViewDrawableColorFilterAction extends Action { + public TextViewDrawableColorFilterAction(int viewId, boolean isRelative, int index, + int color, PorterDuff.Mode mode) { + this.viewId = viewId; + this.isRelative = isRelative; + this.index = index; + this.color = color; + this.mode = mode; + } + + public TextViewDrawableColorFilterAction(Parcel parcel) { + viewId = parcel.readInt(); + isRelative = (parcel.readInt() != 0); + index = parcel.readInt(); + color = parcel.readInt(); + mode = readPorterDuffMode(parcel); + } + + private PorterDuff.Mode readPorterDuffMode(Parcel parcel) { + int mode = parcel.readInt(); + if (mode >= 0 && mode < PorterDuff.Mode.values().length) { + return PorterDuff.Mode.values()[mode]; + } else { + return PorterDuff.Mode.CLEAR; + } + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + dest.writeInt(isRelative ? 1 : 0); + dest.writeInt(index); + dest.writeInt(color); + dest.writeInt(mode.ordinal()); + } + + @Override + public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { + final TextView target = (TextView) root.findViewById(viewId); + if (target == null) return; + Drawable[] drawables = isRelative + ? target.getCompoundDrawablesRelative() + : target.getCompoundDrawables(); + if (index < 0 || index >= 4) { + throw new IllegalStateException("index must be in range [0, 3]."); + } + Drawable d = drawables[index]; + if (d != null) { + d.mutate(); + d.setColorFilter(color, mode); + } + } + + public String getActionName() { + return "TextViewDrawableColorFilterAction"; + } + + final boolean isRelative; + final int index; + final int color; + final PorterDuff.Mode mode; + + public final static int TAG = 17; + } + /** * Simple class used to keep track of memory usage in a RemoteViews. * @@ -1686,6 +1755,9 @@ public class RemoteViews implements Parcelable, Filter { case SetRemoteViewsAdapterList.TAG: mActions.add(new SetRemoteViewsAdapterList(parcel)); break; + case TextViewDrawableColorFilterAction.TAG: + mActions.add(new TextViewDrawableColorFilterAction(parcel)); + break; default: throw new ActionException("Tag " + tag + " not found"); } @@ -1920,6 +1992,28 @@ public class RemoteViews implements Parcelable, Filter { addAction(new TextViewDrawableAction(viewId, true, start, top, end, bottom)); } + /** + * Equivalent to applying a color filter on one of the drawables in + * {@link android.widget.TextView#getCompoundDrawablesRelative()}. + * + * @param viewId The id of the view whose text should change. + * @param index The index of the drawable in the array of + * {@link android.widget.TextView#getCompoundDrawablesRelative()} to set the color + * filter on. Must be in [0, 3]. + * @param color The color of the color filter. See + * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}. + * @param mode The mode of the color filter. See + * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}. + * @hide + */ + public void setTextViewCompoundDrawablesRelativeColorFilter(int viewId, + int index, int color, PorterDuff.Mode mode) { + if (index < 0 || index >= 4) { + throw new IllegalArgumentException("index must be in range [0, 3]."); + } + addAction(new TextViewDrawableColorFilterAction(viewId, true, index, color, mode)); + } + /** * Equivalent to calling ImageView.setImageResource * diff --git a/packages/SystemUI/src/com/android/systemui/ImageUtils.java b/core/java/com/android/internal/util/ImageUtils.java similarity index 96% rename from packages/SystemUI/src/com/android/systemui/ImageUtils.java rename to core/java/com/android/internal/util/ImageUtils.java index 540ba20c143a6..a5ce6e0caec29 100644 --- a/packages/SystemUI/src/com/android/systemui/ImageUtils.java +++ b/core/java/com/android/internal/util/ImageUtils.java @@ -14,12 +14,14 @@ * limitations under the License */ -package com.android.systemui; +package com.android.internal.util; import android.graphics.Bitmap; /** * Utility class for image analysis and processing. + * + * @hide */ public class ImageUtils { @@ -65,7 +67,7 @@ public class ImageUtils { * * Note that really transparent colors are always grayscale. */ - public boolean isGrayscale(int color) { + public static boolean isGrayscale(int color) { int alpha = 0xFF & (color >> 24); if (alpha < ALPHA_TOLERANCE) { return true; diff --git a/core/java/com/android/internal/util/LegacyNotificationUtil.java b/core/java/com/android/internal/util/LegacyNotificationUtil.java new file mode 100644 index 0000000000000..0394bbcf96250 --- /dev/null +++ b/core/java/com/android/internal/util/LegacyNotificationUtil.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2014 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.util; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.AnimationDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.TextAppearanceSpan; +import android.util.Log; +import android.util.Pair; + +import java.util.Arrays; +import java.util.WeakHashMap; + +/** + * Helper class to process legacy (Holo) notifications to make them look like quantum notifications. + * + * @hide + */ +public class LegacyNotificationUtil { + + private static final String TAG = "LegacyNotificationUtil"; + + private static final Object sLock = new Object(); + private static LegacyNotificationUtil sInstance; + + private final ImageUtils mImageUtils = new ImageUtils(); + private final WeakHashMap> mGrayscaleBitmapCache = + new WeakHashMap>(); + + public static LegacyNotificationUtil getInstance() { + synchronized (sLock) { + if (sInstance == null) { + sInstance = new LegacyNotificationUtil(); + } + return sInstance; + } + } + + /** + * Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect + * gray". + * + * @param bitmap The bitmap to test. + * @return Whether the bitmap is grayscale. + */ + public boolean isGrayscale(Bitmap bitmap) { + synchronized (sLock) { + Pair cached = mGrayscaleBitmapCache.get(bitmap); + if (cached != null) { + if (cached.second == bitmap.getGenerationId()) { + return cached.first; + } + } + } + boolean result; + int generationId; + synchronized (mImageUtils) { + result = mImageUtils.isGrayscale(bitmap); + + // generationId and the check whether the Bitmap is grayscale can't be read atomically + // here. However, since the thread is in the process of posting the notification, we can + // assume that it doesn't modify the bitmap while we are checking the pixels. + generationId = bitmap.getGenerationId(); + } + synchronized (sLock) { + mGrayscaleBitmapCache.put(bitmap, Pair.create(result, generationId)); + } + return result; + } + + /** + * Checks whether a drawable is grayscale. Grayscale here means "very close to a perfect + * gray". + * + * @param d The drawable to test. + * @return Whether the drawable is grayscale. + */ + public boolean isGrayscale(Drawable d) { + if (d == null) { + return false; + } else if (d instanceof BitmapDrawable) { + BitmapDrawable bd = (BitmapDrawable) d; + return bd.getBitmap() != null && isGrayscale(bd.getBitmap()); + } else if (d instanceof AnimationDrawable) { + AnimationDrawable ad = (AnimationDrawable) d; + int count = ad.getNumberOfFrames(); + return count > 0 && isGrayscale(ad.getFrame(0)); + } else { + return false; + } + } + + /** + * Checks whether a drawable with a resoure id is grayscale. Grayscale here means "very close + * to a perfect gray". + * + * @param context The context to load the drawable from. + * @return Whether the drawable is grayscale. + */ + public boolean isGrayscale(Context context, int drawableResId) { + if (drawableResId != 0) { + try { + return isGrayscale(context.getDrawable(drawableResId)); + } catch (Resources.NotFoundException ex) { + Log.e(TAG, "Drawable not found: " + drawableResId); + return false; + } + } else { + return false; + } + } + + /** + * Inverts all the grayscale colors set by {@link android.text.style.TextAppearanceSpan}s on + * the text. + * + * @param charSequence The text to process. + * @return The color inverted text. + */ + public CharSequence invertCharSequenceColors(CharSequence charSequence) { + if (charSequence instanceof Spanned) { + Spanned ss = (Spanned) charSequence; + Object[] spans = ss.getSpans(0, ss.length(), Object.class); + SpannableStringBuilder builder = new SpannableStringBuilder(ss.toString()); + for (Object span : spans) { + Object resultSpan = span; + if (span instanceof TextAppearanceSpan) { + resultSpan = processTextAppearanceSpan((TextAppearanceSpan) span); + } + builder.setSpan(resultSpan, ss.getSpanStart(span), ss.getSpanEnd(span), + ss.getSpanFlags(span)); + } + return builder; + } + return charSequence; + } + + private TextAppearanceSpan processTextAppearanceSpan(TextAppearanceSpan span) { + ColorStateList colorStateList = span.getTextColor(); + if (colorStateList != null) { + int[] colors = colorStateList.getColors(); + boolean changed = false; + for (int i = 0; i < colors.length; i++) { + if (ImageUtils.isGrayscale(colors[i])) { + + // Allocate a new array so we don't change the colors in the old color state + // list. + if (!changed) { + colors = Arrays.copyOf(colors, colors.length); + } + colors[i] = processColor(colors[i]); + changed = true; + } + } + if (changed) { + return new TextAppearanceSpan( + span.getFamily(), span.getTextStyle(), span.getTextSize(), + new ColorStateList(colorStateList.getStates(), colors), + span.getLinkTextColor()); + } + } + return span; + } + + private int processColor(int color) { + return Color.argb(Color.alpha(color), + 255 - Color.red(color), + 255 - Color.green(color), + 255 - Color.blue(color)); + } +} diff --git a/packages/SystemUI/res/drawable/notification_icon_legacy_bg.xml b/core/res/res/drawable/notification_icon_legacy_bg.xml similarity index 100% rename from packages/SystemUI/res/drawable/notification_icon_legacy_bg.xml rename to core/res/res/drawable/notification_icon_legacy_bg.xml diff --git a/packages/SystemUI/res/drawable/notification_icon_legacy_bg_inset.xml b/core/res/res/drawable/notification_icon_legacy_bg_inset.xml similarity index 100% rename from packages/SystemUI/res/drawable/notification_icon_legacy_bg_inset.xml rename to core/res/res/drawable/notification_icon_legacy_bg_inset.xml diff --git a/core/res/res/layout/notification_quantum_action.xml b/core/res/res/layout/notification_quantum_action.xml index 775182f4a699f..0986343935a70 100644 --- a/core/res/res/layout/notification_quantum_action.xml +++ b/core/res/res/layout/notification_quantum_action.xml @@ -16,7 +16,7 @@ -->