Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5c2d8467 authored by Jorim Jaggi's avatar Jorim Jaggi
Browse files

Move legacy notification processing to Notification.Builder

Bug: 13485610
Change-Id: I5466d3dbc328c77876dc701c17e7a5a06777dbbe
parent 7638b1f3
Loading
Loading
Loading
Loading
+94 −24
Original line number Diff line number Diff line
@@ -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++;
            }
+94 −0
Original line number Diff line number Diff line
@@ -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
     *
+4 −2
Original line number Diff line number Diff line
@@ -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;
+193 −0
Original line number Diff line number Diff line
/*
 * 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));
    }
}
Loading