am 73581eff: Merge "Move legacy notification processing to Notification.Builder"

* commit '73581effb0b4029961501c6f699e95a9930ea1e6':
  Move legacy notification processing to Notification.Builder
This commit is contained in:
Jorim Jaggi
2014-03-27 14:30:58 +00:00
committed by Android Git Automerger
17 changed files with 419 additions and 259 deletions

View File

@@ -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++;
}

View File

@@ -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
*

View File

@@ -0,0 +1,84 @@
/*
* 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.graphics.Bitmap;
/**
* Utility class for image analysis and processing.
*
* @hide
*/
public class ImageUtils {
// Amount (max is 255) that two channels can differ before the color is no longer "gray".
private static final int TOLERANCE = 20;
// Alpha amount for which values below are considered transparent.
private static final int ALPHA_TOLERANCE = 50;
private int[] mTempBuffer;
/**
* Checks whether a bitmap is grayscale. Grayscale here means "very close to a perfect
* gray".
*/
public boolean isGrayscale(Bitmap bitmap) {
final int height = bitmap.getHeight();
final int width = bitmap.getWidth();
int size = height*width;
ensureBufferSize(size);
bitmap.getPixels(mTempBuffer, 0, width, 0, 0, width, height);
for (int i = 0; i < size; i++) {
if (!isGrayscale(mTempBuffer[i])) {
return false;
}
}
return true;
}
/**
* Makes sure that {@code mTempBuffer} has at least length {@code size}.
*/
private void ensureBufferSize(int size) {
if (mTempBuffer == null || mTempBuffer.length < size) {
mTempBuffer = new int[size];
}
}
/**
* Classifies a color as grayscale or not. Grayscale here means "very close to a perfect
* gray"; if all three channels are approximately equal, this will return true.
*
* Note that really transparent colors are always grayscale.
*/
public static boolean isGrayscale(int color) {
int alpha = 0xFF & (color >> 24);
if (alpha < ALPHA_TOLERANCE) {
return true;
}
int r = 0xFF & (color >> 16);
int g = 0xFF & (color >> 8);
int b = 0xFF & color;
return Math.abs(r - g) < TOLERANCE
&& Math.abs(r - b) < TOLERANCE
&& Math.abs(g - b) < TOLERANCE;
}
}

View File

@@ -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<Bitmap, Pair<Boolean, Integer>> mGrayscaleBitmapCache =
new WeakHashMap<Bitmap, Pair<Boolean, Integer>>();
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<Boolean, Integer> 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));
}
}