Commit 87ff5bea authored by Nicolas Dossou-Gbete's avatar Nicolas Dossou-Gbete Committed by Commit Bot

[Suggestions] Add Section support to TileGroup

Related to https://chromium-review.googlesource.com/c/568492/,
stores tile data into sections. The tiles are still transmitted
from the native side as a flat array, but when we load them we
arrange them in a map of tiles.

It should keep the current UI functioning just like before.

Some of the current assumptions are affected:
- We can have more than one tile per URL, but only one per section
- We should not wait for all the tiles before we consider the tile
load to be complete (some of the tiles will not be displayed at all)

Bug: 740905
Change-Id: Ia91c3c7abb5d05a946d0b9baff95b5a05c9e3581
Reviewed-on: https://chromium-review.googlesource.com/571753
Commit-Queue: Nicolas Dossou-Gbété <dgn@chromium.org>
Reviewed-by: default avatarMichael van Ouwerkerk <mvanouwerkerk@chromium.org>
Reviewed-by: default avatarBernhard Bauer <bauerb@chromium.org>
Cr-Commit-Position: refs/heads/master@{#491340}
parent 35af9f7c
......@@ -824,7 +824,7 @@ public class NewTabPageView extends FrameLayout implements TileGroup.Observer {
* items and there is no search provider logo.
*/
private void updateTileGridPlaceholderVisibility() {
boolean showPlaceholder = mTileGroup.hasReceivedData() && mTileGroup.getTiles().length == 0
boolean showPlaceholder = mTileGroup.hasReceivedData() && mTileGroup.getAllTiles().isEmpty()
&& !mSearchProviderHasLogo;
mNoSearchLogoSpacer.setVisibility(
......@@ -909,7 +909,7 @@ public class NewTabPageView extends FrameLayout implements TileGroup.Observer {
@Override
public void onTileDataChanged() {
mTileGroup.renderTileViews(mTileGridLayout);
mTileGroup.renderTiles(mTileGridLayout);
mSnapshotTileGridChanged = true;
// The page contents are initially hidden; otherwise they'll be drawn centered on the page
......
......@@ -16,6 +16,9 @@ public class Tile implements OfflinableSuggestion {
private final String mWhitelistIconPath;
private final int mIndex;
@TileGroup.TileSectionType
private final int mSectionType;
@TileSource
private final int mSource;
......@@ -45,6 +48,8 @@ public class Tile implements OfflinableSuggestion {
mUrl = url;
mWhitelistIconPath = whitelistIconPath;
mIndex = index;
// TODO(dgn): get data from C++ after https://crrev/c/593659 lands.
mSectionType = TileGroup.TileSectionType.PERSONALIZED;
mSource = source;
}
......@@ -62,11 +67,12 @@ public class Tile implements OfflinableSuggestion {
* difference between the two that would require a redraw.
* Assumes that the current tile and the old tile (if provided) both describe the same site,
* so the URLs have to be the same.
*
* @return Whether non-transient data is different and the tile should be redrawn.
*/
public boolean importData(@Nullable Tile tile) {
if (tile == null) return true;
public boolean importData(Tile tile) {
assert tile.getUrl().equals(mUrl);
assert tile.getSectionType() == mSectionType;
mType = tile.getType();
mIcon = tile.getIcon();
......@@ -170,4 +176,9 @@ public class Tile implements OfflinableSuggestion {
public void setIcon(@Nullable Drawable icon) {
mIcon = icon;
}
@TileGroup.TileSectionType
public int getSectionType() {
return mSectionType;
}
}
......@@ -68,7 +68,7 @@ public class TileGrid extends OptionalLeaf implements TileGroup.Observer {
@Override
public void onTileDataChanged() {
setVisibilityInternal(mTileGroup.getTiles().length != 0);
setVisibilityInternal(!mTileGroup.getAllTiles().isEmpty());
if (isVisible()) notifyItemChanged(0, new ViewHolder.UpdateTilesCallback(mTileGroup));
}
......@@ -118,7 +118,7 @@ public class TileGrid extends OptionalLeaf implements TileGroup.Observer {
}
public void updateTiles(TileGroup tileGroup) {
tileGroup.renderTileViews(mLayout);
tileGroup.renderTiles(mLayout);
}
public void updateIconView(Tile tile) {
......
// 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.os.AsyncTask;
import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.favicon.LargeIconBridge;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;
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 = 48;
private final Context mContext;
private final ImageFetcher mImageFetcher;
private final RoundedIconGenerator mIconGenerator;
@TileView.Style
private final int mTileStyle;
private final int mTitleLinesCount;
private final int mDesiredIconSize;
private final int mMinIconSize;
public TileRenderer(Context context, int tileStyle, int titleLines, ImageFetcher imageFetcher) {
mContext = context;
mImageFetcher = imageFetcher;
mTileStyle = tileStyle;
mTitleLinesCount = titleLines;
Resources resources = mContext.getResources();
mDesiredIconSize = resources.getDimensionPixelSize(R.dimen.tile_view_icon_size);
// On ldpi devices, mDesiredIconSize could be even smaller than ICON_MIN_SIZE_PX.
mMinIconSize = Math.min(mDesiredIconSize, ICON_MIN_SIZE_PX);
int desiredIconSizeDp =
Math.round(mDesiredIconSize / resources.getDisplayMetrics().density);
int cornerRadiusDp;
if (tileStyle == TileView.Style.MODERN) {
cornerRadiusDp = desiredIconSizeDp / 2;
} else {
cornerRadiusDp = ICON_CORNER_RADIUS_DP;
}
int iconColor =
ApiCompatibilityUtils.getColor(resources, R.color.default_favicon_background_color);
mIconGenerator = new RoundedIconGenerator(mContext, 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<String, TileView> oldTileViews = new HashMap<>();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
TileView tileView = (TileView) parent.getChildAt(i);
oldTileViews.put(tileView.getUrl(), 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) {
TileView tileView = oldTileViews.get(tile.getUrl());
if (tileView == null) {
tileView = buildTileView(tile, parent, setupDelegate);
} else {
tileView.updateIfDataChanged(tile);
}
parent.addView(tileView);
}
}
/**
* 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
TileView buildTileView(
Tile tile, ViewGroup parentView, TileGroup.TileSetupDelegate setupDelegate) {
TileView tileView = (TileView) LayoutInflater.from(parentView.getContext())
.inflate(R.layout.tile_view, parentView, false);
tileView.initialize(tile, mTitleLinesCount, mTileStyle);
// 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, setupDelegate.createIconLoadCallback(tile));
TileGroup.TileInteractionDelegate delegate = setupDelegate.createInteractionDelegate(tile);
tileView.setOnClickListener(delegate);
tileView.setOnCreateContextMenuListener(delegate);
return tileView;
}
private void fetchIcon(final Tile tile, final LargeIconBridge.LargeIconCallback iconCallback) {
if (tile.getWhitelistIconPath().isEmpty()) {
mImageFetcher.makeLargeIconRequest(tile.getUrl(), mMinIconSize, iconCallback);
return;
}
AsyncTask<Void, Void, Bitmap> task = new AsyncTask<Void, Void, Bitmap>() {
@Override
protected Bitmap doInBackground(Void... params) {
Bitmap bitmap = BitmapFactory.decodeFile(tile.getWhitelistIconPath());
if (bitmap == null) {
Log.d(TAG, "Image decoding failed: %s", tile.getWhitelistIconPath());
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap icon) {
if (icon == null) {
mImageFetcher.makeLargeIconRequest(tile.getUrl(), mMinIconSize, iconCallback);
} else {
iconCallback.onLargeIconAvailable(icon, Color.BLACK, false);
}
}
};
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void updateIcon(Tile tile, LargeIconBridge.LargeIconCallback iconCallback) {
mImageFetcher.makeLargeIconRequest(tile.getUrl(), mMinIconSize, iconCallback);
}
public void setTileIconFromBitmap(Tile tile, Bitmap icon) {
RoundedBitmapDrawable roundedIcon =
RoundedBitmapDrawableFactory.create(mContext.getResources(), icon);
int cornerRadius = Math.round(ICON_CORNER_RADIUS_DP
* mContext.getResources().getDisplayMetrics().density * icon.getWidth()
/ mDesiredIconSize);
roundedIcon.setCornerRadius(cornerRadius);
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(mContext.getResources(), icon));
tile.setType(
isFallbackColorDefault ? TileVisualType.ICON_DEFAULT : TileVisualType.ICON_COLOR);
}
}
......@@ -1046,6 +1046,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/suggestions/SuggestionsOfflineModelObserver.java",
"java/src/org/chromium/chrome/browser/suggestions/TileGroup.java",
"java/src/org/chromium/chrome/browser/suggestions/TileGroupDelegateImpl.java",
"java/src/org/chromium/chrome/browser/suggestions/TileRenderer.java",
"java/src/org/chromium/chrome/browser/suggestions/TileView.java",
"java/src/org/chromium/chrome/browser/suggestions/NavigationRecorder.java",
"java/src/org/chromium/chrome/browser/suggestions/SuggestionsDependencyFactory.java",
......
......@@ -273,14 +273,14 @@ public class TileGroupUnitTest {
notifyTileUrlsAvailable(URLS);
// Render them to the layout.
tileGroup.renderTileViews(layout);
tileGroup.renderTiles(layout);
assertThat(layout.getChildCount(), is(2));
assertThat(((TileView) layout.getChildAt(0)).getUrl(), is(URLS[0]));
assertThat(((TileView) layout.getChildAt(1)).getUrl(), is(URLS[1]));
}
/** Check for https://crbug.com/703628: handle duplicated URLs by ignoring them. */
@Test
@Test(expected = IllegalStateException.class)
public void testRenderTileViewWithDuplicatedUrl() {
SuggestionsUiDelegate uiDelegate = mock(SuggestionsUiDelegate.class);
when(uiDelegate.getImageFetcher()).thenReturn(mock(ImageFetcher.class));
......@@ -293,11 +293,8 @@ public class TileGroupUnitTest {
// Initialise the internal list of tiles
notifyTileUrlsAvailable(URLS[0], URLS[1], URLS[0]);
// Render them to the layout. The duplicated URL is skipped.
tileGroup.renderTileViews(layout);
assertThat(layout.getChildCount(), is(2));
assertThat(((TileView) layout.getChildAt(0)).getUrl(), is(URLS[0]));
assertThat(((TileView) layout.getChildAt(1)).getUrl(), is(URLS[1]));
// Render them to the layout. The duplicated URL should trigger the exception.
tileGroup.renderTiles(layout);
}
@Test
......@@ -319,7 +316,7 @@ public class TileGroupUnitTest {
layout.addView(view2);
// The tiles should be updated, the old ones removed.
tileGroup.renderTileViews(layout);
tileGroup.renderTiles(layout);
assertThat(layout.getChildCount(), is(2));
assertThat(layout.indexOfChild(view1), is(-1));
assertThat(layout.indexOfChild(view2), is(-1));
......@@ -347,7 +344,7 @@ public class TileGroupUnitTest {
layout.addView(view2);
// The tiles should be updated, the old ones reused.
tileGroup.renderTileViews(layout);
tileGroup.renderTiles(layout);
assertThat(layout.getChildCount(), is(2));
assertThat(layout.getChildAt(0), CoreMatchers.<View>is(view1));
assertThat(layout.getChildAt(1), CoreMatchers.<View>is(view2));
......@@ -377,7 +374,7 @@ public class TileGroupUnitTest {
Tile tile = new Tile("title", URLS[0], "", 0, TileSource.POPULAR);
ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
tileGroup.buildTileView(tile, layout);
buildTileView(tile, layout, tileGroup);
// Ensure we run the callback for the new tile.
assertEquals(1, mImageFetcher.getPendingIconCallbackCount());
......@@ -395,7 +392,7 @@ public class TileGroupUnitTest {
// Notify for a second set.
notifyTileUrlsAvailable(URLS);
ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
tileGroup.renderTileViews(layout);
tileGroup.renderTiles(layout);
mImageFetcher.fulfillLargeIconRequests();
// Data changed but no loading complete event is sent
......@@ -414,7 +411,7 @@ public class TileGroupUnitTest {
notifyTileUrlsAvailable(URLS);
tileGroup.onSwitchToForeground(/* trackLoadTask: */ false);
ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
tileGroup.renderTileViews(layout);
tileGroup.renderTiles(layout);
mImageFetcher.fulfillLargeIconRequests();
// Data changed but no loading complete event is sent (same as sync)
......@@ -433,7 +430,7 @@ public class TileGroupUnitTest {
notifyTileUrlsAvailable(URLS);
tileGroup.onSwitchToForeground(/* trackLoadTask: */ true);
ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
tileGroup.renderTileViews(layout);
tileGroup.renderTiles(layout);
mImageFetcher.fulfillLargeIconRequests();
// Data changed but no loading complete event is sent
......@@ -482,13 +479,17 @@ public class TileGroupUnitTest {
notifyTileUrlsAvailable(urls);
ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
tileGroup.renderTileViews(layout);
tileGroup.renderTiles(layout);
reset(mTileGroupObserver);
reset(mTileGroupDelegate);
return tileGroup;
}
private void buildTileView(Tile tile, ViewGroup layout, TileGroup tileGroup) {
tileGroup.getTileRenderer().buildTileView(tile, layout, tileGroup.getTileSetupDelegate());
}
private class FakeTileGroupDelegate implements TileGroup.Delegate {
public MostVisitedSites.Observer mObserver;
......
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