// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "chrome/browser/ui/ash/app_icon_color_cache/app_icon_color_cache.h"
#include <array>
#include <memory>
#include <optional>
#include <set>
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "base/feature_list.h"
#include "base/no_destructor.h"
#include "base/trace_event/trace_event.h"
#include "base/values.h"
#include "chrome/browser/profiles/profile.h"
#include "components/prefs/pref_service.h"
#include "services/preferences/public/cpp/dictionary_value_update.h"
#include "services/preferences/public/cpp/scoped_pref_update.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/color_analysis.h"
#include "ui/gfx/image/image_skia.h"
namespace ash {
BASE_FEATURE(kEnablePersistentAshIconColorCache,
"EnablePersistentAshIconColorCache",
base::FEATURE_ENABLED_BY_DEFAULT);
namespace {
// Constants -------------------------------------------------------------------
// An hsv color with a value less than this cutoff will be categorized as black.
constexpr float kBlackValueCutoff = 0.35f;
// When an hsv color has a saturation below 'kBlackWhiteSaturationCutoff' then
// if its value is below this cutoff it will be categorized as white and with a
// value above this cutoff is will be categorized as black.
constexpr float kBlackWhiteLowSaturatonValueCutoff = 0.9f;
// An hsv color with saturation below this cutoff will be categorized as either
// black or white.
constexpr float kBlackWhiteSaturationCutoff = 0.1f;
// A default return value for the GetLightVibrantColorForApp().
constexpr SkColor kDefaultLightVibrantColor = SK_ColorWHITE;
// On the 360 degree hue color spectrum, this value is used as a cutuff to
// indicate that any value equal to or higher than this is considered red.
constexpr float kRedHueCutoff = 315.0f;
// Utilities for the vibrant color prefs cache ---------------------------------
// Returns the vibrant color of the app icon specified by `key` from the
// prefs cache associated with `profile`. Returns `std::nullopt` if the queried
// color cannot be found.
std::optional<SkColor> GetLightVibrantColorForAppFromPrefsCache(
Profile* profile,
const std::string& key) {
return profile->GetPrefs()
->GetDict(prefs::kAshAppIconLightVibrantColorCache)
.FindInt(key);
}
// Sets the vibrant color of the app icon specified by `key` in the prefs
// cache associated with the `profile`. Overwrites the existing color in the
// cache if any.
void SetLightVibrantColorForAppInPrefsCache(Profile* profile,
const std::string& key,
SkColor color) {
::prefs::ScopedDictionaryPrefUpdate(profile->GetPrefs(),
prefs::kAshAppIconLightVibrantColorCache)
->SetInteger(key, color);
}
// Utilities for the icon color prefs cache -----------------------------------
// NOTE: Prefs cache can only store primitive types. Therefore, we cache color
// groups and hues instead of `IconColor` instances.
void SetIntegerInPrefsDict(Profile* profile,
const std::string& dictionary,
const std::string& key,
int value) {
::prefs::ScopedDictionaryPrefUpdate(profile->GetPrefs(), dictionary)
->SetInteger(key, value);
}
void RemoveEntryFromColorPrefsCache(Profile* profile,
const std::string& dictionary,
const std::string& key) {
::prefs::ScopedDictionaryPrefUpdate(profile->GetPrefs(), dictionary)
->Remove(key);
}
std::optional<sync_pb::AppListSpecifics::ColorGroup>
GetColorGroupFromPrefsCache(Profile* profile, const std::string& app_id) {
const auto result = profile->GetPrefs()
->GetDict(prefs::kAshAppIconSortableColorGroupCache)
.FindInt(app_id);
return result && sync_pb::AppListSpecifics::ColorGroup_IsValid(*result)
? std::optional<sync_pb::AppListSpecifics::ColorGroup>(
sync_pb::AppListSpecifics::ColorGroup(*result))
: std::nullopt;
}
std::optional<int> GetHueFromPrefsCache(Profile* profile,
const std::string& app_id) {
const auto result = profile->GetPrefs()
->GetDict(prefs::kAshAppIconSortableColorHueCache)
.FindInt(app_id);
return result >= IconColor::kHueMin && result <= IconColor::kHueMax
? result
: std::nullopt;
}
void StoreColorGroupToPrefsCache(
Profile* profile,
const std::string& app_id,
sync_pb::AppListSpecifics::ColorGroup color_group) {
SetIntegerInPrefsDict(profile, prefs::kAshAppIconSortableColorGroupCache,
app_id, color_group);
}
void StoreHueToPrefsCache(Profile* profile,
const std::string& app_id,
int hue) {
SetIntegerInPrefsDict(profile, prefs::kAshAppIconSortableColorHueCache,
app_id, hue);
}
// Uses the icon image to calculate the light vibrant color.
std::optional<SkColor> CalculateLightVibrantColor(const gfx::ImageSkia& image) {
TRACE_EVENT0("ui",
"app_icon_color_cache::{anonynous}::CalculateLightVibrantColor");
const SkBitmap* source = image.bitmap();
if (!source || source->empty() || source->isNull())
return std::nullopt;
std::vector<color_utils::ColorProfile> color_profiles;
color_profiles.emplace_back(color_utils::LumaRange::LIGHT,
color_utils::SaturationRange::VIBRANT);
std::vector<color_utils::Swatch> best_swatches =
color_utils::CalculateProminentColorsOfBitmap(
*source, color_profiles, nullptr /* bitmap region */,
color_utils::ColorSwatchFilter());
// If the best swatch color is transparent, then
// CalculateProminentColorsOfBitmap() failed to find a suitable color.
if (best_swatches.empty() || best_swatches[0].color == SK_ColorTRANSPARENT)
return std::nullopt;
return best_swatches[0].color;
}
// Categorizes `color` into one color group.
sync_pb::AppListSpecifics::ColorGroup ColorToColorGroup(SkColor color) {
TRACE_EVENT0("ui", "app_list::reorder::ColorToColorGroup");
SkScalar hsv[3];
SkColorToHSV(color, hsv);
const float h = hsv[0];
const float s = hsv[1];
const float v = hsv[2];
sync_pb::AppListSpecifics::ColorGroup group;
// Assign the ColorGroup based on the hue of `color`. Each if statement checks
// the value of the hue and groups the color based on it. These values are
// approximations for grouping like colors together.
if (h < 15) {
group = sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_RED;
} else if (h < 45) {
group = sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_ORANGE;
} else if (h < 75) {
group = sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_YELLOW;
} else if (h < 182) {
group = sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_GREEN;
} else if (h < 255) {
group = sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_BLUE;
} else if (h < kRedHueCutoff) {
group = sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_MAGENTA;
} else {
group = sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_RED;
}
if (s < kBlackWhiteSaturationCutoff) {
if (v < kBlackWhiteLowSaturatonValueCutoff) {
group = sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_BLACK;
} else {
group = sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_WHITE;
}
} else if (v < kBlackValueCutoff) {
group = sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_BLACK;
}
return group;
}
// Calculates the color group of the background of `source`.
// Samples color from the left, right, and top edge of the icon image and
// determines the color group for each. Returns the most common grouping from
// the samples. If all three sampled groups are different, then returns
// 'light_vibrant_group' which is the color group for the light vibrant color of
// the whole icon image.
sync_pb::AppListSpecifics::ColorGroup CalculateBackgroundColorGroup(
const SkBitmap& source,
sync_pb::AppListSpecifics::ColorGroup light_vibrant_group) {
TRACE_EVENT0("ui", "app_list::reorder::CalculateBackgroundColorGroup");
if (source.empty()) {
return sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_WHITE;
}
DCHECK_EQ(kN32_SkColorType, source.info().colorType());
const int width = source.width();
const int height = source.height();
sync_pb::AppListSpecifics::ColorGroup left_group = sync_pb::AppListSpecifics::
ColorGroup::AppListSpecifics_ColorGroup_COLOR_BLACK;
sync_pb::AppListSpecifics::ColorGroup right_group = sync_pb::
AppListSpecifics::ColorGroup::AppListSpecifics_ColorGroup_COLOR_BLACK;
// Find the color group for the first opaque pixel on the left edge of the
// icon.
const SkColor* current =
reinterpret_cast<SkColor*>(source.getAddr32(0, height / 2));
for (int x = 0; x < width; ++x, ++current) {
if (SkColorGetA(*current) < SK_AlphaOPAQUE) {
continue;
}
left_group = ColorToColorGroup(*current);
break;
}
// Find the color group for the first opaque pixel on the right edge of the
// icon.
current = reinterpret_cast<SkColor*>(source.getAddr32(width - 1, height / 2));
for (int x = width - 1; x >= 0; --x, --current) {
if (SkColorGetA(*current) < SK_AlphaOPAQUE) {
continue;
}
right_group = ColorToColorGroup(*current);
break;
}
// If the left and right edge have the same color grouping, then return that
// group as the calculated background color group.
if (left_group == right_group) {
return left_group;
}
// Find the color group for the first opaque pixel on the top edge of the
// icon.
sync_pb::AppListSpecifics::ColorGroup top_group = sync_pb::AppListSpecifics::
ColorGroup::AppListSpecifics_ColorGroup_COLOR_BLACK;
current = reinterpret_cast<SkColor*>(source.getAddr32(width / 2, 0));
for (int y = 0; y < height; ++y, current += width) {
if (SkColorGetA(*current) < SK_AlphaOPAQUE) {
continue;
}
top_group = ColorToColorGroup(*current);
break;
}
// If the top edge has a matching color group with the left or right group,
// then return that group.
if (top_group == right_group || top_group == left_group) {
return top_group;
}
// When all three sampled color groups are different, then there is no
// conclusive color group for the icon's background. Return the group
// corresponding to the app icon's light vibrant color.
return light_vibrant_group;
}
// Returns a `IconColor` which can be used to sort icons by their
// background color and light vibrant color.
IconColor CalculateIconColorForApp(const std::string& id,
SkColor extracted_light_vibrant_color,
const gfx::ImageSkia& image) {
TRACE_EVENT0("ui", "app_icon_color_cache::CalculateIconColorForApp");
const sync_pb::AppListSpecifics::ColorGroup light_vibrant_color_group =
ColorToColorGroup(extracted_light_vibrant_color);
// `hue` represents the hue of the extracted light vibrant color and can be
// defined by the interval [-1, 360], where -1 (kHueMin) denotes that the hue
// should come before all other hue values, and 360 (kHueMax) denotes that the
// hue should come after all other hue values.
int hue;
if (light_vibrant_color_group ==
sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_BLACK) {
// If `light_vibrant_color_group` is black it should be ordered after all
// other hues.
hue = IconColor::kHueMax;
} else if (light_vibrant_color_group ==
sync_pb::AppListSpecifics::ColorGroup::
AppListSpecifics_ColorGroup_COLOR_WHITE) {
// If 'light_vibrant_color_group' is white, then the hue should be ordered
// before all other hues.
hue = IconColor::kHueMin;
} else {
SkScalar hsv[3];
SkColorToHSV(extracted_light_vibrant_color, hsv);
hue = hsv[0];
// If the hue is a red on the high end of the hsv color spectrum, then
// subtract the maximum possible hue so that reds on the high end of the hsv
// color spectrum are ordered next to reds on the low end of the hsv color
// spectrum.
if (hue >= kRedHueCutoff) {
hue -= IconColor::kHueMax;
}
// Shift up the hue value so that the returned hue value always remains
// within the interval [0, 360].
hue += (IconColor::kHueMax - kRedHueCutoff);
DCHECK_GE(hue, 0);
DCHECK_LE(hue, IconColor::kHueMax);
}
return IconColor(
CalculateBackgroundColorGroup(*image.bitmap(), light_vibrant_color_group),
hue);
}
bool IsPersistentCacheEnabled() {
return base::FeatureList::IsEnabled(kEnablePersistentAshIconColorCache);
}
std::map<Profile*, std::unique_ptr<AppIconColorCache>>&
GetInstanceStorageMap() {
static base::NoDestructor<
std::map<Profile*, std::unique_ptr<AppIconColorCache>>>
color_cache;
return *color_cache;
}
void DestroyInstance(Profile* profile) {
GetInstanceStorageMap().erase(profile);
}
} // namespace
AppIconColorCache& AppIconColorCache::GetInstance(Profile* profile) {
auto& color_cache = GetInstanceStorageMap();
auto it = color_cache.find(profile);
if (it == color_cache.end()) {
const auto [new_it, success] = color_cache.emplace(
profile, std::make_unique<AppIconColorCache>(profile));
CHECK(success);
it = new_it;
}
return *(it->second);
}
// PartitionAlloc DanglingRawPtr detector will check on unused pointer so we
// only initialize it when needed.
AppIconColorCache::AppIconColorCache(Profile* profile)
: profile_(IsPersistentCacheEnabled() ? profile : nullptr) {
// AppIconColorCache is only valid for a real user so profile must be valid
// if it is not used.
DCHECK(profile);
if (IsPersistentCacheEnabled()) {
profile_observation_.Observe(profile);
}
// Clean up cached data.
if (profile && !IsPersistentCacheEnabled()) {
for (const auto* const dictionary :
{prefs::kAshAppIconLightVibrantColorCache,
prefs::kAshAppIconSortableColorGroupCache,
prefs::kAshAppIconSortableColorHueCache}) {
profile->GetPrefs()->ClearPref(dictionary);
}
}
}
AppIconColorCache::~AppIconColorCache() = default;
void AppIconColorCache::OnProfileWillBeDestroyed(Profile* profile) {
profile_observation_.Reset();
DestroyInstance(profile_);
}
SkColor AppIconColorCache::GetLightVibrantColorForApp(
const std::string& app_id,
const gfx::ImageSkia& icon) {
if (IsPersistentCacheEnabled() && !profile_) {
// This could happen when `profile_` is removed before the destruction of
// `AppIconColorCache`.
return kDefaultLightVibrantColor;
}
AppIdLightVibrantColor::const_iterator it =
vibrant_colors_by_ids_.find(app_id);
if (it != vibrant_colors_by_ids_.end()) {
return it->second;
}
if (HasPrefsCache()) {
if (const auto result =
GetLightVibrantColorForAppFromPrefsCache(profile_, app_id)) {
vibrant_colors_by_ids_[app_id] = *result;
return *result;
}
}
SkColor light_vibrant_color =
CalculateLightVibrantColor(icon).value_or(kDefaultLightVibrantColor);
// TODO(crbug.com/40176836): Find a way to evict stale items in the
// AppIconColorCache.
vibrant_colors_by_ids_[app_id] = light_vibrant_color;
if (HasPrefsCache()) {
SetLightVibrantColorForAppInPrefsCache(profile_, app_id,
light_vibrant_color);
}
return light_vibrant_color;
}
IconColor AppIconColorCache::GetIconColorForApp(const std::string& app_id,
const gfx::ImageSkia& image) {
if (IsPersistentCacheEnabled()) {
if (!profile_) {
// This could happen when `profile_` is removed before the destruction of
// `AppIconColorCache`.
return IconColor();
}
if (const AppIdIconColor::const_iterator it =
icon_colors_by_ids_.find(app_id);
it != icon_colors_by_ids_.end()) {
return it->second;
}
}
std::optional<IconColor> result = GetIconColorForAppFromPrefsCache(app_id);
if (!result) {
result = CalculateIconColorForApp(
app_id, GetLightVibrantColorForApp(app_id, image), image);
}
if (IsPersistentCacheEnabled()) {
icon_colors_by_ids_[app_id] = *result;
}
MaybeStoreIconColorToPrefsCache(app_id, *result);
return *result;
}
void AppIconColorCache::RemoveColorDataForApp(const std::string& app_id) {
icon_colors_by_ids_.erase(app_id);
vibrant_colors_by_ids_.erase(app_id);
if (!HasPrefsCache()) {
return;
}
RemoveEntryFromColorPrefsCache(
profile_, prefs::kAshAppIconLightVibrantColorCache, app_id);
RemoveEntryFromColorPrefsCache(
profile_, prefs::kAshAppIconSortableColorGroupCache, app_id);
RemoveEntryFromColorPrefsCache(
profile_, prefs::kAshAppIconSortableColorHueCache, app_id);
}
std::optional<IconColor> AppIconColorCache::GetIconColorForAppFromPrefsCache(
const std::string& app_id) {
if (!HasPrefsCache()) {
return std::nullopt;
}
const auto color_group = GetColorGroupFromPrefsCache(profile_, app_id);
if (!color_group) {
return std::nullopt;
}
const auto hue = GetHueFromPrefsCache(profile_, app_id);
if (!hue) {
return std::nullopt;
}
return IconColor(*color_group, *hue);
}
void AppIconColorCache::MaybeStoreIconColorToPrefsCache(
const std::string& app_id,
const IconColor& icon_color) {
if (HasPrefsCache()) {
StoreColorGroupToPrefsCache(profile_, app_id,
icon_color.background_color());
StoreHueToPrefsCache(profile_, app_id, icon_color.hue());
}
}
bool AppIconColorCache::HasPrefsCache() const {
return IsPersistentCacheEnabled() && profile_;
}
} // namespace ash