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

Commit a3fb334c authored by Robert Snoeberger's avatar Robert Snoeberger Committed by Android (Google) Code Review
Browse files

Merge "Compute background color." into qt-dev

parents df10681a fb1e9263
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;