/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ package org.mozilla.goanna; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.json.JSONArray; import org.json.JSONObject; import org.mozilla.goanna.db.BrowserContract; import org.mozilla.goanna.db.SuggestedSites; import org.mozilla.goanna.distribution.Distribution; import org.mozilla.goanna.preferences.GoannaPreferences; import android.content.ContentResolver; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.SystemClock; import android.test.RenamingDelegatingContext; import android.test.mock.MockResources; public class TestSuggestedSites extends BrowserTestCase { private static class TestContext extends RenamingDelegatingContext { private static final String PREFIX = "TestSuggestedSites-"; private final Resources resources; private final Set usedPrefs; public TestContext(Context context) { super(context, PREFIX); resources = new TestResources(); usedPrefs = Collections.synchronizedSet(new HashSet()); } @Override public Resources getResources() { return resources; } @Override public SharedPreferences getSharedPreferences(String name, int mode) { usedPrefs.add(name); return super.getSharedPreferences(PREFIX + name, mode); } public void clearUsedPrefs() { for (String prefsName : usedPrefs) { getSharedPreferences(prefsName, 0).edit().clear().commit(); } usedPrefs.clear(); } } private static class TestResources extends MockResources { private String suggestedSites; @Override public InputStream openRawResource(int id) { if (id == R.raw.suggestedsites && suggestedSites != null) { return new ByteArrayInputStream(suggestedSites.getBytes()); } return null; } public void setSuggestedSitesResource(String suggestedSites) { this.suggestedSites = suggestedSites; } } private static class TestDistribution extends Distribution { private final Map filesPerLocale; public TestDistribution(Context context) { super(context); this.filesPerLocale = new HashMap(); } @Override public File getDistributionFile(String name) { for (Locale locale : filesPerLocale.keySet()) { if (name.startsWith("suggestedsites/locales/" + Locales.getLanguageTag(locale))) { return filesPerLocale.get(locale); } } return null; } @Override public boolean exists() { return true; } public void setFileForLocale(Locale locale, File file) { filesPerLocale.put(locale, file); } public void start() { doInit(); } } class TestObserver extends ContentObserver { private final Object changeLock; public TestObserver(Object changeLock) { super(null); this.changeLock = changeLock; } @Override public void onChange(boolean selfChange) { synchronized(changeLock) { changeLock.notifyAll(); } } } private static final int DEFAULT_LIMIT = 6; private static final String DIST_PREFIX = "dist"; private TestContext context; private TestResources resources; private List tempFiles; private String generateSites(int n) { return generateSites(n, ""); } private String generateSites(int n, String prefix) { return generateSites(n, false, prefix); } private String generateSites(int n, boolean includeIds, String prefix) { JSONArray sites = new JSONArray(); try { for (int i = 0; i < n; i++) { JSONObject site = new JSONObject(); if (includeIds) { site.put("trackingid", i); } site.put("url", prefix + "url" + i); site.put("title", prefix + "title" + i); site.put("imageurl", prefix + "imageUrl" + i); site.put("bgcolor", prefix + "bgColor" + i); sites.put(site); } } catch (Exception e) { return ""; } return sites.toString(); } private File createDistSuggestedSitesFile(int n) { FileOutputStream fos = null; try { File distFile = File.createTempFile("distrosites", ".json", context.getCacheDir()); fos = new FileOutputStream(distFile); fos.write(generateSites(n, DIST_PREFIX).getBytes()); return distFile; } catch (IOException e) { fail("Failed to create temp suggested sites file"); } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { // Ignore. } } } return null; } 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); assertEquals(expectedCount, c.getCount()); c.close(); } protected void setUp() { context = new TestContext(getApplicationContext()); resources = (TestResources) context.getResources(); tempFiles = new ArrayList(); } protected void tearDown() { context.clearUsedPrefs(); for (File f : tempFiles) { f.delete(); } } public void testCount() { // Empty array = empty cursor checkCursorCount(generateSites(0), 0); // 2 items = cursor with 2 rows checkCursorCount(generateSites(2), 2); // 10 items with lower limit = cursor respects limit checkCursorCount(generateSites(10), 3, 3); } public void testEmptyCursor() { // Null resource = empty cursor checkCursorCount(null, 0); // Empty string = empty cursor checkCursorCount("", 0); // Invalid json string = empty cursor checkCursorCount("{ broken: }", 0); } public void testCursorContent() { resources.setSuggestedSitesResource(generateSites(3)); Cursor c = new SuggestedSites(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); String title = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.TITLE)); assertEquals("title" + position, title); } c.close(); } public void testExcludeUrls() { resources.setSuggestedSitesResource(generateSites(6)); List excludedUrls = new ArrayList(3); excludedUrls.add("url1"); excludedUrls.add("url3"); excludedUrls.add("url5"); List includedUrls = new ArrayList(3); includedUrls.add("url0"); includedUrls.add("url2"); includedUrls.add("url4"); Cursor c = new SuggestedSites(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)); } c.close(); } public void testHiddenSites() { resources.setSuggestedSitesResource(generateSites(6)); List visibleUrls = new ArrayList(3); visibleUrls.add("url3"); visibleUrls.add("url4"); visibleUrls.add("url5"); List hiddenUrls = new ArrayList(3); hiddenUrls.add("url0"); hiddenUrls.add("url1"); hiddenUrls.add("url2"); // Add mocked hidden sites to SharedPreferences. StringBuilder hiddenUrlBuilder = new StringBuilder(); for (String s : hiddenUrls) { hiddenUrlBuilder.append(" "); hiddenUrlBuilder.append(Uri.encode(s)); } final String hiddenPref = hiddenUrlBuilder.toString(); GoannaSharedPrefs.forProfile(context).edit() .putString(SuggestedSites.PREF_SUGGESTED_SITES_HIDDEN, hiddenPref) .commit(); Cursor c = new SuggestedSites(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); assertEquals(3, c.getCount()); c.close(); // Disable suggested sites GoannaSharedPrefs.forApp(context).edit() .putBoolean(GoannaPreferences.PREFS_SUGGESTED_SITES, false) .commit(); c = new SuggestedSites(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); // 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)); } Cursor c = suggestedSites.get(DEFAULT_LIMIT); c.moveToPosition(-1); // We should have cached results after the get() call. while (c.moveToNext()) { String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL)); assertTrue(suggestedSites.contains(url)); assertEquals("imageUrl" + c.getPosition(), suggestedSites.getImageUrlForUrl(url)); assertEquals("bgColor" + c.getPosition(), suggestedSites.getBackgroundColorForUrl(url)); } c.close(); // No valid values for unknown URLs. assertFalse(suggestedSites.contains("foo")); assertNull(suggestedSites.getImageUrlForUrl("foo")); assertNull(suggestedSites.getBackgroundColorForUrl("foo")); } public void testTrackingIds() { final int count = 3; // Test suggested sites with IDs. resources.setSuggestedSitesResource(generateSites(count, true, "")); SuggestedSites suggestedSites = new SuggestedSites(context); Cursor c = suggestedSites.get(DEFAULT_LIMIT); assertEquals(count, c.getCount()); for (int i = 0; i < count; i++) { c.moveToNext(); String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL)); assertTrue(suggestedSites.contains(url)); assertEquals(i, suggestedSites.getTrackingIdForUrl(url)); } c.close(); // Test suggested sites where IDs are undefined. resources.setSuggestedSitesResource(generateSites(count, false, "")); suggestedSites = new SuggestedSites(context); c = suggestedSites.get(DEFAULT_LIMIT); assertEquals(count, c.getCount()); for (int i = 0; i < count; i++) { c.moveToNext(); String url = c.getString(c.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL)); assertTrue(suggestedSites.contains(url)); assertEquals(SuggestedSites.TRACKING_ID_NONE, suggestedSites.getTrackingIdForUrl(url)); } c.close(); } public void testLocaleChanges() { resources.setSuggestedSitesResource(generateSites(3)); SuggestedSites suggestedSites = new SuggestedSites(context); // Initial load with predefined locale Cursor c = suggestedSites.get(DEFAULT_LIMIT, Locale.UK); assertEquals(3, c.getCount()); c.close(); resources.setSuggestedSitesResource(generateSites(5)); // Second load with same locale should return same results // even though the contents of the resource have changed. c = suggestedSites.get(DEFAULT_LIMIT, Locale.UK); assertEquals(3, c.getCount()); c.close(); // Changing the locale forces the cached list to be refreshed. c = suggestedSites.get(DEFAULT_LIMIT, Locale.US); assertEquals(5, c.getCount()); c.close(); } public void testDistribution() { final int DIST_COUNT = 2; final int DEFAULT_COUNT = 3; File sitesFile = new File(context.getCacheDir(), "suggestedsites-" + SystemClock.uptimeMillis() + ".json"); tempFiles.add(sitesFile); assertFalse(sitesFile.exists()); File distFile = createDistSuggestedSitesFile(DIST_COUNT); tempFiles.add(distFile); assertTrue(distFile.exists()); // 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); Object changeLock = new Object(); // Watch for change notifications on suggested sites. ContentResolver cr = context.getContentResolver(); ContentObserver observer = new TestObserver(changeLock); cr.registerContentObserver(BrowserContract.SuggestedSites.CONTENT_URI, false, observer); // 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()); } finally { if (c1 != null) { c1.close(); } } synchronized(changeLock) { try { changeLock.wait(5000); } catch (InterruptedException ie) { fail("No change notification after fetching distribution file"); } } // Target file should exist after distribution is deployed. assertTrue(sitesFile.exists()); cr.unregisterContentObserver(observer); Cursor c2 = null; try { c2 = suggestedSites.get(DEFAULT_LIMIT); // The next query should contain the distribution contents. assertEquals(DIST_COUNT + DEFAULT_COUNT, c2.getCount()); // The first items should be from the distribution for (int i = 0; i < DIST_COUNT; i++) { c2.moveToPosition(i); String url = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL)); assertEquals(DIST_PREFIX + "url" + i, url); String title = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.TITLE)); assertEquals(DIST_PREFIX + "title" + i, title); } // The remaining items should be the default ones for (int i = 0; i < c2.getCount() - DIST_COUNT; i++) { c2.moveToPosition(i + DIST_COUNT); String url = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.URL)); assertEquals("url" + i, url); String title = c2.getString(c2.getColumnIndexOrThrow(BrowserContract.SuggestedSites.TITLE)); assertEquals("title" + i, title); } } finally { if (c2 != null) { c2.close(); } } } }