Commit b57a5dc2 authored by Brandon Wylie's avatar Brandon Wylie Committed by Commit Bot

Add experimental flag to round the corners of the omnibox dse logo

This color samples the edges of the dse logo and creates a background
that the logo is then laid on top of.

Bug: 993416
Change-Id: I64ccd7358165334914eff54563f71f141dd4d4db
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1737331
Commit-Queue: Brandon Wylie <wylieb@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Reviewed-by: default avatarEnder <ender@google.com>
Cr-Commit-Position: refs/heads/master@{#686672}
parent a08dc036
......@@ -146,6 +146,7 @@ chrome_junit_test_java_sources = [
"junit/src/org/chromium/chrome/browser/omnibox/AutocompleteEditTextTest.java",
"junit/src/org/chromium/chrome/browser/omnibox/AutocompleteStateUnitTest.java",
"junit/src/org/chromium/chrome/browser/omnibox/KeyboardHideHelperUnitTest.java",
"junit/src/org/chromium/chrome/browser/omnibox/SearchEngineLogoUtilsTest.java",
"junit/src/org/chromium/chrome/browser/omnibox/SpannableAutocompleteEditTextModelUnitTest.java",
"junit/src/org/chromium/chrome/browser/omnibox/UrlBarDataTest.java",
"junit/src/org/chromium/chrome/browser/omnibox/UrlBarMediatorUnitTest.java",
......
......@@ -327,7 +327,9 @@
<dimen name="omnibox_suggestion_refine_view_modern_end_padding">4dp</dimen>
<!-- Adding search engine logo to the omnibox. -->
<dimen name="omnibox_search_engine_logo_favicon_size">24dp</dimen>
<!-- Max size which will fit completely in the composed/rounded bg. -->
<dimen name="omnibox_search_engine_logo_favicon_size">17dp</dimen>
<dimen name="omnibox_search_engine_logo_composed_size">24dp</dimen>
<!-- NTP dimensions -->
<dimen name="tile_grid_layout_max_width">504dp</dimen>
......
......@@ -6,10 +6,13 @@ package org.chromium.chrome.browser.omnibox;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.chromium.base.Callback;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.favicon.FaviconHelper;
......@@ -17,16 +20,27 @@ import org.chromium.chrome.browser.locale.LocaleManager;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;
import java.util.HashMap;
import java.util.Map;
/**
* Collection of shared code for displaying search engine logos.
*/
public class SearchEngineLogoUtils {
private static final String ROUNDED_EDGES_VARIANT = "rounded_edges";
private static final String DUMMY_URL_QUERY = "replace_me";
// Cache the logo and return it when the logo url that's cached matches the current logo url.
private static Bitmap sCachedComposedBackground;
private static String sCachedComposedBackgroundLogoUrl;
private static FaviconHelper sFaviconHelper;
private static RoundedIconGenerator sRoundedIconGenerator;
// Cache these values so they don't need to be recalculated.
private static int sSearchEngineLogoTargetSizePixels;
private static int sSearchEngineLogoComposedSizePixels;
/**
* Encapsulates complicated boolean check for reuse and readability.
......@@ -38,6 +52,13 @@ public class SearchEngineLogoUtils {
&& ChromeFeatureList.isEnabled(ChromeFeatureList.OMNIBOX_SEARCH_ENGINE_LOGO);
}
public static boolean shouldRoundedSearchEngineLogo() {
return ChromeFeatureList.isInitialized()
&& ChromeFeatureList.isEnabled(ChromeFeatureList.OMNIBOX_SEARCH_ENGINE_LOGO)
&& ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
ChromeFeatureList.OMNIBOX_SEARCH_ENGINE_LOGO, ROUNDED_EDGES_VARIANT, false);
}
/**
* @return True if the search engine is Google.
*/
......@@ -69,17 +90,35 @@ public class SearchEngineLogoUtils {
/**
* @param resources Android resources object, used to read the dimension.
* @return The size that the logo should be displayed as.
* @return The size that the logo favicon should be.
*/
public static int getSearchEngineLogoSizePixels(Resources resources) {
if (sSearchEngineLogoTargetSizePixels == 0) {
sSearchEngineLogoTargetSizePixels = resources.getDimensionPixelSize(
R.dimen.omnibox_search_engine_logo_favicon_size);
if (shouldRoundedSearchEngineLogo()) {
sSearchEngineLogoTargetSizePixels = resources.getDimensionPixelSize(
R.dimen.omnibox_search_engine_logo_favicon_size);
} else {
sSearchEngineLogoTargetSizePixels =
getSearchEngineLogoComposedSizePixels(resources);
}
}
return sSearchEngineLogoTargetSizePixels;
}
/**
* @param resources Android resources object, used to read the dimension.
* @return The total size the logo will be on screen.
*/
public static int getSearchEngineLogoComposedSizePixels(Resources resources) {
if (sSearchEngineLogoComposedSizePixels == 0) {
sSearchEngineLogoComposedSizePixels = resources.getDimensionPixelSize(
R.dimen.omnibox_search_engine_logo_composed_size);
}
return sSearchEngineLogoComposedSizePixels;
}
/**
* Get the search engine logo favicon. This can return a null bitmap under certain
* circumstances, such as: no logo url found, network/cache error, etc.
......@@ -93,19 +132,120 @@ public class SearchEngineLogoUtils {
if (sFaviconHelper == null) sFaviconHelper = new FaviconHelper();
String logoUrl = getSearchLogoUrl();
if (logoUrl == null) callback.onResult(null);
if (logoUrl == null) {
callback.onResult(null);
return;
}
// Return a cached copy if it's available.
if (sCachedComposedBackground != null
&& sCachedComposedBackgroundLogoUrl.equals(getSearchLogoUrl())) {
callback.onResult(sCachedComposedBackground);
return;
}
final int logoSizePixels = SearchEngineLogoUtils.getSearchEngineLogoSizePixels(resources);
boolean willReturn = sFaviconHelper.getLocalFaviconImageForURL(
profile, logoUrl, getSearchEngineLogoSizePixels(resources), (image, iconUrl) -> {
profile, logoUrl, logoSizePixels, (image, iconUrl) -> {
if (image == null) {
callback.onResult(image);
return;
}
callback.onResult(Bitmap.createScaledBitmap(image,
SearchEngineLogoUtils.getSearchEngineLogoSizePixels(resources),
SearchEngineLogoUtils.getSearchEngineLogoSizePixels(resources), true));
processReturnedLogo(logoUrl, image, resources, callback);
});
if (!willReturn) callback.onResult(null);
}
/**
* Process the image returned from a network fetch or cache hit. This method processes the logo
* to make it eligible for display. The logo is resized to ensure it will fill the required
* size. This is done because the icon returned from native could be a different size. If the
* rounded edges variant is active, then a smaller icon is downloaded and drawn on top of a
* circle background. This looks better and also has more predictable behavior than rounding the
* edges of the full size icon. The circle background is a solid color made up of the result
* from a call to getMostCommonEdgeColor(...).
* @param logoUrl The url for the given logo.
* @param image The logo to process.
* @param resources Android resources object used to access dimensions.
* @param callback The client callback to receive the processed logo.
*/
private static void processReturnedLogo(
String logoUrl, Bitmap image, Resources resources, Callback<Bitmap> callback) {
// Scale the logo up to the desired size.
int logoSizePixels = SearchEngineLogoUtils.getSearchEngineLogoSizePixels(resources);
Bitmap scaledIcon = Bitmap.createScaledBitmap(image,
SearchEngineLogoUtils.getSearchEngineLogoSizePixels(resources),
SearchEngineLogoUtils.getSearchEngineLogoSizePixels(resources), true);
Bitmap composedIcon = scaledIcon;
if (shouldRoundedSearchEngineLogo()) {
int composedSizePixels = getSearchEngineLogoComposedSizePixels(resources);
if (sRoundedIconGenerator == null) {
sRoundedIconGenerator = new RoundedIconGenerator(composedSizePixels,
composedSizePixels, composedSizePixels, Color.TRANSPARENT, 0);
}
sRoundedIconGenerator.setBackgroundColor(getMostCommonEdgeColor(image));
// Generate a rounded background with no text.
composedIcon = sRoundedIconGenerator.generateIconForText("");
Canvas canvas = new Canvas(composedIcon);
// Draw the logo in the middle of the generated background.
int dx = (composedSizePixels - logoSizePixels) / 2;
canvas.drawBitmap(scaledIcon, dx, dx, null);
}
// Cache the result icon to reduce future work.
sCachedComposedBackground = composedIcon;
sCachedComposedBackgroundLogoUrl = logoUrl;
callback.onResult(sCachedComposedBackground);
}
/**
* Samples the edges of given bitmap and returns the most common color.
* @param icon Bitmap to be sampled.
*/
@VisibleForTesting
static int getMostCommonEdgeColor(Bitmap icon) {
Map<Integer, Integer> colorCount = new HashMap<>();
for (int i = 0; i < icon.getWidth(); i++) {
// top edge
int color = icon.getPixel(i, 0);
System.out.println("current color: " + color);
if (!colorCount.containsKey(color)) colorCount.put(color, 0);
colorCount.put(color, colorCount.get(color) + 1);
// bottom edge
color = icon.getPixel(i, icon.getHeight() - 1);
if (!colorCount.containsKey(color)) colorCount.put(color, 0);
colorCount.put(color, colorCount.get(color) + 1);
// Measure the lateral edges offset by 1 on each side.
if (i > 0 && i < icon.getWidth() - 1) {
// left edge
color = icon.getPixel(0, i);
if (!colorCount.containsKey(color)) colorCount.put(color, 0);
colorCount.put(color, colorCount.get(color) + 1);
// right edge
color = icon.getPixel(icon.getWidth() - 1, i);
if (!colorCount.containsKey(color)) colorCount.put(color, 0);
colorCount.put(color, colorCount.get(color) + 1);
}
}
// Find the most common color out of the map.
int maxKey = Color.TRANSPARENT;
int maxVal = -1;
for (int color : colorCount.keySet()) {
int count = colorCount.get(color);
if (count > maxVal) {
maxKey = color;
maxVal = count;
}
}
assert maxVal > -1;
return maxKey;
}
}
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.omnibox;
import static org.junit.Assert.assertEquals;
import android.graphics.Bitmap;
import android.graphics.Color;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.chromium.base.test.BaseRobolectricTestRunner;
/**
* Tests for SearchEngineLogoUtils.
*/
@RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class SearchEngineLogoUtilsTest {
@Test
public void getMostCommonEdgeColor_allOneColor() {
int color = Color.BLUE;
Bitmap bitmap = createSolidImage(100, 100, color);
assertEquals(color, SearchEngineLogoUtils.getMostCommonEdgeColor(bitmap));
}
@Test
public void getMostCommonEdgeColor_outerInnerColor() {
int color = Color.BLUE;
Bitmap bitmap = createSolidImageWithDifferentInnerColor(100, 100, color, Color.RED);
assertEquals(color, SearchEngineLogoUtils.getMostCommonEdgeColor(bitmap));
}
@Test
public void getMostCommonEdgeColor_slightlyLargerColor() {
int color = Color.BLUE;
Bitmap bitmap = createSolidImageWithSlighlyLargerEdgeCoverage(100, 100, color, Color.RED);
assertEquals(color, SearchEngineLogoUtils.getMostCommonEdgeColor(bitmap));
}
private static Bitmap createSolidImage(int width, int height, int color) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
for (int x = 0; x < bitmap.getWidth(); x++) {
for (int y = 0; y < bitmap.getHeight(); y++) {
bitmap.setPixel(x, y, color);
}
}
return bitmap;
}
private static Bitmap createSolidImageWithDifferentInnerColor(
int width, int height, int outerColor, int innerColor) {
Bitmap bitmap = createSolidImage(width, height, outerColor);
for (int x = 1; x < bitmap.getWidth() - 1; x++) {
for (int y = 1; y < bitmap.getHeight() - 1; y++) {
bitmap.setPixel(x, y, innerColor);
}
}
return bitmap;
}
private static Bitmap createSolidImageWithSlighlyLargerEdgeCoverage(
int width, int height, int largerColor, int smallerColor) {
Bitmap bitmap = createSolidImage(width, height, largerColor);
for (int x = 0; x < bitmap.getWidth(); x++) {
for (int y = bitmap.getHeight() + 1; y < bitmap.getHeight(); y++) {
bitmap.setPixel(x, y, smallerColor);
}
}
return bitmap;
}
}
......@@ -1209,6 +1209,18 @@ const FeatureEntry::Choice kNotificationSchedulerChoices[] = {
""},
};
#if defined(OS_ANDROID)
const FeatureEntry::FeatureParam
kOmniboxSearchEngineLogoRoundedEdgesVariationConstant[] = {
{"rounded_edges", "true"}};
const FeatureEntry::FeatureVariation
kOmniboxSearchEngineLogoFeatureVariations[] = {
{"(rounded edges)",
kOmniboxSearchEngineLogoRoundedEdgesVariationConstant,
base::size(kOmniboxSearchEngineLogoRoundedEdgesVariationConstant),
nullptr}};
#endif // OS_ANDROID
// RECORDING USER METRICS FOR FLAGS:
// -----------------------------------------------------------------------------
// The first line of the entry is the internal name.
......@@ -2640,7 +2652,9 @@ const FeatureEntry kFeatureEntries[] = {
{"omnibox-search-engine-logo",
flag_descriptions::kOmniboxSearchEngineLogoName,
flag_descriptions::kOmniboxSearchEngineLogoDescription, kOsAndroid,
FEATURE_VALUE_TYPE(omnibox::kOmniboxSearchEngineLogo)},
FEATURE_WITH_PARAMS_VALUE_TYPE(omnibox::kOmniboxSearchEngineLogo,
kOmniboxSearchEngineLogoFeatureVariations,
"OmniboxSearchEngineLogo")},
#endif // defined(OS_ANDROID)
{"omnibox-rich-entity-suggestions",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment