blob: 2901ab9439570dbd5c138c05c20b46e6fd5df615 [file] [log] [blame]
// 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 android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import org.chromium.base.Callback;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.favicon.FaviconHelper;
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.ui.widget.RoundedIconGenerator;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.content_public.browser.BrowserStartupController;
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 LOUPE_EVERYWHERE_VARIANT = "loupe_everywhere";
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.
* @return True if the search engine logo is enabled, regardless of visibility.
*/
public static boolean isSearchEngineLogoEnabled() {
return !LocaleManager.getInstance().needToCheckForSearchEnginePromo()
&& ChromeFeatureList.isInitialized()
&& ChromeFeatureList.isEnabled(ChromeFeatureList.OMNIBOX_SEARCH_ENGINE_LOGO);
}
/**
* Encapsulates complicated boolean check for reuse and readability.
* @param isOffTheRecord True if the user is currently using an incognito tab.
* @return True if we should show the search engine logo.
*/
public static boolean shouldShowSearchEngineLogo(boolean isOffTheRecord) {
return !isOffTheRecord
&& isSearchEngineLogoEnabled()
// Using the profile now, so we need to pay attention to browser initialization.
&& BrowserStartupController.get(LibraryProcessType.PROCESS_BROWSER)
.isFullBrowserStarted();
}
/**
* Encapsulates complicated boolean check for reuse and readability.
* @param isOffTheRecord True if the user is currently using an incognito tab.
* @return True if we should show the rounded search engine logo.
*/
public static boolean shouldShowRoundedSearchEngineLogo(boolean isOffTheRecord) {
return shouldShowSearchEngineLogo(isOffTheRecord) && ChromeFeatureList.isInitialized()
&& ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
ChromeFeatureList.OMNIBOX_SEARCH_ENGINE_LOGO, ROUNDED_EDGES_VARIANT, false);
}
/** Ignores the incognito state for instances where a caller would otherwise pass "false". */
static boolean isRoundedSearchEngineLogoEnabled() {
return shouldShowRoundedSearchEngineLogo(false);
}
/**
* Encapsulates complicated boolean check for reuse and readability.
* @param isOffTheRecord True if the user is currently using an incognito tab.
* @return True if we should show the search engine logo as a loupe everywhere.
*/
public static boolean shouldShowSearchLoupeEverywhere(boolean isOffTheRecord) {
return shouldShowSearchEngineLogo(isOffTheRecord)
&& ChromeFeatureList.getFieldTrialParamByFeatureAsBoolean(
ChromeFeatureList.OMNIBOX_SEARCH_ENGINE_LOGO, LOUPE_EVERYWHERE_VARIANT,
false);
}
/**
* @return True if the search engine is Google.
*/
public static boolean isSearchEngineGoogle() {
return TemplateUrlServiceFactory.get().isDefaultSearchEngineGoogle();
}
/**
* @return True if the given url is the same domain as the DSE.
*/
public static boolean doesUrlMatchDefaultSearchEngine(String url) {
if (TextUtils.isEmpty(url)) return false;
return UrlUtilities.sameDomainOrHost(url, getSearchLogoUrl(), false);
}
/**
* @return The search URL of the current DSE or null if one cannot be found.
*/
@Nullable
public static String getSearchLogoUrl() {
String logoUrlWithPath =
TemplateUrlServiceFactory.get().getUrlForSearchQuery(DUMMY_URL_QUERY);
if (logoUrlWithPath == null || !UrlUtilities.isHttpOrHttps(logoUrlWithPath)) {
return logoUrlWithPath;
}
return UrlUtilities.stripPath(logoUrlWithPath);
}
/**
* @param resources Android resources object, used to read the dimension.
* @return The size that the logo favicon should be.
*/
public static int getSearchEngineLogoSizePixels(Resources resources) {
if (sSearchEngineLogoTargetSizePixels == 0) {
if (isRoundedSearchEngineLogoEnabled()) {
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.
*
* @param profile The current profile.
* @param resources Provides access to Android resources.
* @param callback How the bitmap will be returned to the caller.
*/
public static void getSearchEngineLogoFavicon(
Profile profile, Resources resources, Callback<Bitmap> callback) {
if (sFaviconHelper == null) sFaviconHelper = new FaviconHelper();
String logoUrl = getSearchLogoUrl();
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, logoSizePixels, (image, iconUrl) -> {
if (image == null) {
callback.onResult(image);
return;
}
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 (isRoundedSearchEngineLogoEnabled()) {
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;
}
}