--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -16,16 +16,17 @@ import org.mozilla.gecko.DynamicToolbar.
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.Tabs.TabEvents;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.TransitionsTracker;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.LocalSuggestedSites;
import org.mozilla.gecko.db.SuggestedSites;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.dlc.DownloadContentService;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
@@ -700,17 +701,17 @@ public class BrowserApp extends GeckoApp
"Sanitize:ClearSyncedTabs",
"Settings:Show",
"Telemetry:Gather",
"Updater:Launch");
Distribution distribution = Distribution.init(this);
// Init suggested sites engine in BrowserDB.
- final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
+ final SuggestedSites suggestedSites = new LocalSuggestedSites(appContext, distribution);
final BrowserDB db = getProfile().getDB();
db.setSuggestedSites(suggestedSites);
JavaAddonManager.getInstance().init(appContext);
mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
mOrderedBroadcastHelper = new OrderedBroadcastHelper(appContext);
mReadingListHelper = new ReadingListHelper(appContext, getProfile(), this);
mAccountsHelper = new AccountsHelper(appContext, getProfile());
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalSuggestedSites.java
@@ -0,0 +1,523 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.db;
+
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MatrixCursor.RowBuilder;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.mozilla.gecko.annotation.RobocopTarget;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.Restrictions;
+import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.util.RawResource;
+import org.mozilla.gecko.util.ThreadUtils;
+
+@RobocopTarget
+public class LocalSuggestedSites implements SuggestedSites {
+ private static final String LOGTAG = "GeckoSuggestedSites";
+
+ // File in profile dir with the list of suggested sites.
+ private static final String FILENAME = "suggestedsites.json";
+
+ private static final String[] COLUMNS = new String[] {
+ BrowserContract.SuggestedSites._ID,
+ BrowserContract.SuggestedSites.URL,
+ BrowserContract.SuggestedSites.TITLE,
+ };
+
+ final Context context;
+ final Distribution distribution;
+ private File cachedFile;
+ private Map<String, Site> cachedSites;
+ private Set<String> cachedBlacklist;
+
+ public LocalSuggestedSites(Context appContext) {
+ this(appContext, null);
+ }
+
+ public LocalSuggestedSites(Context appContext, Distribution distribution) {
+ this(appContext, distribution, null);
+ }
+
+ public LocalSuggestedSites(Context appContext, Distribution distribution, File file) {
+ this.context = appContext;
+ this.distribution = distribution;
+ this.cachedFile = file;
+ }
+
+ synchronized File getFile() {
+ if (cachedFile == null) {
+ cachedFile = GeckoProfile.get(context).getFile(FILENAME);
+ }
+ return cachedFile;
+ }
+
+ private static boolean isNewLocale(Context context, Locale requestedLocale) {
+ final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
+
+ String locale = prefs.getString(PREF_SUGGESTED_SITES_LOCALE, null);
+ if (locale == null) {
+ // Initialize config with the current locale
+ updateSuggestedSitesLocale(context);
+ return true;
+ }
+
+ return !TextUtils.equals(requestedLocale.toString(), locale);
+ }
+
+ /**
+ * Return the current locale and its fallback (en_US) in order.
+ */
+ private static List<Locale> getAcceptableLocales() {
+ final List<Locale> locales = new ArrayList<Locale>();
+
+ final Locale defaultLocale = Locale.getDefault();
+ locales.add(defaultLocale);
+
+ if (!defaultLocale.equals(Locale.US)) {
+ locales.add(Locale.US);
+ }
+
+ return locales;
+ }
+
+ private static Map<String, Site> loadSites(File f) throws IOException {
+ Scanner scanner = null;
+
+ try {
+ scanner = new Scanner(f, "UTF-8");
+ return loadSites(scanner.useDelimiter("\\A").next());
+ } finally {
+ if (scanner != null) {
+ scanner.close();
+ }
+ }
+ }
+
+ private static Map<String, Site> loadSites(String jsonString) {
+ if (TextUtils.isEmpty(jsonString)) {
+ return null;
+ }
+
+ Map<String, Site> sites = null;
+
+ try {
+ final JSONArray jsonSites = new JSONArray(jsonString);
+ sites = new LinkedHashMap<String, Site>(jsonSites.length());
+
+ final int count = jsonSites.length();
+ for (int i = 0; i < count; i++) {
+ final Site site = new Site(jsonSites.getJSONObject(i));
+ sites.put(site.url, site);
+ }
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Failed to refresh suggested sites", e);
+ return null;
+ }
+
+ return sites;
+ }
+
+ /**
+ * Saves suggested sites file to disk. Access to this method should
+ * be synchronized on 'file'.
+ */
+ static void saveSites(File f, Map<String, Site> sites) {
+ ThreadUtils.assertNotOnUiThread();
+
+ if (sites == null || sites.isEmpty()) {
+ return;
+ }
+
+ OutputStreamWriter osw = null;
+
+ try {
+ final JSONArray jsonSites = new JSONArray();
+ for (Site site : sites.values()) {
+ jsonSites.put(site.toJSON());
+ }
+
+ osw = new OutputStreamWriter(new FileOutputStream(f), "UTF-8");
+
+ final String jsonString = jsonSites.toString();
+ osw.write(jsonString, 0, jsonString.length());
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Failed to save suggested sites", e);
+ } finally {
+ if (osw != null) {
+ try {
+ osw.close();
+ } catch (IOException e) {
+ // Ignore.
+ }
+ }
+ }
+ }
+
+ private void maybeWaitForDistribution() {
+ if (distribution == null) {
+ return;
+ }
+
+ distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
+ @Override
+ public void distributionNotFound() {
+ // If distribution doesn't exist, simply continue to load
+ // suggested sites directly from resources. See refresh().
+ }
+
+ @Override
+ public void distributionFound(Distribution distribution) {
+ Log.d(LOGTAG, "Running post-distribution task: suggested sites.");
+ // Merge suggested sites from distribution with the
+ // default ones. Distribution takes precedence.
+ Map<String, Site> sites = loadFromDistribution(distribution);
+ if (sites == null) {
+ sites = new LinkedHashMap<String, Site>();
+ }
+ sites.putAll(loadFromResource());
+
+ // Update cached list of sites.
+ setCachedSites(sites);
+
+ // Save the result to disk.
+ final File file = getFile();
+ synchronized (file) {
+ saveSites(file, sites);
+ }
+
+ // Then notify any active loaders about the changes.
+ final ContentResolver cr = context.getContentResolver();
+ cr.notifyChange(BrowserContract.SuggestedSites.CONTENT_URI, null);
+ }
+
+ @Override
+ public void distributionArrivedLate(Distribution distribution) {
+ distributionFound(distribution);
+ }
+ });
+ }
+
+ /**
+ * Loads suggested sites from a distribution file either matching the
+ * current locale or with the fallback locale (en-US).
+ *
+ * It's assumed that the given distribution instance is ready to be
+ * used and exists.
+ */
+ static Map<String, Site> loadFromDistribution(Distribution dist) {
+ for (Locale locale : getAcceptableLocales()) {
+ try {
+ final String languageTag = Locales.getLanguageTag(locale);
+ final String path = String.format("suggestedsites/locales/%s/%s",
+ languageTag, FILENAME);
+
+ final File f = dist.getDistributionFile(path);
+ if (f == null) {
+ Log.d(LOGTAG, "No suggested sites for locale: " + languageTag);
+ continue;
+ }
+
+ return loadSites(f);
+ } catch (Exception e) {
+ Log.e(LOGTAG, "Failed to open suggested sites for locale " +
+ locale + " in distribution.", e);
+ }
+ }
+
+ return null;
+ }
+
+ private Map<String, Site> loadFromProfile() {
+ try {
+ final File file = getFile();
+ synchronized (file) {
+ return loadSites(file);
+ }
+ } catch (FileNotFoundException e) {
+ maybeWaitForDistribution();
+ } catch (IOException e) {
+ // Fall through, return null.
+ }
+
+ return null;
+ }
+
+ Map<String, Site> loadFromResource() {
+ try {
+ return loadSites(RawResource.getAsString(context, R.raw.suggestedsites));
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ private synchronized void setCachedSites(Map<String, Site> sites) {
+ cachedSites = Collections.unmodifiableMap(sites);
+ updateSuggestedSitesLocale(context);
+ }
+
+ /**
+ * Refreshes the cached list of sites either from the default raw
+ * source or standard file location. This will be called on every
+ * cache miss during a {@code get()} call.
+ */
+ private void refresh() {
+ Log.d(LOGTAG, "Refreshing suggested sites from file");
+
+ Map<String, Site> sites = loadFromProfile();
+ if (sites == null) {
+ sites = loadFromResource();
+ }
+
+ // Update cached list of sites.
+ if (sites != null) {
+ setCachedSites(sites);
+ }
+ }
+
+ private static void updateSuggestedSitesLocale(Context context) {
+ final Editor editor = GeckoSharedPrefs.forProfile(context).edit();
+ editor.putString(PREF_SUGGESTED_SITES_LOCALE, Locale.getDefault().toString());
+ editor.apply();
+ }
+
+ private boolean isEnabled() {
+ final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
+ return prefs.getBoolean(GeckoPreferences.PREFS_SUGGESTED_SITES, true);
+ }
+
+ private synchronized Site getSiteForUrl(String url) {
+ if (cachedSites == null) {
+ return null;
+ }
+
+ return cachedSites.get(url);
+ }
+
+ /**
+ * Returns a {@code Cursor} with the list of suggested websites.
+ *
+ * @param limit maximum number of suggested sites.
+ */
+ @Override
+ public Cursor get(int limit) {
+ return get(limit, Locale.getDefault());
+ }
+
+ /**
+ * Returns a {@code Cursor} with the list of suggested websites.
+ *
+ * @param limit maximum number of suggested sites.
+ * @param locale the target locale.
+ */
+ @Override
+ public Cursor get(int limit, Locale locale) {
+ return get(limit, locale, null);
+ }
+
+ /**
+ * Returns a {@code Cursor} with the list of suggested websites.
+ *
+ * @param limit maximum number of suggested sites.
+ * @param excludeUrls list of URLs to be excluded from the list.
+ */
+ @Override
+ public Cursor get(int limit, List<String> excludeUrls) {
+ return get(limit, Locale.getDefault(), excludeUrls);
+ }
+
+ /**
+ * Returns a {@code Cursor} with the list of suggested websites.
+ *
+ * @param limit maximum number of suggested sites.
+ * @param locale the target locale.
+ * @param excludeUrls list of URLs to be excluded from the list.
+ */
+ @Override
+ public synchronized Cursor get(int limit, Locale locale, List<String> excludeUrls) {
+ final MatrixCursor cursor = new MatrixCursor(COLUMNS);
+
+ // Return an empty cursor if suggested sites have been
+ // disabled by the user.
+ if (!isEnabled()) {
+ return cursor;
+ }
+
+ final boolean isNewLocale = isNewLocale(context, locale);
+
+ // Force the suggested sites file in profile dir to be re-generated
+ // if the locale has changed.
+ if (isNewLocale) {
+ getFile().delete();
+ }
+
+ if (cachedSites == null || isNewLocale) {
+ Log.d(LOGTAG, "No cached sites, refreshing.");
+ refresh();
+ }
+
+ // Return empty cursor if there was an error when
+ // loading the suggested sites or the list is empty.
+ if (cachedSites == null || cachedSites.isEmpty()) {
+ return cursor;
+ }
+
+ excludeUrls = includeBlacklist(excludeUrls);
+
+ final int sitesCount = cachedSites.size();
+ Log.d(LOGTAG, "Number of suggested sites: " + sitesCount);
+
+ final int maxCount = Math.min(limit, sitesCount);
+ for (Site site : cachedSites.values()) {
+ if (cursor.getCount() == maxCount) {
+ break;
+ }
+
+ if (excludeUrls != null && excludeUrls.contains(site.url)) {
+ continue;
+ }
+
+ final boolean restrictedProfile = Restrictions.isRestrictedProfile(context);
+
+ if (restrictedProfile == site.restricted) {
+ final RowBuilder row = cursor.newRow();
+ row.add(-1);
+ row.add(site.url);
+ row.add(site.title);
+ }
+ }
+
+ cursor.setNotificationUri(context.getContentResolver(),
+ BrowserContract.SuggestedSites.CONTENT_URI);
+
+ return cursor;
+ }
+
+ @Override
+ public boolean contains(String url) {
+ return (getSiteForUrl(url) != null);
+ }
+
+ @Override
+ public String getImageUrlForUrl(String url) {
+ final Site site = getSiteForUrl(url);
+ return (site != null ? site.imageUrl : null);
+ }
+
+ @Override
+ public String getBackgroundColorForUrl(String url) {
+ final Site site = getSiteForUrl(url);
+ return (site != null ? site.bgColor : null);
+ }
+
+ private Set<String> loadBlacklist() {
+ Log.d(LOGTAG, "Loading blacklisted suggested sites from SharedPreferences.");
+ final Set<String> blacklist = new HashSet<String>();
+
+ final SharedPreferences preferences = GeckoSharedPrefs.forProfile(context);
+ final String sitesString = preferences.getString(PREF_SUGGESTED_SITES_HIDDEN, null);
+
+ if (sitesString != null) {
+ for (String site : sitesString.trim().split(" ")) {
+ blacklist.add(Uri.decode(site));
+ }
+ }
+
+ return blacklist;
+ }
+
+ private List<String> includeBlacklist(List<String> originalList) {
+ if (cachedBlacklist == null) {
+ cachedBlacklist = loadBlacklist();
+ }
+
+ if (cachedBlacklist.isEmpty()) {
+ return originalList;
+ }
+
+ if (originalList == null) {
+ originalList = new ArrayList<String>();
+ }
+
+ originalList.addAll(cachedBlacklist);
+ return originalList;
+ }
+
+ /**
+ * Blacklist a suggested site so it will no longer be returned as a suggested site.
+ * This method should only be called from a background thread because it may write
+ * to SharedPreferences.
+ *
+ * Urls that are not Suggested Sites are ignored.
+ *
+ * @param url String url of site to blacklist
+ * @return true is blacklisted, false otherwise
+ */
+ @Override
+ public synchronized boolean hideSite(String url) {
+ ThreadUtils.assertNotOnUiThread();
+
+ if (cachedSites == null) {
+ refresh();
+ if (cachedSites == null) {
+ Log.w(LOGTAG, "Could not load suggested sites!");
+ return false;
+ }
+ }
+
+ if (cachedSites.containsKey(url)) {
+ if (cachedBlacklist == null) {
+ cachedBlacklist = loadBlacklist();
+ }
+
+ // Check if site has already been blacklisted, just in case.
+ if (!cachedBlacklist.contains(url)) {
+
+ saveToBlacklist(url);
+ cachedBlacklist.add(url);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void saveToBlacklist(String url) {
+ final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
+ final String prefString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN, "");
+ final String siteString = prefString.concat(" " + Uri.encode(url));
+ prefs.edit().putString(PREF_SUGGESTED_SITES_HIDDEN, siteString).apply();
+ }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java
@@ -1,54 +1,23 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.db;
-import android.content.Context;
-import android.content.ContentResolver;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.database.MatrixCursor.RowBuilder;
-import android.net.Uri;
import android.text.TextUtils;
-import android.util.Log;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
+import org.json.JSONException;
+import org.json.JSONObject;
+
import java.util.List;
import java.util.Locale;
-import java.util.Map;
-import java.util.Scanner;
-import java.util.Set;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.annotation.RobocopTarget;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.distribution.Distribution;
-import org.mozilla.gecko.Restrictions;
-import org.mozilla.gecko.preferences.GeckoPreferences;
-import org.mozilla.gecko.util.RawResource;
-import org.mozilla.gecko.util.ThreadUtils;
/**
* {@code SuggestedSites} provides API to get a list of locale-specific
* suggested sites to be used in Fennec's top sites panel. It provides
* only a single method to fetch the list as a {@code Cursor}. This cursor
* will then be wrapped by {@code TopSitesCursorWrapper} to blend top,
* pinned, and suggested sites in the UI. The returned {@code Cursor}
* uses its own schema defined in {@code BrowserContract.SuggestedSites}
@@ -59,42 +28,45 @@ import org.mozilla.gecko.util.ThreadUtil
* {@code get()} call.
*
* The default list of suggested sites is stored in a raw Android
* resource ({@code R.raw.suggestedsites}) which is dynamically
* generated at build time for each target locale.
*
* Changes to the list of suggested sites are saved in SharedPreferences.
*/
-@RobocopTarget
-public class SuggestedSites {
- private static final String LOGTAG = "GeckoSuggestedSites";
-
+public interface SuggestedSites {
// SharedPreference key for suggested sites that should be hidden.
- public static final String PREF_SUGGESTED_SITES_HIDDEN = "suggestedSites.hidden";
-
+ String PREF_SUGGESTED_SITES_HIDDEN = "suggestedSites.hidden";
// Locale used to generate the current suggested sites.
- public static final String PREF_SUGGESTED_SITES_LOCALE = "suggestedSites.locale";
+ String PREF_SUGGESTED_SITES_LOCALE = "suggestedSites.locale";
+
+ Cursor get(int limit);
- // File in profile dir with the list of suggested sites.
- private static final String FILENAME = "suggestedsites.json";
+ Cursor get(int limit, Locale locale);
+
+ Cursor get(int limit, List<String> excludeUrls);
+
+ Cursor get(int limit, Locale locale, List<String> excludeUrls);
- private static final String[] COLUMNS = new String[] {
- BrowserContract.SuggestedSites._ID,
- BrowserContract.SuggestedSites.URL,
- BrowserContract.SuggestedSites.TITLE,
- };
+ boolean contains(String url);
+
+ String getImageUrlForUrl(String url);
+
+ String getBackgroundColorForUrl(String url);
+
+ boolean hideSite(String url);
- private static final String JSON_KEY_URL = "url";
- private static final String JSON_KEY_TITLE = "title";
- private static final String JSON_KEY_IMAGE_URL = "imageurl";
- private static final String JSON_KEY_BG_COLOR = "bgcolor";
- private static final String JSON_KEY_RESTRICTED = "restricted";
+ static final String JSON_KEY_URL = "url";
+ static final String JSON_KEY_TITLE = "title";
+ static final String JSON_KEY_IMAGE_URL = "imageurl";
+ static final String JSON_KEY_BG_COLOR = "bgcolor";
+ static final String JSON_KEY_RESTRICTED = "restricted";
- private static class Site {
+ public static class Site {
public final String url;
public final String title;
public final String imageUrl;
public final String bgColor;
public final boolean restricted;
public Site(JSONObject json) throws JSONException {
this.restricted = !json.isNull(JSON_KEY_RESTRICTED);
@@ -146,466 +118,9 @@ public class SuggestedSites {
json.put(JSON_KEY_URL, url);
json.put(JSON_KEY_TITLE, title);
json.put(JSON_KEY_IMAGE_URL, imageUrl);
json.put(JSON_KEY_BG_COLOR, bgColor);
return json;
}
}
-
- final Context context;
- final Distribution distribution;
- private File cachedFile;
- private Map<String, Site> cachedSites;
- private Set<String> cachedBlacklist;
-
- public SuggestedSites(Context appContext) {
- this(appContext, null);
- }
-
- public SuggestedSites(Context appContext, Distribution distribution) {
- this(appContext, distribution, null);
- }
-
- public SuggestedSites(Context appContext, Distribution distribution, File file) {
- this.context = appContext;
- this.distribution = distribution;
- this.cachedFile = file;
- }
-
- synchronized File getFile() {
- if (cachedFile == null) {
- cachedFile = GeckoProfile.get(context).getFile(FILENAME);
- }
- return cachedFile;
- }
-
- private static boolean isNewLocale(Context context, Locale requestedLocale) {
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
-
- String locale = prefs.getString(PREF_SUGGESTED_SITES_LOCALE, null);
- if (locale == null) {
- // Initialize config with the current locale
- updateSuggestedSitesLocale(context);
- return true;
- }
-
- return !TextUtils.equals(requestedLocale.toString(), locale);
- }
-
- /**
- * Return the current locale and its fallback (en_US) in order.
- */
- private static List<Locale> getAcceptableLocales() {
- final List<Locale> locales = new ArrayList<Locale>();
-
- final Locale defaultLocale = Locale.getDefault();
- locales.add(defaultLocale);
-
- if (!defaultLocale.equals(Locale.US)) {
- locales.add(Locale.US);
- }
-
- return locales;
- }
-
- private static Map<String, Site> loadSites(File f) throws IOException {
- Scanner scanner = null;
-
- try {
- scanner = new Scanner(f, "UTF-8");
- return loadSites(scanner.useDelimiter("\\A").next());
- } finally {
- if (scanner != null) {
- scanner.close();
- }
- }
- }
-
- private static Map<String, Site> loadSites(String jsonString) {
- if (TextUtils.isEmpty(jsonString)) {
- return null;
- }
-
- Map<String, Site> sites = null;
-
- try {
- final JSONArray jsonSites = new JSONArray(jsonString);
- sites = new LinkedHashMap<String, Site>(jsonSites.length());
-
- final int count = jsonSites.length();
- for (int i = 0; i < count; i++) {
- final Site site = new Site(jsonSites.getJSONObject(i));
- sites.put(site.url, site);
- }
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to refresh suggested sites", e);
- return null;
- }
-
- return sites;
- }
-
- /**
- * Saves suggested sites file to disk. Access to this method should
- * be synchronized on 'file'.
- */
- static void saveSites(File f, Map<String, Site> sites) {
- ThreadUtils.assertNotOnUiThread();
-
- if (sites == null || sites.isEmpty()) {
- return;
- }
-
- OutputStreamWriter osw = null;
-
- try {
- final JSONArray jsonSites = new JSONArray();
- for (Site site : sites.values()) {
- jsonSites.put(site.toJSON());
- }
-
- osw = new OutputStreamWriter(new FileOutputStream(f), "UTF-8");
-
- final String jsonString = jsonSites.toString();
- osw.write(jsonString, 0, jsonString.length());
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to save suggested sites", e);
- } finally {
- if (osw != null) {
- try {
- osw.close();
- } catch (IOException e) {
- // Ignore.
- }
- }
- }
- }
-
- private void maybeWaitForDistribution() {
- if (distribution == null) {
- return;
- }
-
- distribution.addOnDistributionReadyCallback(new Distribution.ReadyCallback() {
- @Override
- public void distributionNotFound() {
- // If distribution doesn't exist, simply continue to load
- // suggested sites directly from resources. See refresh().
- }
-
- @Override
- public void distributionFound(Distribution distribution) {
- Log.d(LOGTAG, "Running post-distribution task: suggested sites.");
- // Merge suggested sites from distribution with the
- // default ones. Distribution takes precedence.
- Map<String, Site> sites = loadFromDistribution(distribution);
- if (sites == null) {
- sites = new LinkedHashMap<String, Site>();
- }
- sites.putAll(loadFromResource());
-
- // Update cached list of sites.
- setCachedSites(sites);
-
- // Save the result to disk.
- final File file = getFile();
- synchronized (file) {
- saveSites(file, sites);
- }
-
- // Then notify any active loaders about the changes.
- final ContentResolver cr = context.getContentResolver();
- cr.notifyChange(BrowserContract.SuggestedSites.CONTENT_URI, null);
- }
-
- @Override
- public void distributionArrivedLate(Distribution distribution) {
- distributionFound(distribution);
- }
- });
- }
-
- /**
- * Loads suggested sites from a distribution file either matching the
- * current locale or with the fallback locale (en-US).
- *
- * It's assumed that the given distribution instance is ready to be
- * used and exists.
- */
- static Map<String, Site> loadFromDistribution(Distribution dist) {
- for (Locale locale : getAcceptableLocales()) {
- try {
- final String languageTag = Locales.getLanguageTag(locale);
- final String path = String.format("suggestedsites/locales/%s/%s",
- languageTag, FILENAME);
-
- final File f = dist.getDistributionFile(path);
- if (f == null) {
- Log.d(LOGTAG, "No suggested sites for locale: " + languageTag);
- continue;
- }
-
- return loadSites(f);
- } catch (Exception e) {
- Log.e(LOGTAG, "Failed to open suggested sites for locale " +
- locale + " in distribution.", e);
- }
- }
-
- return null;
- }
-
- private Map<String, Site> loadFromProfile() {
- try {
- final File file = getFile();
- synchronized (file) {
- return loadSites(file);
- }
- } catch (FileNotFoundException e) {
- maybeWaitForDistribution();
- } catch (IOException e) {
- // Fall through, return null.
- }
-
- return null;
- }
-
- Map<String, Site> loadFromResource() {
- try {
- return loadSites(RawResource.getAsString(context, R.raw.suggestedsites));
- } catch (IOException e) {
- return null;
- }
- }
-
- private synchronized void setCachedSites(Map<String, Site> sites) {
- cachedSites = Collections.unmodifiableMap(sites);
- updateSuggestedSitesLocale(context);
- }
-
- /**
- * Refreshes the cached list of sites either from the default raw
- * source or standard file location. This will be called on every
- * cache miss during a {@code get()} call.
- */
- private void refresh() {
- Log.d(LOGTAG, "Refreshing suggested sites from file");
-
- Map<String, Site> sites = loadFromProfile();
- if (sites == null) {
- sites = loadFromResource();
- }
-
- // Update cached list of sites.
- if (sites != null) {
- setCachedSites(sites);
- }
- }
-
- private static void updateSuggestedSitesLocale(Context context) {
- final Editor editor = GeckoSharedPrefs.forProfile(context).edit();
- editor.putString(PREF_SUGGESTED_SITES_LOCALE, Locale.getDefault().toString());
- editor.apply();
- }
-
- private boolean isEnabled() {
- final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
- return prefs.getBoolean(GeckoPreferences.PREFS_SUGGESTED_SITES, true);
- }
-
- private synchronized Site getSiteForUrl(String url) {
- if (cachedSites == null) {
- return null;
- }
-
- return cachedSites.get(url);
- }
-
- /**
- * Returns a {@code Cursor} with the list of suggested websites.
- *
- * @param limit maximum number of suggested sites.
- */
- public Cursor get(int limit) {
- return get(limit, Locale.getDefault());
- }
-
- /**
- * Returns a {@code Cursor} with the list of suggested websites.
- *
- * @param limit maximum number of suggested sites.
- * @param locale the target locale.
- */
- public Cursor get(int limit, Locale locale) {
- return get(limit, locale, null);
- }
-
- /**
- * Returns a {@code Cursor} with the list of suggested websites.
- *
- * @param limit maximum number of suggested sites.
- * @param excludeUrls list of URLs to be excluded from the list.
- */
- public Cursor get(int limit, List<String> excludeUrls) {
- return get(limit, Locale.getDefault(), excludeUrls);
- }
-
- /**
- * Returns a {@code Cursor} with the list of suggested websites.
- *
- * @param limit maximum number of suggested sites.
- * @param locale the target locale.
- * @param excludeUrls list of URLs to be excluded from the list.
- */
- public synchronized Cursor get(int limit, Locale locale, List<String> excludeUrls) {
- final MatrixCursor cursor = new MatrixCursor(COLUMNS);
-
- // Return an empty cursor if suggested sites have been
- // disabled by the user.
- if (!isEnabled()) {
- return cursor;
- }
-
- final boolean isNewLocale = isNewLocale(context, locale);
-
- // Force the suggested sites file in profile dir to be re-generated
- // if the locale has changed.
- if (isNewLocale) {
- getFile().delete();
- }
-
- if (cachedSites == null || isNewLocale) {
- Log.d(LOGTAG, "No cached sites, refreshing.");
- refresh();
- }
-
- // Return empty cursor if there was an error when
- // loading the suggested sites or the list is empty.
- if (cachedSites == null || cachedSites.isEmpty()) {
- return cursor;
- }
-
- excludeUrls = includeBlacklist(excludeUrls);
-
- final int sitesCount = cachedSites.size();
- Log.d(LOGTAG, "Number of suggested sites: " + sitesCount);
-
- final int maxCount = Math.min(limit, sitesCount);
- for (Site site : cachedSites.values()) {
- if (cursor.getCount() == maxCount) {
- break;
- }
-
- if (excludeUrls != null && excludeUrls.contains(site.url)) {
- continue;
- }
-
- final boolean restrictedProfile = Restrictions.isRestrictedProfile(context);
-
- if (restrictedProfile == site.restricted) {
- final RowBuilder row = cursor.newRow();
- row.add(-1);
- row.add(site.url);
- row.add(site.title);
- }
- }
-
- cursor.setNotificationUri(context.getContentResolver(),
- BrowserContract.SuggestedSites.CONTENT_URI);
-
- return cursor;
- }
-
- public boolean contains(String url) {
- return (getSiteForUrl(url) != null);
- }
-
- public String getImageUrlForUrl(String url) {
- final Site site = getSiteForUrl(url);
- return (site != null ? site.imageUrl : null);
- }
-
- public String getBackgroundColorForUrl(String url) {
- final Site site = getSiteForUrl(url);
- return (site != null ? site.bgColor : null);
- }
-
- private Set<String> loadBlacklist() {
- Log.d(LOGTAG, "Loading blacklisted suggested sites from SharedPreferences.");
- final Set<String> blacklist = new HashSet<String>();
-
- final SharedPreferences preferences = GeckoSharedPrefs.forProfile(context);
- final String sitesString = preferences.getString(PREF_SUGGESTED_SITES_HIDDEN, null);
-
- if (sitesString != null) {
- for (String site : sitesString.trim().split(" ")) {
- blacklist.add(Uri.decode(site));
- }
- }
-
- return blacklist;
- }
-
- private List<String> includeBlacklist(List<String> originalList) {
- if (cachedBlacklist == null) {
- cachedBlacklist = loadBlacklist();
- }
-
- if (cachedBlacklist.isEmpty()) {
- return originalList;
- }
-
- if (originalList == null) {
- originalList = new ArrayList<String>();
- }
-
- originalList.addAll(cachedBlacklist);
- return originalList;
- }
-
- /**
- * Blacklist a suggested site so it will no longer be returned as a suggested site.
- * This method should only be called from a background thread because it may write
- * to SharedPreferences.
- *
- * Urls that are not Suggested Sites are ignored.
- *
- * @param url String url of site to blacklist
- * @return true is blacklisted, false otherwise
- */
- public synchronized boolean hideSite(String url) {
- ThreadUtils.assertNotOnUiThread();
-
- if (cachedSites == null) {
- refresh();
- if (cachedSites == null) {
- Log.w(LOGTAG, "Could not load suggested sites!");
- return false;
- }
- }
-
- if (cachedSites.containsKey(url)) {
- if (cachedBlacklist == null) {
- cachedBlacklist = loadBlacklist();
- }
-
- // Check if site has already been blacklisted, just in case.
- if (!cachedBlacklist.contains(url)) {
-
- saveToBlacklist(url);
- cachedBlacklist.add(url);
-
- return true;
- }
- }
-
- return false;
- }
-
- private void saveToBlacklist(String url) {
- final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
- final String prefString = prefs.getString(PREF_SUGGESTED_SITES_HIDDEN, "");
- final String siteString = prefString.concat(" " + Uri.encode(url));
- prefs.edit().putString(PREF_SUGGESTED_SITES_HIDDEN, siteString).apply();
- }
}
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestTopSites.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestTopSites.java
@@ -2,16 +2,17 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.db;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.Combined;
+import org.mozilla.gecko.db.LocalSuggestedSites;
import org.mozilla.gecko.db.SuggestedSites;
import org.mozilla.gecko.sync.setup.Constants;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
@@ -29,20 +30,17 @@ public class TestTopSites extends Activi
public TestTopSites() {
super(Activity.class);
}
@Override
public void setUp() {
mContext = getInstrumentation().getTargetContext();
- mSuggestedSites = new SuggestedSites(mContext);
-
- // By default we're using StubBrowserDB which has no suggested sites available.
- GeckoProfile.get(mContext, Constants.DEFAULT_PROFILE).getDB().setSuggestedSites(mSuggestedSites);
+ mSuggestedSites = new LocalSuggestedSites(mContext);
}
@Override
public void tearDown() {
GeckoProfile.get(mContext, Constants.DEFAULT_PROFILE).getDB().setSuggestedSites(null);
}
public void testGetTopSites() {
--- a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestSuggestedSites.java
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestSuggestedSites.java
@@ -15,16 +15,17 @@ import android.test.InstrumentationTestC
import android.test.RenamingDelegatingContext;
import android.test.mock.MockResources;
import org.json.JSONArray;
import org.json.JSONObject;
import org.mozilla.gecko.R;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.LocalSuggestedSites;
import org.mozilla.gecko.db.SuggestedSites;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.preferences.GeckoPreferences;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -196,17 +197,17 @@ public class TestSuggestedSites extends
}
private void checkCursorCount(String content, int expectedCount) {
checkCursorCount(content, expectedCount, DEFAULT_LIMIT);
}
private void checkCursorCount(String content, int expectedCount, int limit) {
resources.setSuggestedSitesResource(content);
- Cursor c = new SuggestedSites(context).get(limit);
+ Cursor c = new LocalSuggestedSites(context).get(limit);
assertEquals(expectedCount, c.getCount());
c.close();
}
protected void setUp() {
context = new TestContext(getInstrumentation().getTargetContext());
resources = (TestResources) context.getResources();
tempFiles = new ArrayList<File>();
@@ -239,17 +240,17 @@ public class TestSuggestedSites extends
// Invalid json string = empty cursor
checkCursorCount("{ broken: }", 0);
}
public void testCursorContent() {
resources.setSuggestedSitesResource(generateSites(3));
- Cursor c = new SuggestedSites(context).get(DEFAULT_LIMIT);
+ Cursor c = new LocalSuggestedSites(context).get(DEFAULT_LIMIT);
assertEquals(3, c.getCount());
c.moveToPosition(-1);
while (c.moveToNext()) {
int position = c.getPosition();
String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
assertEquals("url" + position, url);
@@ -269,17 +270,17 @@ public class TestSuggestedSites extends
excludedUrls.add("url3");
excludedUrls.add("url5");
List<String> includedUrls = new ArrayList<String>(3);
includedUrls.add("url0");
includedUrls.add("url2");
includedUrls.add("url4");
- Cursor c = new SuggestedSites(context).get(DEFAULT_LIMIT, excludedUrls);
+ Cursor c = new LocalSuggestedSites(context).get(DEFAULT_LIMIT, excludedUrls);
c.moveToPosition(-1);
while (c.moveToNext()) {
String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
assertFalse(excludedUrls.contains(url));
assertTrue(includedUrls.contains(url));
}
@@ -306,52 +307,52 @@ public class TestSuggestedSites extends
hiddenUrlBuilder.append(Uri.encode(s));
}
final String hiddenPref = hiddenUrlBuilder.toString();
GeckoSharedPrefs.forProfile(context).edit()
.putString(SuggestedSites.PREF_SUGGESTED_SITES_HIDDEN, hiddenPref)
.commit();
- Cursor c = new SuggestedSites(context).get(DEFAULT_LIMIT);
+ Cursor c = new LocalSuggestedSites(context).get(DEFAULT_LIMIT);
assertEquals(Math.min(3, DEFAULT_LIMIT), c.getCount());
c.moveToPosition(-1);
while (c.moveToNext()) {
String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL));
assertFalse(hiddenUrls.contains(url));
assertTrue(visibleUrls.contains(url));
}
c.close();
}
public void testDisabledState() {
resources.setSuggestedSitesResource(generateSites(3));
- Cursor c = new SuggestedSites(context).get(DEFAULT_LIMIT);
+ Cursor c = new LocalSuggestedSites(context).get(DEFAULT_LIMIT);
assertEquals(3, c.getCount());
c.close();
// Disable suggested sites
GeckoSharedPrefs.forApp(context).edit()
.putBoolean(GeckoPreferences.PREFS_SUGGESTED_SITES, false)
.commit();
- c = new SuggestedSites(context).get(DEFAULT_LIMIT);
+ c = new LocalSuggestedSites(context).get(DEFAULT_LIMIT);
assertNotNull(c);
assertEquals(0, c.getCount());
c.close();
}
public void testImageUrlAndBgColor() {
final int count = 3;
resources.setSuggestedSitesResource(generateSites(count));
- SuggestedSites suggestedSites = new SuggestedSites(context);
+ SuggestedSites suggestedSites = new LocalSuggestedSites(context);
// Suggested sites hasn't been loaded yet.
for (int i = 0; i < count; i++) {
String url = "url" + i;
assertFalse(suggestedSites.contains(url));
assertNull(suggestedSites.getImageUrlForUrl(url));
assertNull(suggestedSites.getBackgroundColorForUrl(url));
}
@@ -374,17 +375,17 @@ public class TestSuggestedSites extends
assertFalse(suggestedSites.contains("foo"));
assertNull(suggestedSites.getImageUrlForUrl("foo"));
assertNull(suggestedSites.getBackgroundColorForUrl("foo"));
}
public void testLocaleChanges() {
resources.setSuggestedSitesResource(generateSites(3));
- SuggestedSites suggestedSites = new SuggestedSites(context);
+ SuggestedSites suggestedSites = new LocalSuggestedSites(context);
// Initial load with predefined locale
Cursor c = suggestedSites.get(DEFAULT_LIMIT, Locale.UK);
assertEquals(3, c.getCount());
c.close();
resources.setSuggestedSitesResource(generateSites(5));
@@ -424,17 +425,17 @@ public class TestSuggestedSites extends
// Init distribution with the mock file.
TestDistribution distribution = new TestDistribution(context);
distribution.setFileForLocale(Locale.getDefault(), distFile);
distribution.start();
// Init suggested sites with default values.
resources.setSuggestedSitesResource(generateSites(DEFAULT_COUNT));
SuggestedSites suggestedSites =
- new SuggestedSites(context, distribution, sitesFile);
+ new LocalSuggestedSites(context, distribution, sitesFile);
// The initial query will not contain the distribution sites
// yet. This will happen asynchronously once the distribution
// is installed.
Cursor c1 = null;
try {
c1 = suggestedSites.get(DEFAULT_LIMIT);
assertEquals(DEFAULT_COUNT, c1.getCount());