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

Commit fb1e9263 authored by Robert Snoeberger's avatar Robert Snoeberger
Browse files

Compute background color.

Don't try to get it from the notification because the notification
is inflated in an async task.

Fixes: 130775480
Test: visual -- played music with GPM and checked background updated
Change-Id: I891f06b05b4ca4ba570a3b405fefa7b049f05cca
parent 2c09b3be
Loading
Loading
Loading
Loading
+5 −3
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.systemui.statusbar

import android.annotation.ColorInt
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
@@ -28,6 +27,7 @@ import android.renderscript.RenderScript
import android.renderscript.ScriptIntrinsicBlur
import android.util.MathUtils
import com.android.internal.graphics.ColorUtils
import com.android.systemui.statusbar.notification.MediaNotificationProcessor

import javax.inject.Inject
import javax.inject.Singleton
@@ -42,7 +42,7 @@ class MediaArtworkProcessor @Inject constructor() {
    private val mTmpSize = Point()
    private var mArtworkCache: Bitmap? = null

    fun processArtwork(context: Context, artwork: Bitmap, @ColorInt color: Int): Bitmap {
    fun processArtwork(context: Context, artwork: Bitmap): Bitmap {
        if (mArtworkCache != null) {
            return mArtworkCache!!
        }
@@ -71,13 +71,15 @@ class MediaArtworkProcessor @Inject constructor() {
        blur.forEach(output)
        output.copyTo(outBitmap)

        val swatch = MediaNotificationProcessor.findBackgroundSwatch(artwork)

        input.destroy()
        output.destroy()
        inBitmap.recycle()
        blur.destroy()

        val canvas = Canvas(outBitmap)
        canvas.drawColor(ColorUtils.setAlphaComponent(color, COLOR_ALPHA))
        canvas.drawColor(ColorUtils.setAlphaComponent(swatch.rgb, COLOR_ALPHA))
        return outBitmap
    }

+87 −19
Original line number Diff line number Diff line
@@ -21,11 +21,11 @@ import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_MEDIA_FAKE_AR
import static com.android.systemui.statusbar.phone.StatusBar.ENABLE_LOCKSCREEN_WALLPAPER;
import static com.android.systemui.statusbar.phone.StatusBar.SHOW_LOCKSCREEN_MEDIA_ARTWORK;

import android.annotation.MainThread;
import android.annotation.Nullable;
import android.app.Notification;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
@@ -35,11 +35,13 @@ import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.DeviceConfig.Properties;
import android.util.ArraySet;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
@@ -64,8 +66,10 @@ import com.android.systemui.statusbar.policy.KeyguardMonitor;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Singleton;
@@ -108,6 +112,7 @@ public class NotificationMediaManager implements Dumpable {
    private final MediaSessionManager mMediaSessionManager;
    private final ArrayList<MediaListener> mMediaListeners;
    private final MediaArtworkProcessor mMediaArtworkProcessor;
    private final Set<AsyncTask<?, ?, ?>> mProcessArtworkTasks = new ArraySet<>();

    protected NotificationPresenter mPresenter;
    private MediaController mMediaController;
@@ -449,29 +454,38 @@ public class NotificationMediaManager implements Dumpable {
                    + " state=" + mStatusBarStateController.getState());
        }

        Drawable artworkDrawable = null;
        Bitmap artworkBitmap = null;
        if (mediaMetadata != null) {
            Bitmap artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
            artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
            if (artworkBitmap == null) {
                artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
                // might still be null
            }
        }

        // Process artwork on a background thread and send the resulting bitmap to
        // finishUpdateMediaMetaData.
        if (metaDataChanged) {
            for (AsyncTask<?, ?, ?> task : mProcessArtworkTasks) {
                task.cancel(true);
            }
            mProcessArtworkTasks.clear();
        }
        if (artworkBitmap != null) {
                int notificationColor;
                synchronized (mEntryManager.getNotificationData()) {
                    NotificationEntry entry = mEntryManager.getNotificationData()
                            .get(mMediaNotificationKey);
                    if (entry == null || entry.getRow() == null) {
                        notificationColor = Color.TRANSPARENT;
            mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged,
                    allowEnterAnimation).execute(artworkBitmap));
        } else {
                        notificationColor = entry.getRow().calculateBgColor();
            finishUpdateMediaMetaData(metaDataChanged, allowEnterAnimation, null);
        }

        Trace.endSection();
    }
                Bitmap bmp = mMediaArtworkProcessor.processArtwork(mContext, artworkBitmap,
                        notificationColor);

    private void finishUpdateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation,
            @Nullable Bitmap bmp) {
        Drawable artworkDrawable = null;
        if (bmp != null) {
            artworkDrawable = new BitmapDrawable(mBackdropBack.getResources(), bmp);
        }
        }
        boolean allowWhenShade = false;
        if (ENABLE_LOCKSCREEN_WALLPAPER && artworkDrawable == null) {
            Bitmap lockWallpaper =
@@ -598,7 +612,6 @@ public class NotificationMediaManager implements Dumpable {
                }
            }
        }
        Trace.endSection();
    }

    public void setup(BackDropView backdrop, ImageView backdropFront, ImageView backdropBack,
@@ -629,6 +642,61 @@ public class NotificationMediaManager implements Dumpable {
        }
    };

    private Bitmap processArtwork(Bitmap artwork) {
        return mMediaArtworkProcessor.processArtwork(mContext, artwork);
    }

    @MainThread
    private void removeTask(AsyncTask<?, ?, ?> task) {
        mProcessArtworkTasks.remove(task);
    }

    /**
     * {@link AsyncTask} to prepare album art for use as backdrop on lock screen.
     */
    private static final class ProcessArtworkTask extends AsyncTask<Bitmap, Void, Bitmap> {

        private final WeakReference<NotificationMediaManager> mManagerRef;
        private final boolean mMetaDataChanged;
        private final boolean mAllowEnterAnimation;

        ProcessArtworkTask(NotificationMediaManager manager, boolean changed,
                boolean allowAnimation) {
            mManagerRef = new WeakReference<>(manager);
            mMetaDataChanged = changed;
            mAllowEnterAnimation = allowAnimation;
        }

        @Override
        protected Bitmap doInBackground(Bitmap... bitmaps) {
            NotificationMediaManager manager = mManagerRef.get();
            if (manager == null || bitmaps.length == 0 || isCancelled()) {
                return null;
            }
            return manager.processArtwork(bitmaps[0]);
        }

        @Override
        protected void onPostExecute(@Nullable Bitmap result) {
            NotificationMediaManager manager = mManagerRef.get();
            if (manager != null && !isCancelled()) {
                manager.removeTask(this);
                manager.finishUpdateMediaMetaData(mMetaDataChanged, mAllowEnterAnimation, result);
            }
        }

        @Override
        protected void onCancelled(Bitmap result) {
            if (result != null) {
                result.recycle();
            }
            NotificationMediaManager manager = mManagerRef.get();
            if (manager != null) {
                manager.removeTask(this);
            }
        }
    }

    public interface MediaListener {
        void onMetadataChanged(MediaMetadata metadata);
    }
+51 −28
Original line number Diff line number Diff line
@@ -69,8 +69,7 @@ public class MediaNotificationProcessor {
    private static final int RESIZE_BITMAP_AREA = 150 * 150;
    private final ImageGradientColorizer mColorizer;
    private final Context mContext;
    private float[] mFilteredBackgroundHsl = null;
    private Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl);
    private final Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl);

    /**
     * The context of the notification. This is the app context of the package posting the
@@ -121,23 +120,21 @@ public class MediaNotificationProcessor {
                drawable.setBounds(0, 0, width, height);
                drawable.draw(canvas);

                // for the background we only take the left side of the image to ensure
                // a smooth transition
                Palette.Builder paletteBuilder = Palette.from(bitmap)
                        .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight())
                        .clearFilters() // we want all colors, red / white / black ones too!
                        .resizeBitmapArea(RESIZE_BITMAP_AREA);
                Palette.Builder paletteBuilder = generateArtworkPaletteBuilder(bitmap);
                Palette palette = paletteBuilder.generate();
                backgroundColor = findBackgroundColorAndFilter(palette);
                Palette.Swatch backgroundSwatch = findBackgroundSwatch(palette);
                backgroundColor = backgroundSwatch.getRgb();
                // we want most of the full region again, slightly shifted to the right
                float textColorStartWidthFraction = 0.4f;
                paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0,
                        bitmap.getWidth(),
                        bitmap.getHeight());
                if (mFilteredBackgroundHsl != null) {
                // We're not filtering on white or black
                if (!isWhiteOrBlack(backgroundSwatch.getHsl())) {
                    final float backgroundHue = backgroundSwatch.getHsl()[0];
                    paletteBuilder.addFilter((rgb, hsl) -> {
                        // at least 10 degrees hue difference
                        float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]);
                        float diff = Math.abs(hsl[0] - backgroundHue);
                        return diff > 10 && diff < 350;
                    });
                }
@@ -244,18 +241,31 @@ public class MediaNotificationProcessor {
                && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION);
    }

    private int findBackgroundColorAndFilter(Palette palette) {
    /**
     * Finds an appropriate background swatch from media artwork.
     *
     * @param artwork Media artwork
     * @return Swatch that should be used as the background of the media notification.
     */
    public static Palette.Swatch findBackgroundSwatch(Bitmap artwork) {
        return findBackgroundSwatch(generateArtworkPaletteBuilder(artwork).generate());
    }

    /**
     * Finds an appropriate background swatch from the palette of media artwork.
     *
     * @param palette Artwork palette, should be obtained from {@link generateArtworkPaletteBuilder}
     * @return Swatch that should be used as the background of the media notification.
     */
    private static Palette.Swatch findBackgroundSwatch(Palette palette) {
        // by default we use the dominant palette
        Palette.Swatch dominantSwatch = palette.getDominantSwatch();
        if (dominantSwatch == null) {
            // We're not filtering on white or black
            mFilteredBackgroundHsl = null;
            return Color.WHITE;
            return new Palette.Swatch(Color.WHITE, 100);
        }

        if (!isWhiteOrBlack(dominantSwatch.getHsl())) {
            mFilteredBackgroundHsl = dominantSwatch.getHsl();
            return dominantSwatch.getRgb();
            return dominantSwatch;
        }
        // Oh well, we selected black or white. Lets look at the second color!
        List<Palette.Swatch> swatches = palette.getSwatches();
@@ -270,38 +280,51 @@ public class MediaNotificationProcessor {
            }
        }
        if (second == null) {
            // We're not filtering on white or black
            mFilteredBackgroundHsl = null;
            return dominantSwatch.getRgb();
            return dominantSwatch;
        }
        if (dominantSwatch.getPopulation() / highestNonWhitePopulation
                > POPULATION_FRACTION_FOR_WHITE_OR_BLACK) {
            // The dominant swatch is very dominant, lets take it!
            // We're not filtering on white or black
            mFilteredBackgroundHsl = null;
            return dominantSwatch.getRgb();
            return dominantSwatch;
        } else {
            mFilteredBackgroundHsl = second.getHsl();
            return second.getRgb();
            return second;
        }
    }

    private boolean isWhiteOrBlack(float[] hsl) {
        return isBlack(hsl) || isWhite(hsl);
    /**
     * Generate a palette builder for media artwork.
     *
     * For producing a smooth background transition, the palette is extracted from only the left
     * side of the artwork.
     *
     * @param artwork Media artwork
     * @return Builder that generates the {@link Palette} for the media artwork.
     */
    private static Palette.Builder generateArtworkPaletteBuilder(Bitmap artwork) {
        // for the background we only take the left side of the image to ensure
        // a smooth transition
        return Palette.from(artwork)
                .setRegion(0, 0, artwork.getWidth() / 2, artwork.getHeight())
                .clearFilters() // we want all colors, red / white / black ones too!
                .resizeBitmapArea(RESIZE_BITMAP_AREA);
    }

    private static boolean isWhiteOrBlack(float[] hsl) {
        return isBlack(hsl) || isWhite(hsl);
    }

    /**
     * @return true if the color represents a color which is close to black.
     */
    private boolean isBlack(float[] hslColor) {
    private static boolean isBlack(float[] hslColor) {
        return hslColor[2] <= BLACK_MAX_LIGHTNESS;
    }

    /**
     * @return true if the color represents a color which is close to white.
     */
    private boolean isWhite(float[] hslColor) {
    private static boolean isWhite(float[] hslColor) {
        return hslColor[2] >= WHITE_MIN_LIGHTNESS;
    }
}
+54 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.systemui.statusbar.notification;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertNotSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -24,17 +26,22 @@ import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;

import android.annotation.Nullable;
import android.app.Notification;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.test.suitebuilder.annotation.SmallTest;
import android.widget.RemoteViews;

import androidx.palette.graphics.Palette;
import androidx.test.runner.AndroidJUnit4;

import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -43,9 +50,18 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class MediaNotificationProcessorTest extends SysuiTestCase {

    private static final int BITMAP_WIDTH = 10;
    private static final int BITMAP_HEIGHT = 10;

    /**
     * Color tolerance is borrowed from the AndroidX test utilities for Palette.
     */
    private static final int COLOR_TOLERANCE = 8;

    private MediaNotificationProcessor mProcessor;
    private Bitmap mBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
    private ImageGradientColorizer mColorizer;
    @Nullable private Bitmap mArtwork;

    @Before
    public void setUp() {
@@ -53,6 +69,14 @@ public class MediaNotificationProcessorTest extends SysuiTestCase {
        mProcessor = new MediaNotificationProcessor(getContext(), getContext(), mColorizer);
    }

    @After
    public void tearDown() {
        if (mArtwork != null) {
            mArtwork.recycle();
            mArtwork = null;
        }
    }

    @Test
    public void testColorizedWithLargeIcon() {
        Notification.Builder builder = new Notification.Builder(getContext()).setSmallIcon(
@@ -100,6 +124,36 @@ public class MediaNotificationProcessorTest extends SysuiTestCase {
        assertNotSame(contentView, remoteViews);
    }

    @Test
    public void findBackgroundSwatch_white() {
        // Given artwork that is completely white.
        mArtwork = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(mArtwork);
        canvas.drawColor(Color.WHITE);
        // WHEN the background swatch is computed
        Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(mArtwork);
        // THEN the swatch color is white
        assertCloseColors(swatch.getRgb(), Color.WHITE);
    }

    @Test
    public void findBackgroundSwatch_red() {
        // Given artwork that is completely red.
        mArtwork = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(mArtwork);
        canvas.drawColor(Color.RED);
        // WHEN the background swatch is computed
        Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(mArtwork);
        // THEN the swatch color is red
        assertCloseColors(swatch.getRgb(), Color.RED);
    }

    static void assertCloseColors(int expected, int actual) {
        assertThat((float) Color.red(expected)).isWithin(COLOR_TOLERANCE).of(Color.red(actual));
        assertThat((float) Color.green(expected)).isWithin(COLOR_TOLERANCE).of(Color.green(actual));
        assertThat((float) Color.blue(expected)).isWithin(COLOR_TOLERANCE).of(Color.blue(actual));
    }

    public static class TestableColorizer extends ImageGradientColorizer {
        private final Bitmap mBitmap;