Commit ef624dfe authored by spdonghao's avatar spdonghao Committed by Chromium LUCI CQ

[Instant Start] Refactor MV tiles metadata caching utils classes.

This CL does some refactoring work:
1. Save existing MV tiles info instead of only SiteSuggestion data.
2. Remove MV tiles favicons saving codes - the cached favicons are not
   always consistent with post-native ones so we show the default grey
   background icon instead.
3. Since favicons are no longer cached, we don't need to keep
   |mUrlToIdMap| and |mUrlsToUpdateFavicon|.
4. SiteSuggestion#faviconId is no longer needed.

Bug: 1067386
Change-Id: I2f05ea32a04c30ef8ed21504a78a77771edb4043
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2611670Reviewed-by: default avatarCathy Li <chili@chromium.org>
Reviewed-by: default avatarTheresa  <twellington@chromium.org>
Reviewed-by: default avatarWei-Yin Chen (陳威尹) <wychen@chromium.org>
Reviewed-by: default avatarXi Han <hanxi@chromium.org>
Commit-Queue: Hao Dong <spdonghao@chromium.org>
Cr-Commit-Position: refs/heads/master@{#845955}
parent ae957034
...@@ -1272,8 +1272,6 @@ chrome_java_sources = [ ...@@ -1272,8 +1272,6 @@ chrome_java_sources = [
"java/src/org/chromium/chrome/browser/suggestions/ThumbnailGradient.java", "java/src/org/chromium/chrome/browser/suggestions/ThumbnailGradient.java",
"java/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSites.java", "java/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSites.java",
"java/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSitesBridge.java", "java/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSitesBridge.java",
"java/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSitesFaviconHelper.java",
"java/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSitesHost.java",
"java/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSitesMetadataUtils.java", "java/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSitesMetadataUtils.java",
"java/src/org/chromium/chrome/browser/suggestions/tile/SiteSection.java", "java/src/org/chromium/chrome/browser/suggestions/tile/SiteSection.java",
"java/src/org/chromium/chrome/browser/suggestions/tile/SiteSectionViewHolder.java", "java/src/org/chromium/chrome/browser/suggestions/tile/SiteSectionViewHolder.java",
......
...@@ -520,8 +520,6 @@ chrome_test_java_sources = [ ...@@ -520,8 +520,6 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/status_indicator/StatusIndicatorTest.java", "javatests/src/org/chromium/chrome/browser/status_indicator/StatusIndicatorTest.java",
"javatests/src/org/chromium/chrome/browser/status_indicator/StatusIndicatorViewBinderTest.java", "javatests/src/org/chromium/chrome/browser/status_indicator/StatusIndicatorViewBinderTest.java",
"javatests/src/org/chromium/chrome/browser/suggestions/NavigationRecorderTest.java", "javatests/src/org/chromium/chrome/browser/suggestions/NavigationRecorderTest.java",
"javatests/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSitesFaviconHelperTest.java",
"javatests/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSitesHostTest.java",
"javatests/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSitesMetadataUtilsTest.java", "javatests/src/org/chromium/chrome/browser/suggestions/mostvisited/MostVisitedSitesMetadataUtilsTest.java",
"javatests/src/org/chromium/chrome/browser/suggestions/tile/TileGridLayoutTest.java", "javatests/src/org/chromium/chrome/browser/suggestions/tile/TileGridLayoutTest.java",
"javatests/src/org/chromium/chrome/browser/suggestions/tile/TileGroupTest.java", "javatests/src/org/chromium/chrome/browser/suggestions/tile/TileGroupTest.java",
......
...@@ -23,9 +23,6 @@ public class SiteSuggestion { ...@@ -23,9 +23,6 @@ public class SiteSuggestion {
/** URL of the suggested site. */ /** URL of the suggested site. */
public final GURL url; public final GURL url;
/** Id of the favicon. It is optional and used when caching the favion on device. */
public int faviconId;
/** The path to the icon image file for allowlisted tile, empty string otherwise. */ /** The path to the icon image file for allowlisted tile, empty string otherwise. */
public final String allowlistIconPath; public final String allowlistIconPath;
...@@ -52,7 +49,6 @@ public class SiteSuggestion { ...@@ -52,7 +49,6 @@ public class SiteSuggestion {
int source, int sectionType, Date dataGenerationTime) { int source, int sectionType, Date dataGenerationTime) {
this.title = title; this.title = title;
this.url = url; this.url = url;
this.faviconId = INVALID_FAVICON_ID;
this.allowlistIconPath = allowlistIconPath; this.allowlistIconPath = allowlistIconPath;
this.source = source; this.source = source;
this.titleSource = titleSource; this.titleSource = titleSource;
......
// Copyright 2020 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.mostvisited;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import androidx.core.util.AtomicFile;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Log;
import org.chromium.base.task.AsyncTask;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.suggestions.SiteSuggestion;
import org.chromium.components.browser_ui.widget.RoundedIconGenerator;
import org.chromium.components.favicon.IconType;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.url.GURL;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Set;
/**
* This class provides methods to fetch/save most visited sites favicon info to devices.
*/
public class MostVisitedSitesFaviconHelper {
private static final String TAG = "TopSitesFavicon";
private final int mMinIconSize;
private final int mDesiredIconSize;
private final LargeIconBridge mLargeIconBridge;
private final RoundedIconGenerator mIconGenerator;
public MostVisitedSitesFaviconHelper(Context context, LargeIconBridge largeIconBridge) {
mLargeIconBridge = largeIconBridge;
Resources resources = context.getResources();
mDesiredIconSize = resources.getDimensionPixelSize(R.dimen.tile_view_icon_size);
int minIconSize = resources.getDimensionPixelSize(R.dimen.tile_view_icon_min_size);
// On ldpi devices, mDesiredIconSize could be even smaller than the global limit.
mMinIconSize = Math.min(mDesiredIconSize, minIconSize);
int iconColor =
ApiCompatibilityUtils.getColor(resources, R.color.default_favicon_background_color);
int iconTextSize = resources.getDimensionPixelSize(R.dimen.tile_view_icon_text_size);
mIconGenerator = new RoundedIconGenerator(
mDesiredIconSize, mDesiredIconSize, mDesiredIconSize / 2, iconColor, iconTextSize);
}
/**
* Save the favicon to the disk.
* @param topSitesInfo SiteSuggestions data updated.
* @param urlsToUpdate The set of urls which need to fetch and save the favicon.
* @param callback The callback function after skipping the existing favicon or saving favicon.
*/
public void saveFaviconsToFile(
List<SiteSuggestion> topSitesInfo, Set<GURL> urlsToUpdate, Runnable callback) {
for (SiteSuggestion siteData : topSitesInfo) {
GURL url = siteData.url;
if (!urlsToUpdate.contains(url)) {
if (callback != null) {
callback.run();
}
continue;
}
LargeIconBridge.LargeIconCallback iconCallback =
(icon, fallbackColor, isFallbackColorDefault, iconType) -> {
saveFaviconToFile(String.valueOf(siteData.faviconId),
MostVisitedSitesMetadataUtils.getOrCreateTopSitesDirectory(), url,
fallbackColor, icon, callback);
};
fetchIcon(siteData, iconCallback);
}
}
/**
* Fetch the favicon for a given site.
* @param siteData SiteSuggestion data which needs to fetch and save the favicon.
* @param iconCallback The callback function after fetching the favicon.
*/
// TODO(https://crbug.com/1067386): Change fetchIcon() to public and static, then reuse it in
// other classes.
private void fetchIcon(
final SiteSuggestion siteData, final LargeIconBridge.LargeIconCallback iconCallback) {
if (siteData.allowlistIconPath.isEmpty()) {
mLargeIconBridge.getLargeIconForUrl(siteData.url, mMinIconSize, iconCallback);
return;
}
AsyncTask<Bitmap> task = new AsyncTask<Bitmap>() {
@Override
protected Bitmap doInBackground() {
Bitmap bitmap = BitmapFactory.decodeFile(siteData.allowlistIconPath);
if (bitmap == null) {
Log.d(TAG, "Image decoding failed: %s.", siteData.allowlistIconPath);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap icon) {
if (icon == null) {
mLargeIconBridge.getLargeIconForUrl(siteData.url, mMinIconSize, iconCallback);
} else {
iconCallback.onLargeIconAvailable(icon, Color.BLACK, false, IconType.INVALID);
}
}
};
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Save the favicon to the disk.
* @param fileName The file name to save the favicon.
* @param directory The directory to save the favicon.
* @param url The url which the favicon corresponds to.
* @param fallbackColor The color for generating a new icon when favicon is null from native.
* @param icon The favicon fetched from native.
* @param callback The callback function after saving each favicon.
*/
private void saveFaviconToFile(String fileName, File directory, GURL url, int fallbackColor,
Bitmap icon, Runnable callback) {
new AsyncTask<Void>() {
@Override
protected Void doInBackground() {
Bitmap newIcon = icon;
// If icon is null, we need to generate a favicon.
if (newIcon == null) {
Log.i(TAG,
"Favicon is null for " + url.getSpec()
+ ". Generating an icon for it.");
mIconGenerator.setBackgroundColor(fallbackColor);
newIcon = mIconGenerator.generateIconForUrl(url.getSpec());
}
// Save icon to file.
File metadataFile = new File(directory, fileName);
AtomicFile file = new AtomicFile(metadataFile);
FileOutputStream stream;
try {
stream = file.startWrite();
assert newIcon != null;
newIcon.compress(Bitmap.CompressFormat.PNG, 100, stream);
file.finishWrite(stream);
Log.i(TAG,
"Finished saving top sites favicons to file: "
+ metadataFile.getAbsolutePath());
} catch (IOException e) {
Log.e(TAG, "Fail to write file: " + metadataFile.getAbsolutePath());
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
if (callback != null) {
callback.run();
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
// Copyright 2020 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.mostvisited;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.AsyncTask;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.suggestions.SiteSuggestion;
import org.chromium.chrome.browser.suggestions.SuggestionsDependencyFactory;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.url.GURL;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Class for saving most visited sites info.
*
* This class is a singleton, since at any point at most one MostVisitedSitesHost can exist for
* ensuring that saving files is atomic.
*/
public class MostVisitedSitesHost implements MostVisitedSites.Observer {
private static final String TAG = "TopSites";
// The map mapping URL to faviconId. This map will be updated once there are new suggestions
// available.
private final Map<GURL, Integer> mUrlToIdMap = new HashMap<>();
// The map mapping faviconId to URL. This map will be reconstructed based on the mUrlToIdMap
// once there are new suggestions available.
private final Map<Integer, GURL> mIdToUrlMap = new HashMap<>();
// The set of URLs needed to fetch favicon. This set will be reconstructed once there are new
// suggestions available.
private final Set<GURL> mUrlsToUpdateFavicon = new HashSet<>();
private static boolean sSkipRestoreFromDiskForTests;
/** The singleton helper class for this class. */
private static class SingletonHelper {
private static final MostVisitedSitesHost INSTANCE = new MostVisitedSitesHost();
}
private MostVisitedSites mMostVisitedSites;
private MostVisitedSitesFaviconHelper mFaviconSaver;
private Runnable mCurrentTask;
private Runnable mPendingTask;
// Whether restoreFromDisk() is finished.
private boolean mIsSynced;
// Records how many remaining files the current task needs to save.
private int mCurrentFilesNeedToSaveCount;
// Records how many remaining files the pending task needs to save. This value is used to
// replace mCurrentFilesNeedToSaveCount when updating pending to current task.
private int mPendingFilesNeedToSaveCount;
private MostVisitedSitesHost() {
if (!sSkipRestoreFromDiskForTests) {
restoreFromDisk();
}
}
/**
* @return The singleton instance.
*/
public static MostVisitedSitesHost getInstance() {
return SingletonHelper.INSTANCE;
}
/**
* Once new siteSuggestions info is available, call this function to update map and set and save
* data to the disk. If syncing with disk hasn't finished or there is a current task running,
* make this new task the pending task. Otherwise, make this new task the current task and run
* it.
* @param siteSuggestions The new SiteSuggestions.
*/
public void saveMostVisitedSitesInfo(List<SiteSuggestion> siteSuggestions) {
if (mFaviconSaver == null) {
LargeIconBridge largeIconBridge =
SuggestionsDependencyFactory.getInstance().createLargeIconBridge(
Profile.getLastUsedRegularProfile());
mFaviconSaver = new MostVisitedSitesFaviconHelper(
ContextUtils.getApplicationContext(), largeIconBridge);
}
// Ensure that saving happens after map and set are updated. Use finishOneFileSaving() as
// callback to record when this current task has been finished.
Runnable newTask = () -> updateMapAndSetForNewSites(siteSuggestions, () -> {
MostVisitedSitesMetadataUtils.saveSuggestionListsToFile(
siteSuggestions, this::finishOneFileSaving);
mFaviconSaver.saveFaviconsToFile(
siteSuggestions, mUrlsToUpdateFavicon, this::finishOneFileSaving);
});
if (!mIsSynced || mCurrentTask != null) {
// Skip last mPendingTask which is not necessary to run.
mPendingTask = newTask;
mPendingFilesNeedToSaveCount = siteSuggestions.size() + 1;
} else {
// Assign newTask to mCurrentTask and run this task.
mCurrentTask = newTask;
mCurrentFilesNeedToSaveCount = siteSuggestions.size() + 1;
// Skip any pending task.
mPendingTask = null;
mPendingFilesNeedToSaveCount = 0;
Log.d(TAG, "Start a new task.");
mCurrentTask.run();
}
}
@Override
public void onSiteSuggestionsAvailable(List<SiteSuggestion> siteSuggestions) {
saveMostVisitedSitesInfo(siteSuggestions);
}
@Override
public void onIconMadeAvailable(GURL siteUrl) {}
/**
* Start the observer.
* @param maxResults The max number of sites to observe.
*/
public void startObserving(int maxResults) {
if (mMostVisitedSites == null) {
mMostVisitedSites = SuggestionsDependencyFactory.getInstance().createMostVisitedSites(
Profile.getLastUsedRegularProfile());
}
mMostVisitedSites.setObserver(this, maxResults);
}
/**
* Restore disk info to the mUrlToIDMap.
*/
private void restoreFromDisk() {
new AsyncTask<List<SiteSuggestion>>() {
@Override
protected List<SiteSuggestion> doInBackground() {
List<SiteSuggestion> siteSuggestions = new ArrayList<>();
try {
siteSuggestions = MostVisitedSitesMetadataUtils.restoreFileToSuggestionLists();
} catch (IOException e) {
Log.i(TAG, "No top sites lists file existed in the disk.");
}
return siteSuggestions;
}
@Override
protected void onPostExecute(List<SiteSuggestion> siteSuggestions) {
mUrlToIdMap.clear();
mIdToUrlMap.clear();
for (SiteSuggestion site : siteSuggestions) {
mUrlToIdMap.put(site.url, site.faviconId);
}
buildIdToUrlMap();
mIsSynced = true;
updatePendingToCurrent();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
/**
* Update mUrlToIDMap and mUrlsToUpdateFavicon based on the new SiteSuggestions passed in.
* @param newSuggestions The new SiteSuggestions.
* @param callback The callback function after updating map and set.
*/
@VisibleForTesting
protected void updateMapAndSetForNewSites(
List<SiteSuggestion> newSuggestions, Runnable callback) {
new AsyncTask<Set<String>>() {
@Override
protected Set<String> doInBackground() {
return getExistingIconFiles();
}
@Override
protected void onPostExecute(Set<String> existingIconFiles) {
// Clear mUrlsToUpdateFavicon.
mUrlsToUpdateFavicon.clear();
// Update mIdToUrlMap with current mUrlToIdMap.
buildIdToUrlMap();
// Update mUrlsToUpdateFavicon based on the new SiteSuggestions.
refreshUrlsToUpdate(newSuggestions, existingIconFiles);
// Update mUrlToIdMap based on the new SiteSuggestions.
updateMapForNewSites(newSuggestions, callback);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private static Set<String> getExistingIconFiles() {
Set<String> existingIconFiles = new HashSet<>();
File topSitesDirectory = MostVisitedSitesMetadataUtils.getStateDirectory();
if (topSitesDirectory == null || topSitesDirectory.list() == null) {
return existingIconFiles;
}
existingIconFiles.addAll(Arrays.asList(Objects.requireNonNull(topSitesDirectory.list())));
return existingIconFiles;
}
/**
* Update mUrlsToUpdateFavicon based on the new SiteSuggestions passed in.
* @param newSuggestions The new SiteSuggestions.
* @param existingIconFiles The existing favicon files name set.
*/
private void refreshUrlsToUpdate(
List<SiteSuggestion> newSuggestions, Set<String> existingIconFiles) {
// Add topsites URLs which need to fetch icon to the mUrlsToUpdateFavicon Set.
for (SiteSuggestion topSiteData : newSuggestions) {
GURL url = topSiteData.url;
// If the old map doesn't contain the URL or there is no favicon file for this URL, then
// add this URL to mUrlsToUpdateFavicon.
if (!mUrlToIdMap.containsKey(url)
|| !existingIconFiles.contains(String.valueOf(mUrlToIdMap.get(url)))) {
mUrlsToUpdateFavicon.add(url);
}
}
}
/**
* Update the mUrlToIdMap based on the new SiteSuggestions and mUrlsToUpdateFavicon.
* @param newSuggestions The new SiteSuggestions.
* @param callback The callback function after updating map and set.
*/
private void updateMapForNewSites(List<SiteSuggestion> newSuggestions, Runnable callback) {
// Get the set of new top sites' URLs.
Set<GURL> newUrls = new HashSet<>();
for (SiteSuggestion topSiteData : newSuggestions) {
newUrls.add(topSiteData.url);
}
// Add new URLs and ids to the mUrlToIDMap.
int id = 0;
for (GURL url : mUrlsToUpdateFavicon) {
if (mUrlToIdMap.containsKey(url)) {
continue;
}
// Get the next available ID.
id = getNextAvailableId(id, newUrls);
mUrlToIdMap.put(url, id);
id++;
}
// Remove stale data in mUrlToIdMap.
List<Integer> idsToDeleteFile = removeStaleData(newUrls);
// Remove stale favicon files in the disk asynchronously.
deleteStaleFilesAsync(idsToDeleteFile, () -> {
mIdToUrlMap.clear();
// Save faviconIDs to newSuggestions.
for (SiteSuggestion siteData : newSuggestions) {
siteData.faviconId = Objects.requireNonNull(mUrlToIdMap.get(siteData.url));
}
if (callback != null) {
callback.run();
}
});
}
@VisibleForTesting
protected int getNextAvailableId(int start, Set<GURL> newTopSiteUrls) {
int id = start;
// The available ids should be in range [0, newTopSiteUrls.size()), since we only need
// |newTopSiteUrls.size()| ids.
for (; id < newTopSiteUrls.size(); id++) {
// If this id is not used in mUrlToIdMap or URL corresponding to this id is not in
// the newTopSiteURLs, it's available.
if (!mIdToUrlMap.containsKey(id) || !newTopSiteUrls.contains(mIdToUrlMap.get(id))) {
return id;
}
}
assert false : "Unreachable code.";
return id;
}
@VisibleForTesting
protected void buildIdToUrlMap() {
mIdToUrlMap.clear();
for (Map.Entry<GURL, Integer> entry : mUrlToIdMap.entrySet()) {
mIdToUrlMap.put(entry.getValue(), entry.getKey());
}
}
/**
* Return faviconIds which need to delete files.
* @param newUrls The URLs in new SiteSuggestions.
* @return The list of faviconIds needed to remove.
*/
private List<Integer> removeStaleData(Set<GURL> newUrls) {
List<Integer> idsToDeleteFile = new ArrayList<>();
for (Iterator<Map.Entry<GURL, Integer>> it = mUrlToIdMap.entrySet().iterator();
it.hasNext();) {
Map.Entry<GURL, Integer> entry = it.next();
GURL url = entry.getKey();
int faviconId = entry.getValue();
if (!newUrls.contains(url)) {
it.remove();
idsToDeleteFile.add(faviconId);
}
}
return idsToDeleteFile;
}
private void deleteStaleFilesAsync(List<Integer> idsToDeleteFile, Runnable callback) {
new AsyncTask<Void>() {
@Override
protected Void doInBackground() {
for (int id : idsToDeleteFile) {
File file =
new File(MostVisitedSitesMetadataUtils.getOrCreateTopSitesDirectory(),
String.valueOf(id));
file.delete();
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
if (callback != null) {
callback.run();
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
@VisibleForTesting
protected void updatePendingToCurrent() {
mCurrentTask = mPendingTask;
mCurrentFilesNeedToSaveCount = mPendingFilesNeedToSaveCount;
mPendingTask = null;
if (mCurrentTask != null) {
Log.d(TAG, "Start a new task.");
mCurrentTask.run();
}
}
private void finishOneFileSaving() {
ThreadUtils.assertOnUiThread();
mCurrentFilesNeedToSaveCount--;
// If there is no file needed to save for current task, update pending task to current.
if (mCurrentFilesNeedToSaveCount == 0) {
updatePendingToCurrent();
}
}
@VisibleForTesting
protected Map<GURL, Integer> getUrlToIDMapForTesting() {
return mUrlToIdMap;
}
@VisibleForTesting
protected Set<GURL> getUrlsToUpdateFaviconForTesting() {
return mUrlsToUpdateFavicon;
}
@VisibleForTesting
protected Map<Integer, GURL> getIdToUrlMapForTesting() {
return mIdToUrlMap;
}
@VisibleForTesting
protected static void setSkipRestoreFromDiskForTesting() {
sSkipRestoreFromDiskForTests = true;
}
@VisibleForTesting
public void setIsSyncedForTesting(boolean isSynced) {
mIsSynced = isSynced;
}
@VisibleForTesting
public int getCurrentFilesNeedToSaveCountForTesting() {
return mCurrentFilesNeedToSaveCount;
}
@VisibleForTesting
public int getPendingFilesNeedToSaveCountForTesting() {
return mPendingFilesNeedToSaveCount;
}
@VisibleForTesting
public void setCurrentTaskForTesting(Runnable currentTask) {
mCurrentTask = currentTask;
}
@VisibleForTesting
public void setPendingTaskForTesting(Runnable pendingTask) {
mPendingTask = pendingTask;
}
}
...@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.suggestions.mostvisited; ...@@ -6,6 +6,7 @@ package org.chromium.chrome.browser.suggestions.mostvisited;
import android.content.Context; import android.content.Context;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.AtomicFile; import androidx.core.util.AtomicFile;
import org.chromium.base.ContextUtils; import org.chromium.base.ContextUtils;
...@@ -14,6 +15,7 @@ import org.chromium.base.StreamUtil; ...@@ -14,6 +15,7 @@ import org.chromium.base.StreamUtil;
import org.chromium.base.StrictModeContext; import org.chromium.base.StrictModeContext;
import org.chromium.base.task.AsyncTask; import org.chromium.base.task.AsyncTask;
import org.chromium.chrome.browser.suggestions.SiteSuggestion; import org.chromium.chrome.browser.suggestions.SiteSuggestion;
import org.chromium.chrome.browser.suggestions.tile.Tile;
import org.chromium.url.GURL; import org.chromium.url.GURL;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
...@@ -33,6 +35,13 @@ import java.util.List; ...@@ -33,6 +35,13 @@ import java.util.List;
*/ */
public class MostVisitedSitesMetadataUtils { public class MostVisitedSitesMetadataUtils {
private static final String TAG = "TopSites"; private static final String TAG = "TopSites";
/** The singleton helper class for this class. */
private static class SingletonHelper {
private static final MostVisitedSitesMetadataUtils INSTANCE =
new MostVisitedSitesMetadataUtils();
}
/** Prevents two state directories from getting created simultaneously. */ /** Prevents two state directories from getting created simultaneously. */
private static final Object DIR_CREATION_LOCK = new Object(); private static final Object DIR_CREATION_LOCK = new Object();
...@@ -46,33 +55,40 @@ public class MostVisitedSitesMetadataUtils { ...@@ -46,33 +55,40 @@ public class MostVisitedSitesMetadataUtils {
private static String sStateDirName = "top_sites"; private static String sStateDirName = "top_sites";
private static String sStateFileName = "top_sites"; private static String sStateFileName = "top_sites";
private Runnable mCurrentTask;
private Runnable mPendingTask;
private int mPendingTaskTilesNumForTesting;
/** /**
* Asynchronously serialize the suggestion lists and save it into the disk. * @return The singleton instance.
* @param topSitesInfo Suggestion lists.
* @param callback Callback function after saving file.
*/ */
public static void saveSuggestionListsToFile( public static MostVisitedSitesMetadataUtils getInstance() {
List<SiteSuggestion> topSitesInfo, Runnable callback) { return SingletonHelper.INSTANCE;
new AsyncTask<Void>() { }
@Override
protected Void doInBackground() {
try {
byte[] listData = serializeTopSitesData(topSitesInfo);
saveSuggestionListsToFile(
getOrCreateTopSitesDirectory(), sStateFileName, listData);
} catch (IOException e) {
Log.e(TAG, "Fail to save file.");
}
return null;
}
@Override /**
protected void onPostExecute(Void aVoid) { * Save new suggestion tiles to the disk. If there is already a task running, save this new
if (callback != null) { * saving task as |mPendingTask|.
callback.run(); * @param suggestionTiles The site suggestion tiles.
} */
} public void saveSuggestionListsToFile(List<Tile> suggestionTiles) {
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); Runnable newTask =
() -> saveSuggestionListsToFile(suggestionTiles, this::updatePendingToCurrent);
if (mCurrentTask != null) {
// Skip last mPendingTask which is not necessary to run.
mPendingTask = newTask;
mPendingTaskTilesNumForTesting = suggestionTiles.size();
} else {
// Assign newTask to mCurrentTask and run this task.
mCurrentTask = newTask;
// Skip any pending task.
mPendingTask = null;
Log.i(TAG, "Start a new task.");
mCurrentTask.run();
}
} }
/** /**
...@@ -82,17 +98,16 @@ public class MostVisitedSitesMetadataUtils { ...@@ -82,17 +98,16 @@ public class MostVisitedSitesMetadataUtils {
* stale files and throw an exception, then the UI thread will know there is no cache file and * stale files and throw an exception, then the UI thread will know there is no cache file and
* show something else. * show something else.
*/ */
public static List<SiteSuggestion> restoreFileToSuggestionLists() throws IOException { public static List<Tile> restoreFileToSuggestionLists() throws IOException {
List<SiteSuggestion> suggestions; List<Tile> tiles;
try { try {
byte[] listData = byte[] listData = restoreFileToBytes(getOrCreateTopSitesDirectory(), sStateFileName);
restoreFileToSuggestionLists(getOrCreateTopSitesDirectory(), sStateFileName); tiles = deserializeTopSitesData(listData);
suggestions = deserializeTopSitesData(listData);
} catch (IOException e) { } catch (IOException e) {
getOrCreateTopSitesDirectory().delete(); getOrCreateTopSitesDirectory().delete();
throw e; throw e;
} }
return suggestions; return tiles;
} }
/** /**
...@@ -102,15 +117,43 @@ public class MostVisitedSitesMetadataUtils { ...@@ -102,15 +117,43 @@ public class MostVisitedSitesMetadataUtils {
* stale files and throw an exception, then the UI thread will know there is no cache file and * stale files and throw an exception, then the UI thread will know there is no cache file and
* show something else. * show something else.
*/ */
public static List<SiteSuggestion> restoreFileToSuggestionListsOnUiThread() throws IOException { public static List<Tile> restoreFileToSuggestionListsOnUiThread() throws IOException {
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
return restoreFileToSuggestionLists(); return restoreFileToSuggestionLists();
} }
} }
private static byte[] serializeTopSitesData(List<SiteSuggestion> topSitesInfo) /**
throws IOException { * Asynchronously serialize the suggestion lists and save it into the disk.
int topSitesCount = topSitesInfo.size(); * @param suggestionTiles The site suggestion tiles.
* @param callback Callback function after saving file.
*/
@VisibleForTesting
protected static void saveSuggestionListsToFile(List<Tile> suggestionTiles, Runnable callback) {
new AsyncTask<Void>() {
@Override
protected Void doInBackground() {
try {
byte[] listData = serializeTopSitesData(suggestionTiles);
saveSuggestionListsToFile(
getOrCreateTopSitesDirectory(), sStateFileName, listData);
} catch (IOException e) {
Log.e(TAG, "Fail to save file.");
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
if (callback != null) {
callback.run();
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private static byte[] serializeTopSitesData(List<Tile> suggestionTiles) throws IOException {
int topSitesCount = suggestionTiles.size();
ByteArrayOutputStream output = new ByteArrayOutputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream();
DataOutputStream stream = new DataOutputStream(output); DataOutputStream stream = new DataOutputStream(output);
...@@ -118,48 +161,44 @@ public class MostVisitedSitesMetadataUtils { ...@@ -118,48 +161,44 @@ public class MostVisitedSitesMetadataUtils {
// Save the count of the list of top sites to restore. // Save the count of the list of top sites to restore.
stream.writeInt(topSitesCount); stream.writeInt(topSitesCount);
Log.d(TAG, "Serializing top sites lists; count: " + topSitesCount);
// Save top sites. // Save top sites.
for (int i = 0; i < topSitesCount; i++) { for (int i = 0; i < topSitesCount; i++) {
stream.writeInt(CACHE_VERSION); stream.writeInt(CACHE_VERSION);
stream.writeInt(topSitesInfo.get(i).faviconId); stream.writeInt(suggestionTiles.get(i).getIndex());
stream.writeUTF(topSitesInfo.get(i).title); SiteSuggestion suggestionInfo = suggestionTiles.get(i).getData();
stream.writeUTF(topSitesInfo.get(i).url.serialize()); stream.writeUTF(suggestionInfo.title);
stream.writeUTF(topSitesInfo.get(i).allowlistIconPath); stream.writeUTF(suggestionInfo.url.serialize());
stream.writeInt(topSitesInfo.get(i).titleSource); stream.writeUTF(suggestionInfo.allowlistIconPath);
stream.writeInt(topSitesInfo.get(i).source); stream.writeInt(suggestionInfo.titleSource);
stream.writeInt(topSitesInfo.get(i).sectionType); stream.writeInt(suggestionInfo.source);
stream.writeLong(topSitesInfo.get(i).dataGenerationTime.getTime()); stream.writeInt(suggestionInfo.sectionType);
stream.writeLong(suggestionInfo.dataGenerationTime.getTime());
} }
stream.close(); stream.close();
Log.d(TAG, "Serializing top sites lists finished"); Log.i(TAG, "Serializing top sites lists finished; count: " + topSitesCount);
return output.toByteArray(); return output.toByteArray();
} }
private static List<SiteSuggestion> deserializeTopSitesData(byte[] listData) private static List<Tile> deserializeTopSitesData(byte[] listData) throws IOException {
throws IOException {
if (listData == null || listData.length == 0) { if (listData == null || listData.length == 0) {
return null; return null;
} }
DataInputStream stream = new DataInputStream(new ByteArrayInputStream(listData)); DataInputStream stream = new DataInputStream(new ByteArrayInputStream(listData));
Log.d(TAG, "Deserializing top sites lists");
Date dataGenerationTime; Date dataGenerationTime;
// Get how many top sites there are. // Get how many top sites there are.
final int count = stream.readInt(); final int count = stream.readInt();
// Restore top sites. // Restore top sites.
List<SiteSuggestion> suggestions = new ArrayList<>(); List<Tile> tiles = new ArrayList<>();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
int version = stream.readInt(); int version = stream.readInt();
if (version > CACHE_VERSION) { if (version > CACHE_VERSION) {
throw new IOException("Cache version not supported."); throw new IOException("Cache version not supported.");
} }
int faviconId = stream.readInt(); int index = stream.readInt();
String title = stream.readUTF(); String title = stream.readUTF();
GURL url = GURL.deserialize(stream.readUTF()); GURL url = GURL.deserialize(stream.readUTF());
if (url.isEmpty()) throw new IOException("GURL deserialization failed."); if (url.isEmpty()) throw new IOException("GURL deserialization failed.");
...@@ -171,11 +210,11 @@ public class MostVisitedSitesMetadataUtils { ...@@ -171,11 +210,11 @@ public class MostVisitedSitesMetadataUtils {
dataGenerationTime = new Date(stream.readLong()); dataGenerationTime = new Date(stream.readLong());
SiteSuggestion newSite = new SiteSuggestion(title, url, allowlistIconPath, titleSource, SiteSuggestion newSite = new SiteSuggestion(title, url, allowlistIconPath, titleSource,
source, sectionType, dataGenerationTime); source, sectionType, dataGenerationTime);
newSite.faviconId = faviconId; Tile newTile = new Tile(newSite, index);
suggestions.add(newSite); tiles.add(newTile);
} }
Log.d(TAG, "Deserializing top sites lists finished"); Log.i(TAG, "Deserializing top sites lists finished");
return suggestions; return tiles;
} }
/** /**
...@@ -209,7 +248,7 @@ public class MostVisitedSitesMetadataUtils { ...@@ -209,7 +248,7 @@ public class MostVisitedSitesMetadataUtils {
* @param stateFileName File name to save top sites data into. * @param stateFileName File name to save top sites data into.
* @return Top sites data in the form of a serialized byte array. * @return Top sites data in the form of a serialized byte array.
*/ */
private static byte[] restoreFileToSuggestionLists(File stateDirectory, String stateFileName) private static byte[] restoreFileToBytes(File stateDirectory, String stateFileName)
throws IOException { throws IOException {
FileInputStream stream; FileInputStream stream;
byte[] data; byte[] data;
...@@ -237,7 +276,38 @@ public class MostVisitedSitesMetadataUtils { ...@@ -237,7 +276,38 @@ public class MostVisitedSitesMetadataUtils {
} }
} }
protected static File getStateDirectory() { @VisibleForTesting
return sStateDirectory; protected void updatePendingToCurrent() {
mCurrentTask = mPendingTask;
mPendingTask = null;
if (mCurrentTask != null) {
Log.i(TAG, "Start a new task.");
mCurrentTask.run();
}
}
@VisibleForTesting
public Runnable getCurrentTaskForTesting() {
return mCurrentTask;
}
@VisibleForTesting
public Runnable getPendingTaskForTesting() {
return mPendingTask;
}
@VisibleForTesting
public void setCurrentTaskForTesting(Runnable currentTask) {
mCurrentTask = currentTask;
}
@VisibleForTesting
public void setPendingTaskForTesting(Runnable pendingTask) {
mPendingTask = pendingTask;
}
@VisibleForTesting
public int getPendingTaskTilesNumForTesting() {
return mPendingTaskTilesNumForTesting;
} }
} }
// Copyright 2020 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.mostvisited;
import android.graphics.Bitmap;
import androidx.core.util.AtomicFile;
import androidx.test.filters.MediumTest;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.suggestions.SiteSuggestion;
import org.chromium.chrome.browser.suggestions.tile.TileSectionType;
import org.chromium.chrome.browser.suggestions.tile.TileSource;
import org.chromium.chrome.browser.suggestions.tile.TileTitleSource;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.url.GURL;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Instrumentation tests for {@link MostVisitedSitesFaviconHelper}.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class MostVisitedSitesFaviconHelperTest {
@Rule
public ChromeTabbedActivityTestRule mTestSetupRule = new ChromeTabbedActivityTestRule();
private List<SiteSuggestion> mExpectedSiteSuggestions;
private MostVisitedSitesFaviconHelper mMostVisitedSitesFaviconHelper;
@Before
public void setUp() {
mTestSetupRule.startMainActivityOnBlankPage();
mExpectedSiteSuggestions = createFakeSiteSuggestions();
TestThreadUtils.runOnUiThreadBlocking(() -> {
LargeIconBridge largeIconBridge =
new LargeIconBridge(Profile.getLastUsedRegularProfile());
mMostVisitedSitesFaviconHelper = new MostVisitedSitesFaviconHelper(
mTestSetupRule.getActivity(), largeIconBridge);
});
}
@Test
@MediumTest
@DisabledTest(message = "crbug.com/1149856")
public void testSaveFaviconsToFile() {
// Add sites' URLs into the urlsToUpdate, except the last one.
Set<GURL> urlsToUpdate = new HashSet<>();
for (int i = 0; i < mExpectedSiteSuggestions.size() - 1; i++) {
urlsToUpdate.add(mExpectedSiteSuggestions.get(i).url);
}
// Save file count before saving favicons since there might be other files in the directory.
int originalFilesNum = getStateDirectorySize();
// Call saveFaviconsToFile.
TestThreadUtils.runOnUiThreadBlocking(
()
-> mMostVisitedSitesFaviconHelper.saveFaviconsToFile(
mExpectedSiteSuggestions, urlsToUpdate, null));
// Wait util the file number equals to expected one.
CriteriaHelper.pollInstrumentationThread(() -> {
Criteria.checkThat(
getStateDirectorySize() - originalFilesNum, Matchers.is(urlsToUpdate.size()));
});
// The Favicon File lists in the disk.
File topSitesDirectory = MostVisitedSitesMetadataUtils.getStateDirectory();
Assert.assertNotNull(topSitesDirectory);
String[] faviconFiles = topSitesDirectory.list();
Assert.assertNotNull(faviconFiles);
Set<String> existingIconFiles = new HashSet<>(Arrays.asList(faviconFiles));
// Check whether each URL's favicon exist in the disk.
Assert.assertTrue(existingIconFiles.contains("0"));
Assert.assertTrue(existingIconFiles.contains("1"));
Assert.assertTrue(existingIconFiles.contains("2"));
Assert.assertFalse(existingIconFiles.contains("3"));
}
private static List<SiteSuggestion> createFakeSiteSuggestions() {
List<SiteSuggestion> siteSuggestions = new ArrayList<>();
siteSuggestions.add(new SiteSuggestion("0 TOP_SITES", new GURL("https://www.foo.com"), "",
TileTitleSource.TITLE_TAG, TileSource.TOP_SITES, TileSectionType.PERSONALIZED,
new Date()));
siteSuggestions.add(new SiteSuggestion("1 ALLOWLIST", new GURL("https://www.bar.com"),
"/not_exist.png", TileTitleSource.UNKNOWN, TileSource.ALLOWLIST,
TileSectionType.PERSONALIZED, new Date()));
siteSuggestions.add(new SiteSuggestion("2 TOP_SITES", new GURL("https://www.baz.com"),
createBitmapAndWriteToFile(), TileTitleSource.UNKNOWN, TileSource.ALLOWLIST,
TileSectionType.PERSONALIZED, new Date()));
siteSuggestions.add(new SiteSuggestion("3 TOP_SITES", new GURL("https://www.qux.com"), "",
TileTitleSource.UNKNOWN, TileSource.ALLOWLIST, TileSectionType.PERSONALIZED,
new Date()));
siteSuggestions.get(0).faviconId = 0;
siteSuggestions.get(1).faviconId = 1;
siteSuggestions.get(2).faviconId = 2;
siteSuggestions.get(3).faviconId = 3;
return siteSuggestions;
}
private static String createBitmapAndWriteToFile() {
final Bitmap testBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
String fileName = "test.png";
// Save bitmap to file.
File metadataFile =
new File(MostVisitedSitesMetadataUtils.getOrCreateTopSitesDirectory(), fileName);
AtomicFile file = new AtomicFile(metadataFile);
FileOutputStream stream;
try {
stream = file.startWrite();
testBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
file.finishWrite(stream);
} catch (IOException e) {
e.printStackTrace();
}
return metadataFile.getAbsolutePath();
}
private static int getStateDirectorySize() {
return Objects.requireNonNull(MostVisitedSitesMetadataUtils.getStateDirectory().list())
.length;
}
}
// Copyright 2020 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.mostvisited;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import android.graphics.Bitmap;
import androidx.core.util.AtomicFile;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.suggestions.SiteSuggestion;
import org.chromium.chrome.browser.suggestions.tile.TileSectionType;
import org.chromium.chrome.browser.suggestions.tile.TileSource;
import org.chromium.chrome.browser.suggestions.tile.TileTitleSource;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.url.GURL;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Instrumentation tests for {@link MostVisitedSitesHost}.
*/
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE})
public class MostVisitedSitesHostTest {
@Rule
public ChromeTabbedActivityTestRule mTestSetupRule = new ChromeTabbedActivityTestRule();
MostVisitedSitesHost mMostVisitedSitesHost;
@Before
public void setUp() {
mTestSetupRule.startMainActivityOnBlankPage();
MostVisitedSitesHost.setSkipRestoreFromDiskForTesting();
mMostVisitedSitesHost = MostVisitedSitesHost.getInstance();
}
@Test
@SmallTest
public void testNextAvailableId() {
Set<GURL> newTopSiteUrls = new HashSet<>();
GURL url0 = new GURL("https://www.0.com");
GURL url1 = new GURL("https://www.1.com");
GURL url2 = new GURL("https://www.2.com");
GURL url3 = new GURL("https://www.3.com");
// Update map and set.
newTopSiteUrls.add(url0);
newTopSiteUrls.add(url1);
mMostVisitedSitesHost.getUrlsToUpdateFaviconForTesting().addAll(newTopSiteUrls);
// The next available ID should be 0.
assertEquals(0, mMostVisitedSitesHost.getNextAvailableId(0, newTopSiteUrls));
mMostVisitedSitesHost.getUrlToIDMapForTesting().put(url0, 0);
mMostVisitedSitesHost.buildIdToUrlMap();
// The next available ID should be 1.
assertEquals(1, mMostVisitedSitesHost.getNextAvailableId(1, newTopSiteUrls));
mMostVisitedSitesHost.getUrlToIDMapForTesting().put(url1, 1);
mMostVisitedSitesHost.buildIdToUrlMap();
// Create a new batch of SiteSuggestions.
newTopSiteUrls.clear();
newTopSiteUrls.add(url0);
newTopSiteUrls.add(url2);
newTopSiteUrls.add(url3);
// The next available ID should be ID for "https://www.1.com".
assertEquals(1, mMostVisitedSitesHost.getNextAvailableId(0, newTopSiteUrls));
mMostVisitedSitesHost.getUrlToIDMapForTesting().put(url2, 1);
// After setting 1 to "https://www.2.com", the next available ID should be 2.
assertEquals(2, mMostVisitedSitesHost.getNextAvailableId(2, newTopSiteUrls));
}
@Test
@SmallTest
public void testUpdateMapAndSet() {
GURL url0 = new GURL("https://www.0.com");
GURL url2 = new GURL("https://www.2.com");
mMostVisitedSitesHost.getUrlToIDMapForTesting().clear();
mMostVisitedSitesHost.getUrlsToUpdateFaviconForTesting().clear();
mMostVisitedSitesHost.getIdToUrlMapForTesting().clear();
Set<GURL> newUrls;
Set<Integer> expectedIdsInMap = new HashSet<>();
AtomicBoolean isUpdated = new AtomicBoolean(false);
// Based on the first batch of sites, update the map and set.
List<SiteSuggestion> newTopSites = createFakeSiteSuggestions1();
newUrls = getUrls(newTopSites);
mMostVisitedSitesHost.updateMapAndSetForNewSites(newTopSites, () -> isUpdated.set(true));
CriteriaHelper.pollInstrumentationThread(isUpdated::get);
// Check the map and set.
expectedIdsInMap.add(0);
expectedIdsInMap.add(1);
expectedIdsInMap.add(2);
checkMapAndSet(newUrls, newUrls, expectedIdsInMap, newTopSites);
// Record ID of "https://www.0.com" for checking later.
int expectedId0 =
mMostVisitedSitesHost.getUrlToIDMapForTesting().get(new GURL("https://www.0.com"));
// Emulate saving favicons for the first batch of sites except "https://www.2.com".
int expectedId2 =
mMostVisitedSitesHost.getUrlToIDMapForTesting().get(new GURL("https://www.2.com"));
for (int i = 0; i < 3; i++) {
if (i == expectedId2) {
continue;
}
createBitmapAndWriteToFile(String.valueOf(i));
}
// Based on the second batch of sites, update the map and set.
newTopSites = createFakeSiteSuggestions2();
newUrls = getUrls(newTopSites);
isUpdated.set(false);
mMostVisitedSitesHost.updateMapAndSetForNewSites(newTopSites, () -> isUpdated.set(true));
CriteriaHelper.pollInstrumentationThread(isUpdated::get);
// Check the map and set.
Set<GURL> expectedUrlsToFetchIcon = new HashSet<>(newUrls);
expectedUrlsToFetchIcon.remove(url0);
expectedIdsInMap.add(3);
checkMapAndSet(expectedUrlsToFetchIcon, newUrls, expectedIdsInMap, newTopSites);
assertEquals(expectedId0, (int) mMostVisitedSitesHost.getUrlToIDMapForTesting().get(url0));
assertEquals(expectedId2, (int) mMostVisitedSitesHost.getUrlToIDMapForTesting().get(url2));
}
/**
* Test when synchronization of metadata stored on disk hasn't finished yet, all coming tasks
* will be set as the pending task. Besides, the latest task will override the old one.
*/
@Test
@SmallTest
public void testSyncNotFinished() {
List<SiteSuggestion> newTopSites1 = createFakeSiteSuggestions1();
List<SiteSuggestion> newTopSites2 = createFakeSiteSuggestions2();
mMostVisitedSitesHost.setIsSyncedForTesting(false);
// If restoring from disk is not finished, all coming tasks should be set as the pending
// task.
TestThreadUtils.runOnUiThreadBlocking(() -> {
mMostVisitedSitesHost.saveMostVisitedSitesInfo(newTopSites1);
mMostVisitedSitesHost.saveMostVisitedSitesInfo(newTopSites2);
});
// newTopSites1 should be skipped and newTopSites2 should be the pending task.
assertEquals(newTopSites2.size(),
mMostVisitedSitesHost.getPendingFilesNeedToSaveCountForTesting() - 1);
}
/**
* Test when current task is not finished, all coming tasks will be set as the pending task.
* Besides, the latest task will override the old one.
*/
@Test
@SmallTest
public void testCurrentNotNull() {
List<SiteSuggestion> newTopSites1 = createFakeSiteSuggestions1();
List<SiteSuggestion> newTopSites2 = createFakeSiteSuggestions2();
mMostVisitedSitesHost.setIsSyncedForTesting(true);
mMostVisitedSitesHost.setCurrentTaskForTesting(() -> {});
// If current task is not null, all saving tasks should be set as pending task.
TestThreadUtils.runOnUiThreadBlocking(() -> {
mMostVisitedSitesHost.saveMostVisitedSitesInfo(newTopSites1);
mMostVisitedSitesHost.saveMostVisitedSitesInfo(newTopSites2);
});
// newTopSites1 should be skipped and newTopSites2 should be the pending task.
assertEquals(newTopSites2.size(),
mMostVisitedSitesHost.getPendingFilesNeedToSaveCountForTesting() - 1);
}
/**
* Test when current task is finished, the pending task should be set as current task and run.
*/
@Test
@SmallTest
public void testTasksContinuity() {
AtomicBoolean isPendingRun = new AtomicBoolean(false);
// Set and run current task.
mMostVisitedSitesHost.setIsSyncedForTesting(true);
mMostVisitedSitesHost.setCurrentTaskForTesting(null);
TestThreadUtils.runOnUiThreadBlocking(() -> {
mMostVisitedSitesHost.saveMostVisitedSitesInfo(createFakeSiteSuggestions1());
});
// When current task is not finished, set pending task.
assertTrue(mMostVisitedSitesHost.getCurrentFilesNeedToSaveCountForTesting() > 0);
mMostVisitedSitesHost.setPendingTaskForTesting(() -> isPendingRun.set(true));
// isPendingRun should eventually become true.
CriteriaHelper.pollInstrumentationThread(isPendingRun::get);
}
private void checkMapAndSet(Set<GURL> expectedUrlsToFetchIcon, Set<GURL> expectedUrlsInMap,
Set<Integer> expectedIdsInMap, List<SiteSuggestion> newSiteSuggestions) {
// Check whether mExpectedSiteSuggestions' faviconIDs have been updated.
for (SiteSuggestion siteData : newSiteSuggestions) {
assertEquals((int) mMostVisitedSitesHost.getUrlToIDMapForTesting().get(siteData.url),
siteData.faviconId);
}
Set<GURL> urlsToUpdateFavicon = mMostVisitedSitesHost.getUrlsToUpdateFaviconForTesting();
Map<GURL, Integer> urlToIDMap = mMostVisitedSitesHost.getUrlToIDMapForTesting();
assertEquals(expectedUrlsToFetchIcon, urlsToUpdateFavicon);
assertThat(expectedUrlsInMap, containsInAnyOrder(urlToIDMap.keySet().toArray()));
assertThat(expectedIdsInMap, containsInAnyOrder(urlToIDMap.values().toArray()));
}
private static List<SiteSuggestion> createFakeSiteSuggestions1() {
List<SiteSuggestion> siteSuggestions = new ArrayList<>();
siteSuggestions.add(new SiteSuggestion("0 TOP_SITES", new GURL("https://www.0.com"), "",
TileTitleSource.TITLE_TAG, TileSource.TOP_SITES, TileSectionType.PERSONALIZED,
new Date()));
siteSuggestions.add(new SiteSuggestion("1 ALLOWLIST", new GURL("https://www.1.com"), "",
TileTitleSource.UNKNOWN, TileSource.ALLOWLIST, TileSectionType.PERSONALIZED,
new Date()));
siteSuggestions.add(new SiteSuggestion("2 TOP_SITES", new GURL("https://www.2.com"), "",
TileTitleSource.UNKNOWN, TileSource.TOP_SITES, TileSectionType.PERSONALIZED,
new Date()));
return siteSuggestions;
}
private static List<SiteSuggestion> createFakeSiteSuggestions2() {
List<SiteSuggestion> siteSuggestions = new ArrayList<>();
siteSuggestions.add(new SiteSuggestion("0 TOP_SITES", new GURL("https://www.0.com"), "",
TileTitleSource.TITLE_TAG, TileSource.TOP_SITES, TileSectionType.PERSONALIZED,
new Date()));
siteSuggestions.add(new SiteSuggestion("2 TOP_SITES", new GURL("https://www.2.com"), "",
TileTitleSource.UNKNOWN, TileSource.TOP_SITES, TileSectionType.PERSONALIZED,
new Date()));
siteSuggestions.add(new SiteSuggestion("3 TOP_SITES", new GURL("https://www.3.com"), "",
TileTitleSource.UNKNOWN, TileSource.TOP_SITES, TileSectionType.PERSONALIZED,
new Date()));
siteSuggestions.add(new SiteSuggestion("4 TOP_SITES", new GURL("https://www.4.com"), "",
TileTitleSource.UNKNOWN, TileSource.TOP_SITES, TileSectionType.PERSONALIZED,
new Date()));
return siteSuggestions;
}
private Set<GURL> getUrls(List<SiteSuggestion> newSuggestions) {
Set<GURL> newTopSiteUrls = new HashSet<>();
for (SiteSuggestion topSiteData : newSuggestions) {
newTopSiteUrls.add(topSiteData.url);
}
return newTopSiteUrls;
}
private static void createBitmapAndWriteToFile(String fileName) {
final Bitmap testBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
// Save bitmap to file.
File metadataFile =
new File(MostVisitedSitesMetadataUtils.getOrCreateTopSitesDirectory(), fileName);
AtomicFile file = new AtomicFile(metadataFile);
FileOutputStream stream;
try {
stream = file.startWrite();
testBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
file.finishWrite(stream);
} catch (IOException e) {
e.printStackTrace();
}
}
}
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
package org.chromium.chrome.browser.suggestions.mostvisited; package org.chromium.chrome.browser.suggestions.mostvisited;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
...@@ -15,13 +17,16 @@ import org.junit.Test; ...@@ -15,13 +17,16 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.chromium.base.test.util.CommandLineFlags; import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.chrome.browser.flags.ChromeSwitches; import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.suggestions.SiteSuggestion; import org.chromium.chrome.browser.suggestions.SiteSuggestion;
import org.chromium.chrome.browser.suggestions.tile.Tile;
import org.chromium.chrome.browser.suggestions.tile.TileSectionType; import org.chromium.chrome.browser.suggestions.tile.TileSectionType;
import org.chromium.chrome.browser.suggestions.tile.TileSource; import org.chromium.chrome.browser.suggestions.tile.TileSource;
import org.chromium.chrome.browser.suggestions.tile.TileTitleSource; import org.chromium.chrome.browser.suggestions.tile.TileTitleSource;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner; import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule; import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.url.GURL; import org.chromium.url.GURL;
import java.io.File; import java.io.File;
...@@ -30,6 +35,7 @@ import java.util.ArrayList; ...@@ -30,6 +35,7 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* Instrumentation tests for {@link MostVisitedSitesMetadataUtils}. * Instrumentation tests for {@link MostVisitedSitesMetadataUtils}.
...@@ -40,17 +46,18 @@ public class MostVisitedSitesMetadataUtilsTest { ...@@ -40,17 +46,18 @@ public class MostVisitedSitesMetadataUtilsTest {
@Rule @Rule
public ChromeTabbedActivityTestRule mTestSetupRule = new ChromeTabbedActivityTestRule(); public ChromeTabbedActivityTestRule mTestSetupRule = new ChromeTabbedActivityTestRule();
private List<SiteSuggestion> mExpectedSiteSuggestions; private MostVisitedSitesMetadataUtils mMostVisitedSitesMetadataUtils;
@Before @Before
public void setUp() { public void setUp() {
mTestSetupRule.startMainActivityOnBlankPage(); mTestSetupRule.startMainActivityOnBlankPage();
mMostVisitedSitesMetadataUtils = MostVisitedSitesMetadataUtils.getInstance();
} }
@Test @Test
@SmallTest @SmallTest
public void testSaveRestoreConsistency() throws InterruptedException, IOException { public void testSaveRestoreConsistency() throws InterruptedException, IOException {
mExpectedSiteSuggestions = createFakeSiteSuggestions(); List<Tile> expectedSuggestionTiles = createFakeSiteSuggestionTiles1();
// Get old file and ensure to delete it. // Get old file and ensure to delete it.
File oldFile = MostVisitedSitesMetadataUtils.getOrCreateTopSitesDirectory(); File oldFile = MostVisitedSitesMetadataUtils.getOrCreateTopSitesDirectory();
...@@ -59,17 +66,20 @@ public class MostVisitedSitesMetadataUtilsTest { ...@@ -59,17 +66,20 @@ public class MostVisitedSitesMetadataUtilsTest {
// Save suggestion lists to file. // Save suggestion lists to file.
final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1);
MostVisitedSitesMetadataUtils.saveSuggestionListsToFile( MostVisitedSitesMetadataUtils.saveSuggestionListsToFile(
mExpectedSiteSuggestions, latch::countDown); expectedSuggestionTiles, latch::countDown);
// Wait util the file has been saved. // Wait util the file has been saved.
latch.await(); latch.await();
// Restore list from file after saving finished. // Restore list from file after saving finished.
List<SiteSuggestion> sitesAfterRestore = List<Tile> sitesAfterRestore = MostVisitedSitesMetadataUtils.restoreFileToSuggestionLists();
MostVisitedSitesMetadataUtils.restoreFileToSuggestionLists();
// Ensure that the new list equals to old list. // Ensure that the new list equals to old list.
assertEquals(mExpectedSiteSuggestions, sitesAfterRestore); assertEquals(expectedSuggestionTiles.size(), sitesAfterRestore.size());
for (int i = 0; i < expectedSuggestionTiles.size(); i++) {
assertEquals(
expectedSuggestionTiles.get(i).getData(), sitesAfterRestore.get(i).getData());
}
} }
@Test(expected = IOException.class) @Test(expected = IOException.class)
...@@ -83,15 +93,78 @@ public class MostVisitedSitesMetadataUtilsTest { ...@@ -83,15 +93,78 @@ public class MostVisitedSitesMetadataUtilsTest {
MostVisitedSitesMetadataUtils.restoreFileToSuggestionLists(); MostVisitedSitesMetadataUtils.restoreFileToSuggestionLists();
} }
private static List<SiteSuggestion> createFakeSiteSuggestions() { /**
List<SiteSuggestion> siteSuggestions = new ArrayList<>(); * Test when current task is not finished, all coming tasks will be set as the pending task.
siteSuggestions.add(new SiteSuggestion("0 TOP_SITES", new GURL("https://www.foo.com"), "", * Besides, the latest task will override the old one.
*/
@Test
@SmallTest
public void testCurrentNotNull() {
mMostVisitedSitesMetadataUtils.setCurrentTaskForTesting(() -> {});
Runnable task1 = ()
-> mMostVisitedSitesMetadataUtils.saveSuggestionListsToFile(
createFakeSiteSuggestionTiles1());
List<Tile> task2Tiles = createFakeSiteSuggestionTiles2();
Runnable task2 = () -> mMostVisitedSitesMetadataUtils.saveSuggestionListsToFile(task2Tiles);
// If current task is not null, all saving tasks should be set as pending task.
TestThreadUtils.runOnUiThreadBlocking(() -> {
task1.run();
task2.run();
});
// newTopSites1 should be skipped and newTopSites2 should be the pending task.
assertEquals(task2Tiles.size(),
mMostVisitedSitesMetadataUtils.getPendingTaskTilesNumForTesting());
}
/**
* Test when current task is finished, the pending task should be set as current task and run.
*/
@Test
@SmallTest
public void testTasksContinuity() {
AtomicBoolean isPendingRun = new AtomicBoolean(false);
// Set and run current task.
assertNull(mMostVisitedSitesMetadataUtils.getCurrentTaskForTesting());
TestThreadUtils.runOnUiThreadBlocking(
()
-> mMostVisitedSitesMetadataUtils.saveSuggestionListsToFile(
createFakeSiteSuggestionTiles1()));
// When current task is not finished, set pending task.
assertNotNull(mMostVisitedSitesMetadataUtils.getCurrentTaskForTesting());
mMostVisitedSitesMetadataUtils.setPendingTaskForTesting(() -> isPendingRun.set(true));
// isPendingRun should eventually become true.
CriteriaHelper.pollInstrumentationThread(isPendingRun::get);
}
private static List<Tile> createFakeSiteSuggestionTiles1() {
List<Tile> suggestionTiles = new ArrayList<>();
SiteSuggestion data = new SiteSuggestion("0 TOP_SITES", new GURL("https://www.foo.com"), "",
TileTitleSource.TITLE_TAG, TileSource.TOP_SITES, TileSectionType.PERSONALIZED, TileTitleSource.TITLE_TAG, TileSource.TOP_SITES, TileSectionType.PERSONALIZED,
new Date())); new Date());
siteSuggestions.add(new SiteSuggestion("1 ALLOWLIST", new GURL("https://www.bar.com"), "", suggestionTiles.add(new Tile(data, 0));
data = new SiteSuggestion("1 ALLOWLIST", new GURL("https://www.bar.com"), "",
TileTitleSource.UNKNOWN, TileSource.ALLOWLIST, TileSectionType.PERSONALIZED, TileTitleSource.UNKNOWN, TileSource.ALLOWLIST, TileSectionType.PERSONALIZED,
new Date())); new Date());
siteSuggestions.get(1).faviconId = 1; suggestionTiles.add(new Tile(data, 1));
return siteSuggestions;
return suggestionTiles;
}
private static List<Tile> createFakeSiteSuggestionTiles2() {
List<Tile> suggestionTiles = new ArrayList<>();
SiteSuggestion data = new SiteSuggestion("0 TOP_SITES", new GURL("https://www.baz.com"), "",
TileTitleSource.TITLE_TAG, TileSource.TOP_SITES, TileSectionType.PERSONALIZED,
new Date());
suggestionTiles.add(new Tile(data, 0));
return suggestionTiles;
} }
} }
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