Home | History | Annotate | Download | only in notification
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.systemui.statusbar.notification;
     18 
     19 import android.app.Notification;
     20 import android.content.Context;
     21 import android.graphics.Bitmap;
     22 import android.graphics.Canvas;
     23 import android.graphics.Color;
     24 import android.graphics.drawable.Drawable;
     25 import android.graphics.drawable.Icon;
     26 import android.support.annotation.VisibleForTesting;
     27 import android.support.v7.graphics.Palette;
     28 import android.util.LayoutDirection;
     29 
     30 import com.android.internal.util.NotificationColorUtil;
     31 import com.android.systemui.R;
     32 
     33 import java.util.List;
     34 
     35 /**
     36  * A class the processes media notifications and extracts the right text and background colors.
     37  */
     38 public class MediaNotificationProcessor {
     39 
     40     /**
     41      * The fraction below which we select the vibrant instead of the light/dark vibrant color
     42      */
     43     private static final float POPULATION_FRACTION_FOR_MORE_VIBRANT = 1.0f;
     44 
     45     /**
     46      * Minimum saturation that a muted color must have if there exists if deciding between two
     47      * colors
     48      */
     49     private static final float MIN_SATURATION_WHEN_DECIDING = 0.19f;
     50 
     51     /**
     52      * Minimum fraction that any color must have to be picked up as a text color
     53      */
     54     private static final double MINIMUM_IMAGE_FRACTION = 0.002;
     55 
     56     /**
     57      * The population fraction to select the dominant color as the text color over a the colored
     58      * ones.
     59      */
     60     private static final float POPULATION_FRACTION_FOR_DOMINANT = 0.01f;
     61 
     62     /**
     63      * The population fraction to select a white or black color as the background over a color.
     64      */
     65     private static final float POPULATION_FRACTION_FOR_WHITE_OR_BLACK = 2.5f;
     66     private static final float BLACK_MAX_LIGHTNESS = 0.08f;
     67     private static final float WHITE_MIN_LIGHTNESS = 0.90f;
     68     private static final int RESIZE_BITMAP_AREA = 150 * 150;
     69     private final ImageGradientColorizer mColorizer;
     70     private final Context mContext;
     71     private float[] mFilteredBackgroundHsl = null;
     72     private Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl);
     73 
     74     /**
     75      * The context of the notification. This is the app context of the package posting the
     76      * notification.
     77      */
     78     private final Context mPackageContext;
     79 
     80     public MediaNotificationProcessor(Context context, Context packageContext) {
     81         this(context, packageContext, new ImageGradientColorizer());
     82     }
     83 
     84     @VisibleForTesting
     85     MediaNotificationProcessor(Context context, Context packageContext,
     86             ImageGradientColorizer colorizer) {
     87         mContext = context;
     88         mPackageContext = packageContext;
     89         mColorizer = colorizer;
     90     }
     91 
     92     /**
     93      * Processes a builder of a media notification and calculates the appropriate colors that should
     94      * be used.
     95      *
     96      * @param notification the notification that is being processed
     97      * @param builder the recovered builder for the notification. this will be modified
     98      */
     99     public void processNotification(Notification notification, Notification.Builder builder) {
    100         Icon largeIcon = notification.getLargeIcon();
    101         Bitmap bitmap = null;
    102         Drawable drawable = null;
    103         if (largeIcon != null) {
    104             // We're transforming the builder, let's make sure all baked in RemoteViews are
    105             // rebuilt!
    106             builder.setRebuildStyledRemoteViews(true);
    107             drawable = largeIcon.loadDrawable(mPackageContext);
    108             int backgroundColor = 0;
    109             if (notification.isColorizedMedia()) {
    110                 int width = drawable.getIntrinsicWidth();
    111                 int height = drawable.getIntrinsicHeight();
    112                 int area = width * height;
    113                 if (area > RESIZE_BITMAP_AREA) {
    114                     double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area);
    115                     width = (int) (factor * width);
    116                     height = (int) (factor * height);
    117                 }
    118                 bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    119                 Canvas canvas = new Canvas(bitmap);
    120                 drawable.setBounds(0, 0, width, height);
    121                 drawable.draw(canvas);
    122 
    123                 // for the background we only take the left side of the image to ensure
    124                 // a smooth transition
    125                 Palette.Builder paletteBuilder = Palette.from(bitmap)
    126                         .setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight())
    127                         .clearFilters() // we want all colors, red / white / black ones too!
    128                         .resizeBitmapArea(RESIZE_BITMAP_AREA);
    129                 Palette palette = paletteBuilder.generate();
    130                 backgroundColor = findBackgroundColorAndFilter(palette);
    131                 // we want most of the full region again, slightly shifted to the right
    132                 float textColorStartWidthFraction = 0.4f;
    133                 paletteBuilder.setRegion((int) (bitmap.getWidth() * textColorStartWidthFraction), 0,
    134                         bitmap.getWidth(),
    135                         bitmap.getHeight());
    136                 if (mFilteredBackgroundHsl != null) {
    137                     paletteBuilder.addFilter((rgb, hsl) -> {
    138                         // at least 10 degrees hue difference
    139                         float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]);
    140                         return diff > 10 && diff < 350;
    141                     });
    142                 }
    143                 paletteBuilder.addFilter(mBlackWhiteFilter);
    144                 palette = paletteBuilder.generate();
    145                 int foregroundColor = selectForegroundColor(backgroundColor, palette);
    146                 builder.setColorPalette(backgroundColor, foregroundColor);
    147             } else {
    148                 backgroundColor = mContext.getColor(R.color.notification_material_background_color);
    149             }
    150             Bitmap colorized = mColorizer.colorize(drawable, backgroundColor,
    151                     mContext.getResources().getConfiguration().getLayoutDirection() ==
    152                             LayoutDirection.RTL);
    153             builder.setLargeIcon(Icon.createWithBitmap(colorized));
    154         }
    155     }
    156 
    157     private int selectForegroundColor(int backgroundColor, Palette palette) {
    158         if (NotificationColorUtil.isColorLight(backgroundColor)) {
    159             return selectForegroundColorForSwatches(palette.getDarkVibrantSwatch(),
    160                     palette.getVibrantSwatch(),
    161                     palette.getDarkMutedSwatch(),
    162                     palette.getMutedSwatch(),
    163                     palette.getDominantSwatch(),
    164                     Color.BLACK);
    165         } else {
    166             return selectForegroundColorForSwatches(palette.getLightVibrantSwatch(),
    167                     palette.getVibrantSwatch(),
    168                     palette.getLightMutedSwatch(),
    169                     palette.getMutedSwatch(),
    170                     palette.getDominantSwatch(),
    171                     Color.WHITE);
    172         }
    173     }
    174 
    175     private int selectForegroundColorForSwatches(Palette.Swatch moreVibrant,
    176             Palette.Swatch vibrant, Palette.Swatch moreMutedSwatch, Palette.Swatch mutedSwatch,
    177             Palette.Swatch dominantSwatch, int fallbackColor) {
    178         Palette.Swatch coloredCandidate = selectVibrantCandidate(moreVibrant, vibrant);
    179         if (coloredCandidate == null) {
    180             coloredCandidate = selectMutedCandidate(mutedSwatch, moreMutedSwatch);
    181         }
    182         if (coloredCandidate != null) {
    183             if (dominantSwatch == coloredCandidate) {
    184                 return coloredCandidate.getRgb();
    185             } else if ((float) coloredCandidate.getPopulation() / dominantSwatch.getPopulation()
    186                     < POPULATION_FRACTION_FOR_DOMINANT
    187                     && dominantSwatch.getHsl()[1] > MIN_SATURATION_WHEN_DECIDING) {
    188                 return dominantSwatch.getRgb();
    189             } else {
    190                 return coloredCandidate.getRgb();
    191             }
    192         } else if (hasEnoughPopulation(dominantSwatch)) {
    193             return dominantSwatch.getRgb();
    194         } else {
    195             return fallbackColor;
    196         }
    197     }
    198 
    199     private Palette.Swatch selectMutedCandidate(Palette.Swatch first,
    200             Palette.Swatch second) {
    201         boolean firstValid = hasEnoughPopulation(first);
    202         boolean secondValid = hasEnoughPopulation(second);
    203         if (firstValid && secondValid) {
    204             float firstSaturation = first.getHsl()[1];
    205             float secondSaturation = second.getHsl()[1];
    206             float populationFraction = first.getPopulation() / (float) second.getPopulation();
    207             if (firstSaturation * populationFraction > secondSaturation) {
    208                 return first;
    209             } else {
    210                 return second;
    211             }
    212         } else if (firstValid) {
    213             return first;
    214         } else if (secondValid) {
    215             return second;
    216         }
    217         return null;
    218     }
    219 
    220     private Palette.Swatch selectVibrantCandidate(Palette.Swatch first, Palette.Swatch second) {
    221         boolean firstValid = hasEnoughPopulation(first);
    222         boolean secondValid = hasEnoughPopulation(second);
    223         if (firstValid && secondValid) {
    224             int firstPopulation = first.getPopulation();
    225             int secondPopulation = second.getPopulation();
    226             if (firstPopulation / (float) secondPopulation
    227                     < POPULATION_FRACTION_FOR_MORE_VIBRANT) {
    228                 return second;
    229             } else {
    230                 return first;
    231             }
    232         } else if (firstValid) {
    233             return first;
    234         } else if (secondValid) {
    235             return second;
    236         }
    237         return null;
    238     }
    239 
    240     private boolean hasEnoughPopulation(Palette.Swatch swatch) {
    241         // We want a fraction that is at least 1% of the image
    242         return swatch != null
    243                 && (swatch.getPopulation() / (float) RESIZE_BITMAP_AREA > MINIMUM_IMAGE_FRACTION);
    244     }
    245 
    246     private int findBackgroundColorAndFilter(Palette palette) {
    247         // by default we use the dominant palette
    248         Palette.Swatch dominantSwatch = palette.getDominantSwatch();
    249         if (dominantSwatch == null) {
    250             // We're not filtering on white or black
    251             mFilteredBackgroundHsl = null;
    252             return Color.WHITE;
    253         }
    254 
    255         if (!isWhiteOrBlack(dominantSwatch.getHsl())) {
    256             mFilteredBackgroundHsl = dominantSwatch.getHsl();
    257             return dominantSwatch.getRgb();
    258         }
    259         // Oh well, we selected black or white. Lets look at the second color!
    260         List<Palette.Swatch> swatches = palette.getSwatches();
    261         float highestNonWhitePopulation = -1;
    262         Palette.Swatch second = null;
    263         for (Palette.Swatch swatch: swatches) {
    264             if (swatch != dominantSwatch
    265                     && swatch.getPopulation() > highestNonWhitePopulation
    266                     && !isWhiteOrBlack(swatch.getHsl())) {
    267                 second = swatch;
    268                 highestNonWhitePopulation = swatch.getPopulation();
    269             }
    270         }
    271         if (second == null) {
    272             // We're not filtering on white or black
    273             mFilteredBackgroundHsl = null;
    274             return dominantSwatch.getRgb();
    275         }
    276         if (dominantSwatch.getPopulation() / highestNonWhitePopulation
    277                 > POPULATION_FRACTION_FOR_WHITE_OR_BLACK) {
    278             // The dominant swatch is very dominant, lets take it!
    279             // We're not filtering on white or black
    280             mFilteredBackgroundHsl = null;
    281             return dominantSwatch.getRgb();
    282         } else {
    283             mFilteredBackgroundHsl = second.getHsl();
    284             return second.getRgb();
    285         }
    286     }
    287 
    288     private boolean isWhiteOrBlack(float[] hsl) {
    289         return isBlack(hsl) || isWhite(hsl);
    290     }
    291 
    292 
    293     /**
    294      * @return true if the color represents a color which is close to black.
    295      */
    296     private boolean isBlack(float[] hslColor) {
    297         return hslColor[2] <= BLACK_MAX_LIGHTNESS;
    298     }
    299 
    300     /**
    301      * @return true if the color represents a color which is close to white.
    302      */
    303     private boolean isWhite(float[] hslColor) {
    304         return hslColor[2] >= WHITE_MIN_LIGHTNESS;
    305     }
    306 }
    307