blob: ed513b6e62c64ab66ab96a428ead5d25642ce512 [file] [log] [blame]
// Copyright 2017 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.suggestions;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.support.annotation.LayoutRes;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.AsyncTask;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.favicon.IconType;
import org.chromium.chrome.browser.favicon.LargeIconBridge;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.util.ViewUtils;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;
import org.chromium.chrome.browser.widget.tile.TileWithTextView;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.feature_engagement.Tracker;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Utility class that renders {@link Tile}s into a provided {@link ViewGroup}, creating and
* manipulating the views as needed.
*/
public class TileRenderer {
private static final String TAG = "TileRenderer";
private static final int ICON_CORNER_RADIUS_DP = 4;
private static final int ICON_TEXT_SIZE_DP = 20;
private static final int ICON_MIN_SIZE_PX = 24;
private final Resources mResources;
private final ImageFetcher mImageFetcher;
private final RoundedIconGenerator mIconGenerator;
@TileWithTextView.Style
private final int mStyle;
private final int mTitleLinesCount;
private final int mDesiredIconSize;
private final int mMinIconSize;
@LayoutRes
private final int mLayout;
public TileRenderer(Context context, @TileWithTextView.Style int style, int titleLines,
ImageFetcher imageFetcher) {
mImageFetcher = imageFetcher;
mStyle = style;
mTitleLinesCount = titleLines;
mResources = context.getResources();
mDesiredIconSize = mResources.getDimensionPixelSize(R.dimen.tile_view_icon_size);
int desiredIconSizeDp =
Math.round(mDesiredIconSize / mResources.getDisplayMetrics().density);
// On ldpi devices, mDesiredIconSize could be even smaller than the global limit.
mMinIconSize = Math.min(mDesiredIconSize, ICON_MIN_SIZE_PX);
mLayout = getLayout();
int cornerRadiusDp = desiredIconSizeDp / 2;
int iconColor = ApiCompatibilityUtils.getColor(
mResources, R.color.default_favicon_background_color);
mIconGenerator = new RoundedIconGenerator(mResources, desiredIconSizeDp, desiredIconSizeDp,
cornerRadiusDp, iconColor, ICON_TEXT_SIZE_DP);
}
/**
* Renders tile views in the given {@link ViewGroup}, reusing existing tile views where
* possible because view inflation and icon loading are slow.
* @param parent The layout to render the tile views into.
* @param sectionTiles Tiles to render.
* @param setupDelegate Delegate used to setup callbacks and listeners for the new views.
*/
public void renderTileSection(
List<Tile> sectionTiles, ViewGroup parent, TileGroup.TileSetupDelegate setupDelegate) {
// Map the old tile views by url so they can be reused later.
Map<SiteSuggestion, SuggestionsTileView> oldTileViews = new HashMap<>();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
SuggestionsTileView tileView = (SuggestionsTileView) parent.getChildAt(i);
oldTileViews.put(tileView.getData(), tileView);
}
// Remove all views from the layout because even if they are reused later they'll have to be
// added back in the correct order.
parent.removeAllViews();
for (Tile tile : sectionTiles) {
SuggestionsTileView tileView = oldTileViews.get(tile.getData());
if (tileView == null) {
tileView = buildTileView(tile, parent, setupDelegate);
}
parent.addView(tileView);
}
}
/**
* Record that the homepage tile was clicked for IPH reasons.
*/
private void recordHomepageTileClickedForIPH() {
Tracker tracker = TrackerFactory.getTrackerForProfile(Profile.getLastUsedProfile());
tracker.notifyEvent(EventConstants.HOMEPAGE_TILE_CLICKED);
}
/**
* Inflates a new tile view, initializes it, and loads an icon for it.
* @param tile The tile that holds the data to populate the new tile view.
* @param parentView The parent of the new tile view.
* @param setupDelegate The delegate used to setup callbacks and listeners for the new view.
* @return The new tile view.
*/
@VisibleForTesting
SuggestionsTileView buildTileView(
Tile tile, ViewGroup parentView, TileGroup.TileSetupDelegate setupDelegate) {
SuggestionsTileView tileView =
(SuggestionsTileView) LayoutInflater.from(parentView.getContext())
.inflate(mLayout, parentView, false);
tileView.initialize(tile, mTitleLinesCount, mStyle);
// Note: It is important that the callbacks below don't keep a reference to the tile or
// modify them as there is no guarantee that the same tile would be used to update the view.
fetchIcon(tile.getData(), setupDelegate.createIconLoadCallback(tile));
TileGroup.TileInteractionDelegate delegate = setupDelegate.createInteractionDelegate(tile);
if (tile.getSource() == TileSource.HOMEPAGE) {
delegate.setOnClickRunnable(this ::recordHomepageTileClickedForIPH);
}
tileView.setOnClickListener(delegate);
tileView.setOnCreateContextMenuListener(delegate);
return tileView;
}
private void fetchIcon(
final SiteSuggestion siteData, final LargeIconBridge.LargeIconCallback iconCallback) {
if (siteData.whitelistIconPath.isEmpty()) {
mImageFetcher.makeLargeIconRequest(siteData.url, mMinIconSize, iconCallback);
return;
}
AsyncTask<Bitmap> task = new AsyncTask<Bitmap>() {
@Override
protected Bitmap doInBackground() {
Bitmap bitmap = BitmapFactory.decodeFile(siteData.whitelistIconPath);
if (bitmap == null) {
Log.d(TAG, "Image decoding failed: %s", siteData.whitelistIconPath);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap icon) {
if (icon == null) {
mImageFetcher.makeLargeIconRequest(siteData.url, mMinIconSize, iconCallback);
} else {
iconCallback.onLargeIconAvailable(icon, Color.BLACK, false, IconType.INVALID);
}
}
};
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void updateIcon(
SiteSuggestion siteData, LargeIconBridge.LargeIconCallback iconCallback) {
mImageFetcher.makeLargeIconRequest(siteData.url, mMinIconSize, iconCallback);
}
public void setTileIconFromBitmap(Tile tile, Bitmap icon) {
RoundedBitmapDrawable roundedIcon = ViewUtils.createRoundedBitmapDrawable(icon,
Math.round(ICON_CORNER_RADIUS_DP * mResources.getDisplayMetrics().density
* icon.getWidth() / mDesiredIconSize));
roundedIcon.setAntiAlias(true);
roundedIcon.setFilterBitmap(true);
tile.setIcon(roundedIcon);
tile.setType(TileVisualType.ICON_REAL);
}
public void setTileIconFromColor(Tile tile, int fallbackColor, boolean isFallbackColorDefault) {
mIconGenerator.setBackgroundColor(fallbackColor);
Bitmap icon = mIconGenerator.generateIconForUrl(tile.getUrl());
tile.setIcon(new BitmapDrawable(mResources, icon));
tile.setType(
isFallbackColorDefault ? TileVisualType.ICON_DEFAULT : TileVisualType.ICON_COLOR);
}
@LayoutRes
private int getLayout() {
switch (mStyle) {
case TileWithTextView.Style.MODERN:
return R.layout.suggestions_tile_view;
case TileWithTextView.Style.MODERN_CONDENSED:
return R.layout.suggestions_tile_view_condensed;
}
assert false;
return 0;
}
}