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 { ...@@ -824,7 +824,7 @@ public class NewTabPageView extends FrameLayout implements TileGroup.Observer {
* items and there is no search provider logo. * items and there is no search provider logo.
*/ */
private void updateTileGridPlaceholderVisibility() { private void updateTileGridPlaceholderVisibility() {
boolean showPlaceholder = mTileGroup.hasReceivedData() && mTileGroup.getTiles().length == 0 boolean showPlaceholder = mTileGroup.hasReceivedData() && mTileGroup.getAllTiles().isEmpty()
&& !mSearchProviderHasLogo; && !mSearchProviderHasLogo;
mNoSearchLogoSpacer.setVisibility( mNoSearchLogoSpacer.setVisibility(
...@@ -909,7 +909,7 @@ public class NewTabPageView extends FrameLayout implements TileGroup.Observer { ...@@ -909,7 +909,7 @@ public class NewTabPageView extends FrameLayout implements TileGroup.Observer {
@Override @Override
public void onTileDataChanged() { public void onTileDataChanged() {
mTileGroup.renderTileViews(mTileGridLayout); mTileGroup.renderTiles(mTileGridLayout);
mSnapshotTileGridChanged = true; mSnapshotTileGridChanged = true;
// The page contents are initially hidden; otherwise they'll be drawn centered on the page // The page contents are initially hidden; otherwise they'll be drawn centered on the page
......
...@@ -16,6 +16,9 @@ public class Tile implements OfflinableSuggestion { ...@@ -16,6 +16,9 @@ public class Tile implements OfflinableSuggestion {
private final String mWhitelistIconPath; private final String mWhitelistIconPath;
private final int mIndex; private final int mIndex;
@TileGroup.TileSectionType
private final int mSectionType;
@TileSource @TileSource
private final int mSource; private final int mSource;
...@@ -45,6 +48,8 @@ public class Tile implements OfflinableSuggestion { ...@@ -45,6 +48,8 @@ public class Tile implements OfflinableSuggestion {
mUrl = url; mUrl = url;
mWhitelistIconPath = whitelistIconPath; mWhitelistIconPath = whitelistIconPath;
mIndex = index; mIndex = index;
// TODO(dgn): get data from C++ after https://crrev/c/593659 lands.
mSectionType = TileGroup.TileSectionType.PERSONALIZED;
mSource = source; mSource = source;
} }
...@@ -62,11 +67,12 @@ public class Tile implements OfflinableSuggestion { ...@@ -62,11 +67,12 @@ public class Tile implements OfflinableSuggestion {
* difference between the two that would require a redraw. * 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, * Assumes that the current tile and the old tile (if provided) both describe the same site,
* so the URLs have to be the same. * 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) { public boolean importData(Tile tile) {
if (tile == null) return true;
assert tile.getUrl().equals(mUrl); assert tile.getUrl().equals(mUrl);
assert tile.getSectionType() == mSectionType;
mType = tile.getType(); mType = tile.getType();
mIcon = tile.getIcon(); mIcon = tile.getIcon();
...@@ -170,4 +176,9 @@ public class Tile implements OfflinableSuggestion { ...@@ -170,4 +176,9 @@ public class Tile implements OfflinableSuggestion {
public void setIcon(@Nullable Drawable icon) { public void setIcon(@Nullable Drawable icon) {
mIcon = icon; mIcon = icon;
} }
@TileGroup.TileSectionType
public int getSectionType() {
return mSectionType;
}
} }
...@@ -68,7 +68,7 @@ public class TileGrid extends OptionalLeaf implements TileGroup.Observer { ...@@ -68,7 +68,7 @@ public class TileGrid extends OptionalLeaf implements TileGroup.Observer {
@Override @Override
public void onTileDataChanged() { public void onTileDataChanged() {
setVisibilityInternal(mTileGroup.getTiles().length != 0); setVisibilityInternal(!mTileGroup.getAllTiles().isEmpty());
if (isVisible()) notifyItemChanged(0, new ViewHolder.UpdateTilesCallback(mTileGroup)); if (isVisible()) notifyItemChanged(0, new ViewHolder.UpdateTilesCallback(mTileGroup));
} }
...@@ -118,7 +118,7 @@ public class TileGrid extends OptionalLeaf implements TileGroup.Observer { ...@@ -118,7 +118,7 @@ public class TileGrid extends OptionalLeaf implements TileGroup.Observer {
} }
public void updateTiles(TileGroup tileGroup) { public void updateTiles(TileGroup tileGroup) {
tileGroup.renderTileViews(mLayout); tileGroup.renderTiles(mLayout);
} }
public void updateIconView(Tile tile) { 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 = [ ...@@ -1046,6 +1046,7 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/suggestions/SuggestionsOfflineModelObserver.java", "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/TileGroup.java",
"java/src/org/chromium/chrome/browser/suggestions/TileGroupDelegateImpl.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/TileView.java",
"java/src/org/chromium/chrome/browser/suggestions/NavigationRecorder.java", "java/src/org/chromium/chrome/browser/suggestions/NavigationRecorder.java",
"java/src/org/chromium/chrome/browser/suggestions/SuggestionsDependencyFactory.java", "java/src/org/chromium/chrome/browser/suggestions/SuggestionsDependencyFactory.java",
......
...@@ -273,14 +273,14 @@ public class TileGroupUnitTest { ...@@ -273,14 +273,14 @@ public class TileGroupUnitTest {
notifyTileUrlsAvailable(URLS); notifyTileUrlsAvailable(URLS);
// Render them to the layout. // Render them to the layout.
tileGroup.renderTileViews(layout); tileGroup.renderTiles(layout);
assertThat(layout.getChildCount(), is(2)); assertThat(layout.getChildCount(), is(2));
assertThat(((TileView) layout.getChildAt(0)).getUrl(), is(URLS[0])); assertThat(((TileView) layout.getChildAt(0)).getUrl(), is(URLS[0]));
assertThat(((TileView) layout.getChildAt(1)).getUrl(), is(URLS[1])); assertThat(((TileView) layout.getChildAt(1)).getUrl(), is(URLS[1]));
} }
/** Check for https://crbug.com/703628: handle duplicated URLs by ignoring them. */ /** Check for https://crbug.com/703628: handle duplicated URLs by ignoring them. */
@Test @Test(expected = IllegalStateException.class)
public void testRenderTileViewWithDuplicatedUrl() { public void testRenderTileViewWithDuplicatedUrl() {
SuggestionsUiDelegate uiDelegate = mock(SuggestionsUiDelegate.class); SuggestionsUiDelegate uiDelegate = mock(SuggestionsUiDelegate.class);
when(uiDelegate.getImageFetcher()).thenReturn(mock(ImageFetcher.class)); when(uiDelegate.getImageFetcher()).thenReturn(mock(ImageFetcher.class));
...@@ -293,11 +293,8 @@ public class TileGroupUnitTest { ...@@ -293,11 +293,8 @@ public class TileGroupUnitTest {
// Initialise the internal list of tiles // Initialise the internal list of tiles
notifyTileUrlsAvailable(URLS[0], URLS[1], URLS[0]); notifyTileUrlsAvailable(URLS[0], URLS[1], URLS[0]);
// Render them to the layout. The duplicated URL is skipped. // Render them to the layout. The duplicated URL should trigger the exception.
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]));
} }
@Test @Test
...@@ -319,7 +316,7 @@ public class TileGroupUnitTest { ...@@ -319,7 +316,7 @@ public class TileGroupUnitTest {
layout.addView(view2); layout.addView(view2);
// The tiles should be updated, the old ones removed. // The tiles should be updated, the old ones removed.
tileGroup.renderTileViews(layout); tileGroup.renderTiles(layout);
assertThat(layout.getChildCount(), is(2)); assertThat(layout.getChildCount(), is(2));
assertThat(layout.indexOfChild(view1), is(-1)); assertThat(layout.indexOfChild(view1), is(-1));
assertThat(layout.indexOfChild(view2), is(-1)); assertThat(layout.indexOfChild(view2), is(-1));
...@@ -347,7 +344,7 @@ public class TileGroupUnitTest { ...@@ -347,7 +344,7 @@ public class TileGroupUnitTest {
layout.addView(view2); layout.addView(view2);
// The tiles should be updated, the old ones reused. // The tiles should be updated, the old ones reused.
tileGroup.renderTileViews(layout); tileGroup.renderTiles(layout);
assertThat(layout.getChildCount(), is(2)); assertThat(layout.getChildCount(), is(2));
assertThat(layout.getChildAt(0), CoreMatchers.<View>is(view1)); assertThat(layout.getChildAt(0), CoreMatchers.<View>is(view1));
assertThat(layout.getChildAt(1), CoreMatchers.<View>is(view2)); assertThat(layout.getChildAt(1), CoreMatchers.<View>is(view2));
...@@ -377,7 +374,7 @@ public class TileGroupUnitTest { ...@@ -377,7 +374,7 @@ public class TileGroupUnitTest {
Tile tile = new Tile("title", URLS[0], "", 0, TileSource.POPULAR); Tile tile = new Tile("title", URLS[0], "", 0, TileSource.POPULAR);
ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null); ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
tileGroup.buildTileView(tile, layout); buildTileView(tile, layout, tileGroup);
// Ensure we run the callback for the new tile. // Ensure we run the callback for the new tile.
assertEquals(1, mImageFetcher.getPendingIconCallbackCount()); assertEquals(1, mImageFetcher.getPendingIconCallbackCount());
...@@ -395,7 +392,7 @@ public class TileGroupUnitTest { ...@@ -395,7 +392,7 @@ public class TileGroupUnitTest {
// Notify for a second set. // Notify for a second set.
notifyTileUrlsAvailable(URLS); notifyTileUrlsAvailable(URLS);
ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null); ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
tileGroup.renderTileViews(layout); tileGroup.renderTiles(layout);
mImageFetcher.fulfillLargeIconRequests(); mImageFetcher.fulfillLargeIconRequests();
// Data changed but no loading complete event is sent // Data changed but no loading complete event is sent
...@@ -414,7 +411,7 @@ public class TileGroupUnitTest { ...@@ -414,7 +411,7 @@ public class TileGroupUnitTest {
notifyTileUrlsAvailable(URLS); notifyTileUrlsAvailable(URLS);
tileGroup.onSwitchToForeground(/* trackLoadTask: */ false); tileGroup.onSwitchToForeground(/* trackLoadTask: */ false);
ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null); ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
tileGroup.renderTileViews(layout); tileGroup.renderTiles(layout);
mImageFetcher.fulfillLargeIconRequests(); mImageFetcher.fulfillLargeIconRequests();
// Data changed but no loading complete event is sent (same as sync) // Data changed but no loading complete event is sent (same as sync)
...@@ -433,7 +430,7 @@ public class TileGroupUnitTest { ...@@ -433,7 +430,7 @@ public class TileGroupUnitTest {
notifyTileUrlsAvailable(URLS); notifyTileUrlsAvailable(URLS);
tileGroup.onSwitchToForeground(/* trackLoadTask: */ true); tileGroup.onSwitchToForeground(/* trackLoadTask: */ true);
ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null); ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
tileGroup.renderTileViews(layout); tileGroup.renderTiles(layout);
mImageFetcher.fulfillLargeIconRequests(); mImageFetcher.fulfillLargeIconRequests();
// Data changed but no loading complete event is sent // Data changed but no loading complete event is sent
...@@ -482,13 +479,17 @@ public class TileGroupUnitTest { ...@@ -482,13 +479,17 @@ public class TileGroupUnitTest {
notifyTileUrlsAvailable(urls); notifyTileUrlsAvailable(urls);
ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null); ViewGroup layout = new FrameLayout(RuntimeEnvironment.application, null);
tileGroup.renderTileViews(layout); tileGroup.renderTiles(layout);
reset(mTileGroupObserver); reset(mTileGroupObserver);
reset(mTileGroupDelegate); reset(mTileGroupDelegate);
return tileGroup; return tileGroup;
} }
private void buildTileView(Tile tile, ViewGroup layout, TileGroup tileGroup) {
tileGroup.getTileRenderer().buildTileView(tile, layout, tileGroup.getTileSetupDelegate());
}
private class FakeTileGroupDelegate implements TileGroup.Delegate { private class FakeTileGroupDelegate implements TileGroup.Delegate {
public MostVisitedSites.Observer mObserver; 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