org.mozilla.gecko.tests.testDistribution.java Source code

Java tutorial

Introduction

Here is the source code for org.mozilla.gecko.tests.testDistribution.java

Source

/* 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.tests;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.util.Locale;
import java.util.jar.JarInputStream;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import org.mozilla.gecko.Actions;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.SuggestedSites;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.distribution.ReferrerDescriptor;
import org.mozilla.gecko.distribution.ReferrerReceiver;
import org.mozilla.gecko.util.ThreadUtils;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;

/**
 * Tests distribution customization.
 * mock-package.zip should contain the following directory structure:
 *
 *   distribution/
 *     preferences.json
 *     bookmarks.json
 *     searchplugins/
 *       common/
 *         engine.xml
 *     suggestedsites/
 *       locales/
 *         en-US/
 *           suggestedsites.json
 *     extensions/
 *       distribution.test@mozilla.org.xpi
 */
public class testDistribution extends ContentProviderTest {
    private static final String CLASS_REFERRER_RECEIVER = "org.mozilla.gecko.distribution.ReferrerReceiver";
    private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER";
    private static final int WAIT_TIMEOUT_MSEC = 10000;
    public static final String LOGTAG = "GeckoTestDistribution";

    public static class TestableDistribution extends Distribution {
        @Override
        protected JarInputStream fetchDistribution(URI uri, HttpURLConnection connection) throws IOException {
            Log.i(LOGTAG, "Not downloading: this is a test.");
            return null;
        }

        public TestableDistribution(Context context) {
            super(context);
        }

        public void go() {
            doInit();
        }

        public static void clearReferrerDescriptorForTesting() {
            referrer = null;
        }

        public static ReferrerDescriptor getReferrerDescriptorForTesting() {
            return referrer;
        }
    }

    private static final String MOCK_PACKAGE = "mock-package.zip";
    private static final int PREF_REQUEST_ID = 0x7357;

    private Activity mActivity;

    /**
     * This is a hack.
     *
     * Startup results in us writing prefs -- we fetch the Distribution, which
     * caches its state. Our tests try to wipe those prefs, but apparently
     * sometimes race with startup, which leads to us not getting one of our
     * expected messages. The test fails.
     *
     * This hack waits for any existing background tasks -- such as the one that
     * writes prefs -- to finish before we begin the test.
     */
    private void waitForBackgroundHappiness() {
        final Object signal = new Object();
        final Runnable done = new Runnable() {
            @Override
            public void run() {
                synchronized (signal) {
                    signal.notify();
                }
            }
        };
        synchronized (signal) {
            ThreadUtils.postToBackgroundThread(done);
            try {
                signal.wait();
            } catch (InterruptedException e) {
                mAsserter.ok(false, "InterruptedException waiting on background thread.", e.toString());
            }
        }
        mAsserter.dumpLog("Background task completed. Proceeding.");
    }

    public void testDistribution() throws Exception {
        mActivity = getActivity();

        String mockPackagePath = getMockPackagePath();

        // Wait for any startup-related background distribution shenanigans to
        // finish. This reduces the chance of us racing with startup pref writes.
        waitForBackgroundHappiness();

        // Pre-clear distribution pref, override suggested sites and run tiles tests.
        clearDistributionPref();
        Distribution dist = initDistribution(mockPackagePath);
        SuggestedSites suggestedSites = new SuggestedSites(mActivity, dist);
        GeckoProfile.get(mActivity).getDB().setSuggestedSites(suggestedSites);

        // Test tiles uploading for an en-US OS locale with no app locale.
        setOSLocale(Locale.US);
        checkTilesReporting("en-US");

        // Test tiles uploading for an es-MX OS locale with no app locale.
        setOSLocale(new Locale("es", "MX"));
        checkTilesReporting("es-MX");

        // Pre-clear distribution pref, run basic preferences and en-US localized preferences Tests
        clearDistributionPref();
        setTestLocale("en-US");
        initDistribution(mockPackagePath);
        checkPreferences();
        checkLocalizedPreferences("en-US");
        checkSearchPlugin();
        checkAddon();

        // Pre-clear distribution pref, and run es-MX localized preferences Test
        clearDistributionPref();
        setTestLocale("es-MX");
        initDistribution(mockPackagePath);
        checkLocalizedPreferences("es-MX");

        // Test the (stubbed) download interaction.
        setTestLocale("en-US");
        clearDistributionPref();
        doTestValidReferrerIntent();

        clearDistributionPref();
        doTestInvalidReferrerIntent();
    }

    private void setOSLocale(Locale locale) {
        Locale.setDefault(locale);
        BrowserLocaleManager.storeAndNotifyOSLocale(GeckoSharedPrefs.forProfile(mActivity), locale);
    }

    private abstract class ExpectNoDistributionCallback implements Distribution.ReadyCallback {
        @Override
        public void distributionFound(final Distribution distribution) {
            mAsserter.ok(false, "No distributionFound.", "Wasn't expecting a distribution!");
            synchronized (distribution) {
                distribution.notifyAll();
            }
        }

        @Override
        public void distributionArrivedLate(final Distribution distribution) {
            mAsserter.ok(false, "No distributionArrivedLate.", "Wasn't expecting a late distribution!");
        }
    }

    private void doReferrerTest(String ref, final TestableDistribution distribution,
            final Distribution.ReadyCallback distributionReady) throws InterruptedException {
        final Intent intent = new Intent(ACTION_INSTALL_REFERRER);
        intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER);
        intent.putExtra("referrer", ref);

        final BroadcastReceiver receiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                Log.i(LOGTAG, "Test received " + intent.getAction());

                ThreadUtils.postToBackgroundThread(new Runnable() {
                    @Override
                    public void run() {
                        distribution.addOnDistributionReadyCallback(distributionReady);
                        distribution.go();
                    }
                });
            }
        };

        IntentFilter intentFilter = new IntentFilter(ReferrerReceiver.ACTION_REFERRER_RECEIVED);
        final LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(mActivity);
        localBroadcastManager.registerReceiver(receiver, intentFilter);

        Log.i(LOGTAG, "Broadcasting referrer intent.");
        try {
            mActivity.sendBroadcast(intent, null);
            synchronized (distribution) {
                distribution.wait(WAIT_TIMEOUT_MSEC);
            }
        } finally {
            localBroadcastManager.unregisterReceiver(receiver);
        }
    }

    public void doTestValidReferrerIntent() throws Exception {
        // Equivalent to
        // am broadcast -a com.android.vending.INSTALL_REFERRER \
        //              -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \
        //              --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution"
        final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=distribution";
        final TestableDistribution distribution = new TestableDistribution(mActivity);
        final Distribution.ReadyCallback distributionReady = new ExpectNoDistributionCallback() {
            @Override
            public void distributionNotFound() {
                Log.i(LOGTAG, "Test told distribution processing is done.");
                mAsserter.ok(!distribution.exists(), "Not processed.", "No download because we're offline.");
                ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
                mAsserter.dumpLog("Referrer was " + referrerValue);
                mAsserter.is(referrerValue.content, "testcontent", "Referrer content");
                mAsserter.is(referrerValue.medium, "testmedium", "Referrer medium");
                mAsserter.is(referrerValue.campaign, "distribution", "Referrer campaign");
                synchronized (distribution) {
                    distribution.notifyAll();
                }
            }
        };

        doReferrerTest(ref, distribution, distributionReady);
    }

    /**
     * Test processing if the campaign isn't "distribution". The intent shouldn't
     * result in a download, and won't be saved as the temporary referrer,
     * even if we *do* include it in a Campaign:Set message.
     */
    public void doTestInvalidReferrerIntent() throws Exception {
        // Equivalent to
        // am broadcast -a com.android.vending.INSTALL_REFERRER \
        //              -n org.mozilla.fennec/org.mozilla.gecko.distribution.ReferrerReceiver \
        //              --es "referrer" "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname"
        final String ref = "utm_source=mozilla&utm_medium=testmedium&utm_term=testterm&utm_content=testcontent&utm_campaign=testname";
        final TestableDistribution distribution = new TestableDistribution(mActivity);
        final Distribution.ReadyCallback distributionReady = new ExpectNoDistributionCallback() {
            @Override
            public void distributionNotFound() {
                mAsserter.ok(!distribution.exists(), "Not processed.", "No download because campaign was wrong.");
                ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
                mAsserter.is(referrerValue, null, "No referrer.");
                synchronized (distribution) {
                    distribution.notifyAll();
                }
            }
        };

        doReferrerTest(ref, distribution, distributionReady);
    }

    // Initialize the distribution from the mock package.
    private Distribution initDistribution(String aPackagePath) {
        // Call Distribution.init with the mock package.
        Actions.EventExpecter distributionSetExpecter = mActions.expectGeckoEvent("Distribution:Set:OK");
        Distribution dist = Distribution.init(mActivity, aPackagePath, "prefs-" + System.currentTimeMillis());
        distributionSetExpecter.blockForEvent();
        distributionSetExpecter.unregisterListener();
        return dist;
    }

    // Test distribution and preferences values stored in preferences.json
    private void checkPreferences() {
        String prefID = "distribution.id";
        String prefAbout = "distribution.about";
        String prefVersion = "distribution.version";
        String prefTestBoolean = "distribution.test.boolean";
        String prefTestString = "distribution.test.string";
        String prefTestInt = "distribution.test.int";

        try {
            final String[] prefNames = { prefID, prefAbout, prefVersion, prefTestBoolean, prefTestString,
                    prefTestInt };

            final JSONArray preferences = getPrefs(prefNames);
            for (int i = 0; i < preferences.length(); i++) {
                JSONObject pref = (JSONObject) preferences.get(i);
                String name = pref.getString("name");

                if (name.equals(prefID)) {
                    mAsserter.is(pref.getString("value"), "test-partner", "check " + prefID);
                } else if (name.equals(prefAbout)) {
                    mAsserter.is(pref.getString("value"), "Test Partner", "check " + prefAbout);
                } else if (name.equals(prefVersion)) {
                    mAsserter.is(pref.getInt("value"), 1, "check " + prefVersion);
                } else if (name.equals(prefTestBoolean)) {
                    mAsserter.is(pref.getBoolean("value"), true, "check " + prefTestBoolean);
                } else if (name.equals(prefTestString)) {
                    mAsserter.is(pref.getString("value"), "test", "check " + prefTestString);
                } else if (name.equals(prefTestInt)) {
                    mAsserter.is(pref.getInt("value"), 5, "check " + prefTestInt);
                }
            }

        } catch (JSONException e) {
            mAsserter.ok(false, "exception getting preferences", e.toString());
        }
    }

    private void checkSearchPlugin() {
        Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("SearchEngines:Data");
        mActions.sendGeckoEvent("SearchEngines:GetVisible", null);

        try {
            JSONObject data = new JSONObject(eventExpecter.blockForEventData());
            eventExpecter.unregisterListener();
            JSONArray searchEngines = data.getJSONArray("searchEngines");
            boolean foundEngine = false;
            for (int i = 0; i < searchEngines.length(); i++) {
                JSONObject engine = (JSONObject) searchEngines.get(i);
                String name = engine.getString("name");
                if (name.equals("Test search engine")) {
                    foundEngine = true;
                    break;
                }
            }
            mAsserter.ok(foundEngine, "check search plugin", "found test search plugin");
        } catch (JSONException e) {
            mAsserter.ok(false, "exception getting search plugins", e.toString());
        }
    }

    private void checkAddon() {
        try {
            final String[] prefNames = { "distribution.test.addonEnabled" };
            final JSONArray preferences = getPrefs(prefNames);
            final JSONObject pref = (JSONObject) preferences.get(0);
            mAsserter.is(pref.getBoolean("value"), true, "check distribution add-on is enabled");
        } catch (JSONException e) {
            mAsserter.ok(false, "exception getting preferences", e.toString());
        }
    }

    private JSONArray getPrefs(String[] prefNames) throws JSONException {
        Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data");
        mActions.sendPreferencesGetEvent(PREF_REQUEST_ID, prefNames);

        JSONObject data = null;
        int requestId = -1;

        // Wait until we get the correct "Preferences:Data" event
        while (requestId != PREF_REQUEST_ID) {
            data = new JSONObject(eventExpecter.blockForEventData());
            requestId = data.getInt("requestId");
        }
        eventExpecter.unregisterListener();

        return data.getJSONArray("preferences");
    }

    // Sets the distribution locale preference for the test.
    private void setTestLocale(String locale) {
        BrowserLocaleManager.getInstance().setSelectedLocale(mActivity, locale);
    }

    // Test localized distribution and preferences values stored in preferences.json
    private void checkLocalizedPreferences(String aLocale) {
        String prefAbout = "distribution.about";
        String prefLocalizeable = "distribution.test.localizeable";
        String prefLocalizeableOverride = "distribution.test.localizeable-override";

        try {
            final String[] prefNames = { prefAbout, prefLocalizeable, prefLocalizeableOverride };

            Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data");
            mActions.sendPreferencesGetEvent(PREF_REQUEST_ID, prefNames);

            JSONObject data = null;
            int requestId = -1;

            // Wait until we get the correct "Preferences:Data" event
            while (requestId != PREF_REQUEST_ID) {
                data = new JSONObject(eventExpecter.blockForEventData());
                requestId = data.getInt("requestId");
            }
            eventExpecter.unregisterListener();

            JSONArray preferences = data.getJSONArray("preferences");
            for (int i = 0; i < preferences.length(); i++) {
                JSONObject pref = (JSONObject) preferences.get(i);
                String name = pref.getString("name");

                if (name.equals(prefAbout)) {
                    if (aLocale.equals("en-US")) {
                        mAsserter.is(pref.getString("value"), "Test Partner", "check " + prefAbout);
                    } else if (aLocale.equals("es-MX")) {
                        mAsserter.is(pref.getString("value"), "Afiliado de Prueba", "check " + prefAbout);
                    }
                } else if (name.equals(prefLocalizeable)) {
                    if (aLocale.equals("en-US")) {
                        mAsserter.is(pref.getString("value"), "http://test.org/en-US/en-US/",
                                "check " + prefLocalizeable);
                    } else if (aLocale.equals("es-MX")) {
                        mAsserter.is(pref.getString("value"), "http://test.org/es-MX/es-MX/",
                                "check " + prefLocalizeable);
                    }
                } else if (name.equals(prefLocalizeableOverride)) {
                    if (aLocale.equals("en-US")) {
                        mAsserter.is(pref.getString("value"), "http://cheese.com",
                                "check " + prefLocalizeableOverride);
                    } else if (aLocale.equals("es-MX")) {
                        mAsserter.is(pref.getString("value"), "http://test.org/es-MX/",
                                "check " + prefLocalizeableOverride);
                    }
                }
            }

        } catch (JSONException e) {
            mAsserter.ok(false, "exception getting preferences", e.toString());
        }
    }

    // Copies the mock package to the data directory and returns the file path to it.
    private String getMockPackagePath() {
        String mockPackagePath = "";

        try {
            InputStream inStream = getAsset(MOCK_PACKAGE);
            File dataDir = new File(mActivity.getApplicationInfo().dataDir);
            File outFile = new File(dataDir, MOCK_PACKAGE);

            OutputStream outStream = new FileOutputStream(outFile);
            int b;
            while ((b = inStream.read()) != -1) {
                outStream.write(b);
            }
            inStream.close();
            outStream.close();

            mockPackagePath = outFile.getPath();

        } catch (Exception e) {
            mAsserter.ok(false, "exception copying mock distribution package to data directory", e.toString());
        }

        return mockPackagePath;
    }

    /**
     * Clears the distribution pref to return distribution state to STATE_UNKNOWN,
     * and wipes the in-memory referrer pigeonhole.
     */
    private void clearDistributionPref() {
        mAsserter.dumpLog("Clearing distribution pref.");
        SharedPreferences settings = mActivity.getSharedPreferences("GeckoApp", Activity.MODE_PRIVATE);
        String keyName = mActivity.getPackageName() + ".distribution_state";
        settings.edit().remove(keyName).commit();
        TestableDistribution.clearReferrerDescriptorForTesting();
    }

    public void checkTilesReporting(String localeCode) throws JSONException {
        // Slight hack: Force top sites grid to reload.
        inputAndLoadUrl(mStringHelper.ABOUT_BLANK_URL);
        inputAndLoadUrl(mStringHelper.ABOUT_HOME_URL);

        // Click the first tracking tile and verify the posted data.
        JSONObject response = clickTrackingTile(mStringHelper.DISTRIBUTION1_LABEL);
        mAsserter.is(response.getInt("click"), 0, "JSON click index matched");
        mAsserter.is(response.getString("locale"), localeCode, "JSON locale code matched");
        mAsserter.is(response.getString("tiles"),
                "[{\"id\":123},{\"id\":456},{\"id\":632},{\"id\":629},{\"id\":630},{\"id\":631}]",
                "JSON tiles data matched");

        inputAndLoadUrl(mStringHelper.ABOUT_HOME_URL);

        // Pin the second tracking tile.
        pinTopSite(mStringHelper.DISTRIBUTION2_LABEL);

        // Click the second tracking tile and verify the posted data.
        response = clickTrackingTile(mStringHelper.DISTRIBUTION2_LABEL);
        mAsserter.is(response.getInt("click"), 1, "JSON click index matched");
        mAsserter.is(response.getString("tiles"),
                "[{\"id\":123},{\"id\":456,\"pin\":true},{\"id\":632},{\"id\":629},{\"id\":630},{\"id\":631}]",
                "JSON tiles data matched");

        inputAndLoadUrl(mStringHelper.ABOUT_HOME_URL);

        // Unpin the second tracking tile.
        unpinTopSite(mStringHelper.DISTRIBUTION2_LABEL);
    }

    private JSONObject clickTrackingTile(String text) throws JSONException {
        boolean tileFound = waitForText(text);
        mAsserter.ok(tileFound, "Found tile: " + text, null);

        Actions.EventExpecter loadExpecter = mActions.expectGeckoEvent("Robocop:TilesResponse");
        mSolo.clickOnText(text);
        String data = loadExpecter.blockForEventData();
        JSONObject dataJSON = new JSONObject(data);
        String response = dataJSON.getString("response");
        return new JSONObject(response);
    }

    @Override
    public void setUp() throws Exception {
        // TODO: Set up the content provider after setting the distribution.
        super.setUp(sBrowserProviderCallable, BrowserContract.AUTHORITY, "browser.db");
    }

    private void delete(File file) throws Exception {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            for (File f : files) {
                delete(f);
            }
        }
        mAsserter.ok(file.delete(), "clean up distribution files", "deleted " + file.getPath());
    }

    @Override
    public void tearDown() throws Exception {
        File dataDir = new File(mActivity.getApplicationInfo().dataDir);

        // Delete mock package from data directory.
        File mockPackage = new File(dataDir, MOCK_PACKAGE);
        mAsserter.ok(mockPackage.delete(), "clean up mock package", "deleted " + mockPackage.getPath());

        // Recursively delete distribution files that Distribution.init copied to data directory.
        File distDir = new File(dataDir, "distribution");
        delete(distDir);

        clearDistributionPref();

        super.tearDown();
    }
}