1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "components/media_message_center/media_notification_background_impl.h"
6 
7 #include <algorithm>
8 #include <vector>
9 
10 #include "skia/ext/image_operations.h"
11 #include "ui/gfx/canvas.h"
12 #include "ui/gfx/color_analysis.h"
13 #include "ui/gfx/color_utils.h"
14 #include "ui/gfx/scoped_canvas.h"
15 #include "ui/native_theme/native_theme.h"
16 #include "ui/views/style/typography.h"
17 #include "ui/views/view.h"
18 
19 namespace media_message_center {
20 
21 namespace {
22 
23 constexpr int kMediaImageGradientWidth = 40;
24 
25 // The ratio for a background color option to be considered very popular.
26 constexpr double kMediaNotificationBackgroundColorVeryPopularRatio = 2.5;
27 
28 // The ratio for the most popular foreground color to be used.
29 constexpr double kMediaNotificationForegroundColorMostPopularRatio = 0.01;
30 
31 // The minimum saturation for the most popular foreground color to be used.
32 constexpr double kMediaNotificationForegroundColorMostPopularMinSaturation =
33     0.19;
34 
35 // The ratio for the more vibrant foreground color to use.
36 constexpr double kMediaNotificationForegroundColorMoreVibrantRatio = 1.0;
37 
38 constexpr float kMediaNotificationMinimumContrastRatio = 7.0;
39 
IsNearlyWhiteOrBlack(SkColor color)40 bool IsNearlyWhiteOrBlack(SkColor color) {
41   color_utils::HSL hsl;
42   color_utils::SkColorToHSL(color, &hsl);
43   return hsl.l >= 0.9 || hsl.l <= 0.08;
44 }
45 
GetHueDegrees(const SkColor & color)46 int GetHueDegrees(const SkColor& color) {
47   color_utils::HSL hsl;
48   color_utils::SkColorToHSL(color, &hsl);
49   return hsl.h * 360;
50 }
51 
GetSaturation(const color_utils::Swatch & swatch)52 double GetSaturation(const color_utils::Swatch& swatch) {
53   color_utils::HSL hsl;
54   color_utils::SkColorToHSL(swatch.color, &hsl);
55   return hsl.s;
56 }
57 
IsForegroundColorSwatchAllowed(const SkColor & background,const SkColor & candidate)58 bool IsForegroundColorSwatchAllowed(const SkColor& background,
59                                     const SkColor& candidate) {
60   if (IsNearlyWhiteOrBlack(candidate))
61     return false;
62 
63   if (IsNearlyWhiteOrBlack(background))
64     return true;
65 
66   int diff = abs(GetHueDegrees(candidate) - GetHueDegrees(background));
67   return diff > 10 && diff < 350;
68 }
69 
GetNotificationBackgroundColor(const SkBitmap * source)70 base::Optional<SkColor> GetNotificationBackgroundColor(const SkBitmap* source) {
71   if (!source || source->empty() || source->isNull())
72     return base::nullopt;
73 
74   std::vector<color_utils::Swatch> swatches =
75       color_utils::CalculateColorSwatches(
76           *source, 16, gfx::Rect(source->width() / 2, source->height()),
77           base::nullopt);
78 
79   if (swatches.empty())
80     return base::nullopt;
81 
82   base::Optional<color_utils::Swatch> most_popular;
83   base::Optional<color_utils::Swatch> non_white_black;
84 
85   // Find the most popular color with the most weight and the color which
86   // is the color with the most weight that is not white or black.
87   for (auto& swatch : swatches) {
88     if (!IsNearlyWhiteOrBlack(swatch.color) &&
89         (!non_white_black || swatch.population > non_white_black->population)) {
90       non_white_black = swatch;
91     }
92 
93     if (most_popular && swatch.population < most_popular->population)
94       continue;
95 
96     most_popular = swatch;
97   }
98 
99   DCHECK(most_popular);
100 
101   // If the most popular color is not white or black then we should use that.
102   if (!IsNearlyWhiteOrBlack(most_popular->color))
103     return most_popular->color;
104 
105   // If we could not find a color that is not white or black then we should
106   // use the most popular color.
107   if (!non_white_black)
108     return most_popular->color;
109 
110   // If the most popular color is very popular then we should use that color.
111   if (static_cast<double>(most_popular->population) /
112           non_white_black->population >
113       kMediaNotificationBackgroundColorVeryPopularRatio) {
114     return most_popular->color;
115   }
116 
117   return non_white_black->color;
118 }
119 
SelectVibrantSwatch(const color_utils::Swatch & more_vibrant,const color_utils::Swatch & vibrant)120 color_utils::Swatch SelectVibrantSwatch(const color_utils::Swatch& more_vibrant,
121                                         const color_utils::Swatch& vibrant) {
122   if ((static_cast<double>(more_vibrant.population) / vibrant.population) <
123       kMediaNotificationForegroundColorMoreVibrantRatio) {
124     return vibrant;
125   }
126 
127   return more_vibrant;
128 }
129 
SelectMutedSwatch(const color_utils::Swatch & muted,const color_utils::Swatch & more_muted)130 color_utils::Swatch SelectMutedSwatch(const color_utils::Swatch& muted,
131                                       const color_utils::Swatch& more_muted) {
132   double population_ratio =
133       static_cast<double>(muted.population) / more_muted.population;
134 
135   // Use the swatch with the higher saturation ratio.
136   return (GetSaturation(muted) * population_ratio > GetSaturation(more_muted))
137              ? muted
138              : more_muted;
139 }
140 
GetNotificationForegroundColor(const base::Optional<SkColor> & background_color,const SkBitmap * source)141 base::Optional<SkColor> GetNotificationForegroundColor(
142     const base::Optional<SkColor>& background_color,
143     const SkBitmap* source) {
144   if (!background_color || !source || source->empty() || source->isNull())
145     return base::nullopt;
146 
147   const bool is_light =
148       color_utils::GetRelativeLuminance(*background_color) > 0.5;
149   const SkColor fallback_color = is_light ? SK_ColorBLACK : SK_ColorWHITE;
150 
151   gfx::Rect bitmap_area(source->width(), source->height());
152   bitmap_area.Inset(source->width() * 0.4, 0, 0, 0);
153 
154   // If the background color is dark we want to look for colors that are darker
155   // and vice versa.
156   const color_utils::LumaRange more_luma_range =
157       is_light ? color_utils::LumaRange::DARK : color_utils::LumaRange::LIGHT;
158 
159   std::vector<color_utils::ColorProfile> color_profiles;
160   color_profiles.push_back(color_utils::ColorProfile(
161       more_luma_range, color_utils::SaturationRange::VIBRANT));
162   color_profiles.push_back(color_utils::ColorProfile(
163       color_utils::LumaRange::NORMAL, color_utils::SaturationRange::VIBRANT));
164   color_profiles.push_back(color_utils::ColorProfile(
165       color_utils::LumaRange::NORMAL, color_utils::SaturationRange::MUTED));
166   color_profiles.push_back(color_utils::ColorProfile(
167       more_luma_range, color_utils::SaturationRange::MUTED));
168   color_profiles.push_back(color_utils::ColorProfile(
169       color_utils::LumaRange::ANY, color_utils::SaturationRange::ANY));
170 
171   std::vector<color_utils::Swatch> best_swatches =
172       color_utils::CalculateProminentColorsOfBitmap(
173           *source, color_profiles, &bitmap_area,
174           base::BindRepeating(&IsForegroundColorSwatchAllowed,
175                               background_color.value()));
176 
177   if (best_swatches.empty())
178     return fallback_color;
179 
180   DCHECK_EQ(5u, best_swatches.size());
181 
182   const color_utils::Swatch& more_vibrant = best_swatches[0];
183   const color_utils::Swatch& vibrant = best_swatches[1];
184   const color_utils::Swatch& muted = best_swatches[2];
185   const color_utils::Swatch& more_muted = best_swatches[3];
186   const color_utils::Swatch& most_popular = best_swatches[4];
187 
188   // We are looking for a fraction that is at least 0.2% of the image.
189   const size_t population_min =
190       std::min(bitmap_area.width() * bitmap_area.height(),
191                color_utils::kMaxConsideredPixelsForSwatches) *
192       0.002;
193 
194   // This selection algorithm is an implementation of MediaNotificationProcessor
195   // from Android. It will select more vibrant colors first since they stand out
196   // better against the background. If not, it will fallback to muted colors,
197   // the most popular color and then either white/black. Any swatch has to be
198   // above a minimum population threshold to be determined significant enough in
199   // the artwork to be used.
200   base::Optional<color_utils::Swatch> swatch;
201   if (more_vibrant.population > population_min &&
202       vibrant.population > population_min) {
203     swatch = SelectVibrantSwatch(more_vibrant, vibrant);
204   } else if (more_vibrant.population > population_min) {
205     swatch = more_vibrant;
206   } else if (vibrant.population > population_min) {
207     swatch = vibrant;
208   } else if (muted.population > population_min &&
209              more_muted.population > population_min) {
210     swatch = SelectMutedSwatch(muted, more_muted);
211   } else if (muted.population > population_min) {
212     swatch = muted;
213   } else if (more_muted.population > population_min) {
214     swatch = more_muted;
215   } else if (most_popular.population > population_min) {
216     return most_popular.color;
217   } else {
218     return fallback_color;
219   }
220 
221   if (most_popular == *swatch)
222     return swatch->color;
223 
224   if (static_cast<double>(swatch->population) / most_popular.population <
225           kMediaNotificationForegroundColorMostPopularRatio &&
226       GetSaturation(most_popular) >
227           kMediaNotificationForegroundColorMostPopularMinSaturation) {
228     return most_popular.color;
229   }
230 
231   return swatch->color;
232 }
233 
234 }  // namespace
235 
MediaNotificationBackgroundImpl(int top_radius,int bottom_radius,double artwork_max_width_pct)236 MediaNotificationBackgroundImpl::MediaNotificationBackgroundImpl(
237     int top_radius,
238     int bottom_radius,
239     double artwork_max_width_pct)
240     : top_radius_(top_radius),
241       bottom_radius_(bottom_radius),
242       artwork_max_width_pct_(artwork_max_width_pct) {}
243 
244 MediaNotificationBackgroundImpl::~MediaNotificationBackgroundImpl() = default;
245 
Paint(gfx::Canvas * canvas,views::View * view) const246 void MediaNotificationBackgroundImpl::Paint(gfx::Canvas* canvas,
247                                             views::View* view) const {
248   DCHECK(view);
249 
250   gfx::ScopedCanvas scoped_canvas(canvas);
251   gfx::Rect bounds = view->GetContentsBounds();
252 
253   {
254     // Draw a rounded rectangle which the background will be clipped to. The
255     // radius is provided by the notification and can change based on where in
256     // the list the notification is.
257     const SkScalar top_radius = SkIntToScalar(top_radius_);
258     const SkScalar bottom_radius = SkIntToScalar(bottom_radius_);
259 
260     const SkScalar radii[8] = {top_radius,    top_radius,    top_radius,
261                                top_radius,    bottom_radius, bottom_radius,
262                                bottom_radius, bottom_radius};
263 
264     SkPath path;
265     path.addRoundRect(gfx::RectToSkRect(bounds), radii, SkPathDirection::kCW);
266     canvas->ClipPath(path, true);
267   }
268 
269   {
270     // Draw the artwork. The artwork is resized to the height of the view while
271     // maintaining the aspect ratio.
272     gfx::Rect source_bounds =
273         gfx::Rect(0, 0, artwork_.width(), artwork_.height());
274     gfx::Rect artwork_bounds = GetArtworkBounds(*view);
275 
276     canvas->DrawImageInt(
277         artwork_, source_bounds.x(), source_bounds.y(), source_bounds.width(),
278         source_bounds.height(), artwork_bounds.x(), artwork_bounds.y(),
279         artwork_bounds.width(), artwork_bounds.height(), false /* filter */);
280   }
281 
282   // Draw a filled rectangle which will act as the main background of the
283   // notification. This may cover up some of the artwork.
284   const SkColor background_color =
285       background_color_.value_or(GetDefaultBackgroundColor(*view));
286   canvas->FillRect(GetFilledBackgroundBounds(*view), background_color);
287 
288   {
289     // Draw a gradient to fade the color background and the image together.
290     gfx::Rect draw_bounds = GetGradientBounds(*view);
291 
292     const SkColor colors[2] = {
293         background_color, SkColorSetA(background_color, SK_AlphaTRANSPARENT)};
294     const SkPoint points[2] = {GetGradientStartPoint(draw_bounds),
295                                GetGradientEndPoint(draw_bounds)};
296 
297     cc::PaintFlags flags;
298     flags.setAntiAlias(true);
299     flags.setStyle(cc::PaintFlags::kFill_Style);
300     flags.setShader(cc::PaintShader::MakeLinearGradient(points, colors, nullptr,
301                                                         2, SkTileMode::kClamp));
302 
303     canvas->DrawRect(draw_bounds, flags);
304   }
305 
306   if (audio_device_selector_availability_) {
307     // Draw a gradient to fade the color background of the audio device picker
308     // and the image together.
309     gfx::Rect draw_bounds = GetBottomGradientBounds(*view);
310 
311     const SkColor colors[2] = {
312         background_color, SkColorSetA(background_color, SK_AlphaTRANSPARENT)};
313     const SkPoint points[2] = {gfx::PointToSkPoint(draw_bounds.bottom_center()),
314                                gfx::PointToSkPoint(draw_bounds.top_center())};
315 
316     cc::PaintFlags flags;
317     flags.setAntiAlias(true);
318     flags.setStyle(cc::PaintFlags::kFill_Style);
319     flags.setShader(cc::PaintShader::MakeLinearGradient(points, colors, nullptr,
320                                                         2, SkTileMode::kClamp));
321 
322     canvas->DrawRect(draw_bounds, flags);
323   }
324 }
325 
UpdateArtwork(const gfx::ImageSkia & image)326 void MediaNotificationBackgroundImpl::UpdateArtwork(
327     const gfx::ImageSkia& image) {
328   if (artwork_.BackedBySameObjectAs(image))
329     return;
330 
331   artwork_ = image;
332 
333   UpdateColorsInternal();
334 }
335 
UpdateCornerRadius(int top_radius,int bottom_radius)336 bool MediaNotificationBackgroundImpl::UpdateCornerRadius(int top_radius,
337                                                          int bottom_radius) {
338   if (top_radius_ == top_radius && bottom_radius_ == bottom_radius)
339     return false;
340 
341   top_radius_ = top_radius;
342   bottom_radius_ = bottom_radius;
343   return true;
344 }
345 
UpdateArtworkMaxWidthPct(double max_width_pct)346 bool MediaNotificationBackgroundImpl::UpdateArtworkMaxWidthPct(
347     double max_width_pct) {
348   if (artwork_max_width_pct_ == max_width_pct)
349     return false;
350 
351   artwork_max_width_pct_ = max_width_pct;
352   return true;
353 }
354 
UpdateFavicon(const gfx::ImageSkia & icon)355 void MediaNotificationBackgroundImpl::UpdateFavicon(
356     const gfx::ImageSkia& icon) {
357   if (favicon_.BackedBySameObjectAs(icon))
358     return;
359 
360   favicon_ = icon;
361 
362   if (!artwork_.isNull())
363     return;
364 
365   UpdateColorsInternal();
366 }
367 
UpdateDeviceSelectorAvailability(bool availability)368 void MediaNotificationBackgroundImpl::UpdateDeviceSelectorAvailability(
369     bool availability) {
370   if (audio_device_selector_availability_ == availability)
371     return;
372 
373   audio_device_selector_availability_ = availability;
374 }
375 
GetBackgroundColor(const views::View & owner) const376 SkColor MediaNotificationBackgroundImpl::GetBackgroundColor(
377     const views::View& owner) const {
378   if (background_color_.has_value())
379     return *background_color_;
380   return GetDefaultBackgroundColor(owner);
381 }
382 
GetForegroundColor(const views::View & owner) const383 SkColor MediaNotificationBackgroundImpl::GetForegroundColor(
384     const views::View& owner) const {
385   const SkColor foreground =
386       foreground_color_.has_value()
387           ? *foreground_color_
388           : views::style::GetColor(owner, views::style::CONTEXT_LABEL,
389                                    views::style::STYLE_PRIMARY);
390   return color_utils::BlendForMinContrast(
391              foreground, GetBackgroundColor(owner), base::nullopt,
392              kMediaNotificationMinimumContrastRatio)
393       .color;
394 }
395 
GetArtworkWidth(const gfx::Size & view_size) const396 int MediaNotificationBackgroundImpl::GetArtworkWidth(
397     const gfx::Size& view_size) const {
398   if (artwork_.isNull())
399     return 0;
400 
401   // Calculate the aspect ratio of the image and determine what the width of the
402   // image should be based on that ratio and the height of the notification.
403   float aspect_ratio = (float)artwork_.width() / artwork_.height();
404   return ceil(view_size.height() * aspect_ratio);
405 }
406 
GetArtworkVisibleWidth(const gfx::Size & view_size) const407 int MediaNotificationBackgroundImpl::GetArtworkVisibleWidth(
408     const gfx::Size& view_size) const {
409   // The artwork should only take up a maximum percentage of the notification.
410   return std::min(GetArtworkWidth(view_size),
411                   (int)ceil(view_size.width() * artwork_max_width_pct_));
412 }
413 
GetArtworkBounds(const views::View & owner) const414 gfx::Rect MediaNotificationBackgroundImpl::GetArtworkBounds(
415     const views::View& owner) const {
416   const gfx::Rect& view_bounds = owner.GetContentsBounds();
417   int width = GetArtworkWidth(view_bounds.size());
418   int visible_width = GetArtworkVisibleWidth(view_bounds.size());
419 
420   // This offset is for centering artwork if artwork visible width is smaller
421   // than artwork width.
422   int horizontal_offset = (width - visible_width) / 2;
423 
424   // The artwork should be positioned on the far right hand side of the
425   // notification and be the same height.
426   return owner.GetMirroredRect(
427       gfx::Rect(view_bounds.right() - width + horizontal_offset, 0, width,
428                 view_bounds.height()));
429 }
430 
GetFilledBackgroundBounds(const views::View & owner) const431 gfx::Rect MediaNotificationBackgroundImpl::GetFilledBackgroundBounds(
432     const views::View& owner) const {
433   // The filled background should take up the full notification except the area
434   // taken up by the artwork.
435   const gfx::Rect& view_bounds = owner.GetContentsBounds();
436   gfx::Rect bounds = gfx::Rect(view_bounds);
437   bounds.Inset(0, 0, GetArtworkVisibleWidth(view_bounds.size()), 0);
438   return owner.GetMirroredRect(bounds);
439 }
440 
GetGradientBounds(const views::View & owner) const441 gfx::Rect MediaNotificationBackgroundImpl::GetGradientBounds(
442     const views::View& owner) const {
443   if (artwork_.isNull())
444     return gfx::Rect(0, 0, 0, 0);
445 
446   // The gradient should appear above the artwork on the left.
447   const gfx::Rect& view_bounds = owner.GetContentsBounds();
448   return owner.GetMirroredRect(gfx::Rect(
449       view_bounds.width() - GetArtworkVisibleWidth(view_bounds.size()),
450       view_bounds.y(), kMediaImageGradientWidth, view_bounds.height()));
451 }
452 
GetBottomGradientBounds(const views::View & owner) const453 gfx::Rect MediaNotificationBackgroundImpl::GetBottomGradientBounds(
454     const views::View& owner) const {
455   if (artwork_.isNull())
456     return gfx::Rect(0, 0, 0, 0);
457 
458   const gfx::Rect& view_bounds = owner.GetContentsBounds();
459   return owner.GetMirroredRect(gfx::Rect(
460       gfx::Point(
461           view_bounds.width() - GetArtworkVisibleWidth(view_bounds.size()),
462           view_bounds.bottom() - kMediaImageGradientWidth),
463       gfx::Size(GetArtworkVisibleWidth(view_bounds.size()),
464                 kMediaImageGradientWidth)));
465 }
466 
GetGradientStartPoint(const gfx::Rect & draw_bounds) const467 SkPoint MediaNotificationBackgroundImpl::GetGradientStartPoint(
468     const gfx::Rect& draw_bounds) const {
469   return gfx::PointToSkPoint(base::i18n::IsRTL() ? draw_bounds.right_center()
470                                                  : draw_bounds.left_center());
471 }
472 
GetGradientEndPoint(const gfx::Rect & draw_bounds) const473 SkPoint MediaNotificationBackgroundImpl::GetGradientEndPoint(
474     const gfx::Rect& draw_bounds) const {
475   return gfx::PointToSkPoint(base::i18n::IsRTL() ? draw_bounds.left_center()
476                                                  : draw_bounds.right_center());
477 }
478 
GetDefaultBackgroundColor(const views::View & owner) const479 SkColor MediaNotificationBackgroundImpl::GetDefaultBackgroundColor(
480     const views::View& owner) const {
481   return owner.GetNativeTheme()->GetSystemColor(
482       ui::NativeTheme::kColorId_BubbleBackground);
483 }
484 
UpdateColorsInternal()485 void MediaNotificationBackgroundImpl::UpdateColorsInternal() {
486   // If there is an artwork, it should be used.
487   // If there is no artwork, neither a favicon, the artwork bitmap will be used
488   // which is going to be a null bitmap and produce a default value.
489   // In the case of there is a favicon and no artwork, the favicon should be
490   // used to generate the colors.
491   if (!artwork_.isNull() || favicon_.isNull()) {
492     background_color_ = GetNotificationBackgroundColor(artwork_.bitmap());
493     foreground_color_ =
494         GetNotificationForegroundColor(background_color_, artwork_.bitmap());
495     return;
496   }
497 
498   background_color_ = GetNotificationBackgroundColor(favicon_.bitmap());
499   if (background_color_) {
500     // Apply a shade factor on the color as favicons often are fairly bright.
501     *background_color_ = SkColorSetRGB(
502         SkColorGetR(*background_color_) * kBackgroundFaviconColorShadeFactor,
503         SkColorGetG(*background_color_) * kBackgroundFaviconColorShadeFactor,
504         SkColorGetB(*background_color_) * kBackgroundFaviconColorShadeFactor);
505   }
506   foreground_color_ =
507       GetNotificationForegroundColor(background_color_, favicon_.bitmap());
508 }
509 
510 }  // namespace media_message_center
511