зеркало из https://github.com/mozilla/gecko-dev.git
merge fx-team to mozilla-central a=merge
This commit is contained in:
Коммит
c3b8d8d3b2
|
@ -9,6 +9,7 @@ support-files =
|
||||||
doc_media-node-creation.html
|
doc_media-node-creation.html
|
||||||
doc_destroy-nodes.html
|
doc_destroy-nodes.html
|
||||||
doc_connect-toggle.html
|
doc_connect-toggle.html
|
||||||
|
doc_connect-param.html
|
||||||
440hz_sine.ogg
|
440hz_sine.ogg
|
||||||
head.js
|
head.js
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ support-files =
|
||||||
[browser_audionode-actor-is-source.js]
|
[browser_audionode-actor-is-source.js]
|
||||||
[browser_webaudio-actor-simple.js]
|
[browser_webaudio-actor-simple.js]
|
||||||
[browser_webaudio-actor-destroy-node.js]
|
[browser_webaudio-actor-destroy-node.js]
|
||||||
|
[browser_webaudio-actor-connect-param.js]
|
||||||
|
|
||||||
[browser_wa_destroy-node-01.js]
|
[browser_wa_destroy-node-01.js]
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the `connect-param` event on the web audio actor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function spawnTest () {
|
||||||
|
let [target, debuggee, front] = yield initBackend(CONNECT_PARAM_URL);
|
||||||
|
let [_, _, [destNode, carrierNode, modNode, gainNode], _, connectParam] = yield Promise.all([
|
||||||
|
front.setup({ reload: true }),
|
||||||
|
once(front, "start-context"),
|
||||||
|
getN(front, "create-node", 4),
|
||||||
|
get2(front, "connect-node"),
|
||||||
|
once(front, "connect-param")
|
||||||
|
]);
|
||||||
|
|
||||||
|
info(connectParam);
|
||||||
|
|
||||||
|
is(connectParam.source.actorID, modNode.actorID, "`connect-param` has correct actor for `source`");
|
||||||
|
is(connectParam.dest.actorID, gainNode.actorID, "`connect-param` has correct actor for `dest`");
|
||||||
|
is(connectParam.param, "gain", "`connect-param` has correct parameter name for `param`");
|
||||||
|
|
||||||
|
yield removeTab(target.tab);
|
||||||
|
finish();
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
<!-- Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ -->
|
||||||
|
<!doctype html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<title>Web Audio Editor test page</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<script type="text/javascript;version=1.8">
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
let ctx = new AudioContext();
|
||||||
|
let carrier = ctx.createOscillator();
|
||||||
|
let modulator = ctx.createOscillator();
|
||||||
|
let gain = ctx.createGain();
|
||||||
|
carrier.connect(gain);
|
||||||
|
gain.connect(ctx.destination);
|
||||||
|
modulator.connect(gain.gain);
|
||||||
|
modulator.start(0);
|
||||||
|
carrier.start(0);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -28,6 +28,7 @@ const MEDIA_NODES_URL = EXAMPLE_URL + "doc_media-node-creation.html";
|
||||||
const BUFFER_AND_ARRAY_URL = EXAMPLE_URL + "doc_buffer-and-array.html";
|
const BUFFER_AND_ARRAY_URL = EXAMPLE_URL + "doc_buffer-and-array.html";
|
||||||
const DESTROY_NODES_URL = EXAMPLE_URL + "doc_destroy-nodes.html";
|
const DESTROY_NODES_URL = EXAMPLE_URL + "doc_destroy-nodes.html";
|
||||||
const CONNECT_TOGGLE_URL = EXAMPLE_URL + "doc_connect-toggle.html";
|
const CONNECT_TOGGLE_URL = EXAMPLE_URL + "doc_connect-toggle.html";
|
||||||
|
const CONNECT_PARAM_URL = EXAMPLE_URL + "doc_connect-param.html";
|
||||||
|
|
||||||
// All tests are asynchronous.
|
// All tests are asynchronous.
|
||||||
waitForExplicitFinish();
|
waitForExplicitFinish();
|
||||||
|
|
|
@ -290,7 +290,9 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<receiver android:name="org.mozilla.gecko.ReferrerReceiver" android:exported="true">
|
<!-- Catch install referrer so we can do post-install work. -->
|
||||||
|
<receiver android:name="org.mozilla.gecko.distribution.ReferrerReceiver"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="com.android.vending.INSTALL_REFERRER" />
|
<action android:name="com.android.vending.INSTALL_REFERRER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -200,6 +200,13 @@ public final class GeckoProfile {
|
||||||
getGuestDir(context).mkdir();
|
getGuestDir(context).mkdir();
|
||||||
GeckoProfile profile = getGuestProfile(context);
|
GeckoProfile profile = getGuestProfile(context);
|
||||||
profile.lock();
|
profile.lock();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Now do the things that createProfileDirectory normally does --
|
||||||
|
* right now that's kicking off DB init.
|
||||||
|
*/
|
||||||
|
profile.enqueueInitialization();
|
||||||
|
|
||||||
return profile;
|
return profile;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Log.e(LOGTAG, "Error creating guest profile", ex);
|
Log.e(LOGTAG, "Error creating guest profile", ex);
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
/* -*- 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;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.net.URLDecoder;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
public class ReferrerReceiver
|
|
||||||
extends BroadcastReceiver
|
|
||||||
{
|
|
||||||
private static final String LOGTAG = "GeckoReferrerReceiver";
|
|
||||||
|
|
||||||
public static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER";
|
|
||||||
public static final String UTM_SOURCE = "mozilla";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (ACTION_INSTALL_REFERRER.equals(intent.getAction())) {
|
|
||||||
String referrer = intent.getStringExtra("referrer");
|
|
||||||
if (referrer == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
HashMap<String, String> values = new HashMap<String, String>();
|
|
||||||
try {
|
|
||||||
String referrers[] = referrer.split("&");
|
|
||||||
for (String referrerValue : referrers) {
|
|
||||||
String keyValue[] = referrerValue.split("=");
|
|
||||||
values.put(URLDecoder.decode(keyValue[0]), URLDecoder.decode(keyValue[1]));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
}
|
|
||||||
|
|
||||||
String source = values.get("utm_source");
|
|
||||||
String campaign = values.get("utm_campaign");
|
|
||||||
|
|
||||||
if (source != null && UTM_SOURCE.equals(source) && campaign != null) {
|
|
||||||
try {
|
|
||||||
JSONObject data = new JSONObject();
|
|
||||||
data.put("id", "playstore");
|
|
||||||
data.put("version", campaign);
|
|
||||||
|
|
||||||
// Try to make sure the prefs are written as a group
|
|
||||||
GeckoEvent event = GeckoEvent.createBroadcastEvent("Campaign:Set", data.toString());
|
|
||||||
GeckoAppShell.sendEventToGecko(event);
|
|
||||||
} catch (JSONException e) {
|
|
||||||
Log.e(LOGTAG, "Error setting distribution", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,12 +5,19 @@
|
||||||
|
|
||||||
package org.mozilla.gecko.distribution;
|
package org.mozilla.gecko.distribution;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.ProtocolException;
|
||||||
|
import java.net.SocketException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -19,34 +26,95 @@ import java.util.Map;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
import java.util.jar.JarEntry;
|
||||||
|
import java.util.jar.JarInputStream;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipFile;
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
|
||||||
|
import org.apache.http.protocol.HTTP;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.mozilla.gecko.GeckoAppShell;
|
import org.mozilla.gecko.GeckoAppShell;
|
||||||
import org.mozilla.gecko.GeckoEvent;
|
import org.mozilla.gecko.GeckoEvent;
|
||||||
import org.mozilla.gecko.GeckoSharedPrefs;
|
import org.mozilla.gecko.GeckoSharedPrefs;
|
||||||
|
import org.mozilla.gecko.Telemetry;
|
||||||
import org.mozilla.gecko.mozglue.RobocopTarget;
|
import org.mozilla.gecko.mozglue.RobocopTarget;
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
import org.mozilla.gecko.util.ThreadUtils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.SystemClock;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles distribution file loading and fetching,
|
* Handles distribution file loading and fetching,
|
||||||
* and the corresponding hand-offs to Gecko.
|
* and the corresponding hand-offs to Gecko.
|
||||||
*/
|
*/
|
||||||
public final class Distribution {
|
@RobocopTarget
|
||||||
|
public class Distribution {
|
||||||
private static final String LOGTAG = "GeckoDistribution";
|
private static final String LOGTAG = "GeckoDistribution";
|
||||||
|
|
||||||
private static final int STATE_UNKNOWN = 0;
|
private static final int STATE_UNKNOWN = 0;
|
||||||
private static final int STATE_NONE = 1;
|
private static final int STATE_NONE = 1;
|
||||||
private static final int STATE_SET = 2;
|
private static final int STATE_SET = 2;
|
||||||
|
|
||||||
|
private static final String FETCH_PROTOCOL = "https";
|
||||||
|
private static final String FETCH_HOSTNAME = "distro-download.cdn.mozilla.net";
|
||||||
|
private static final String FETCH_PATH = "/android/1/";
|
||||||
|
private static final String FETCH_EXTENSION = ".jar";
|
||||||
|
|
||||||
|
private static final String EXPECTED_CONTENT_TYPE = "application/java-archive";
|
||||||
|
|
||||||
|
private static final String DISTRIBUTION_PATH = "distribution/";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Telemetry constants.
|
||||||
|
*/
|
||||||
|
private static final String HISTOGRAM_REFERRER_INVALID = "FENNEC_DISTRIBUTION_REFERRER_INVALID";
|
||||||
|
private static final String HISTOGRAM_DOWNLOAD_TIME_MS = "FENNEC_DISTRIBUTION_DOWNLOAD_TIME_MS";
|
||||||
|
private static final String HISTOGRAM_CODE_CATEGORY = "FENNEC_DISTRIBUTION_CODE_CATEGORY";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success/failure codes. Don't exceed the maximum listed in Histograms.json.
|
||||||
|
*/
|
||||||
|
private static final int CODE_CATEGORY_STATUS_OUT_OF_RANGE = 0;
|
||||||
|
// HTTP status 'codes' run from 1 to 5.
|
||||||
|
private static final int CODE_CATEGORY_OFFLINE = 6;
|
||||||
|
private static final int CODE_CATEGORY_FETCH_EXCEPTION = 7;
|
||||||
|
|
||||||
|
// It's a post-fetch exception if we were able to download, but not
|
||||||
|
// able to extract.
|
||||||
|
private static final int CODE_CATEGORY_POST_FETCH_EXCEPTION = 8;
|
||||||
|
private static final int CODE_CATEGORY_POST_FETCH_SECURITY_EXCEPTION = 9;
|
||||||
|
|
||||||
|
// It's a malformed distribution if we could extract, but couldn't
|
||||||
|
// process the contents.
|
||||||
|
private static final int CODE_CATEGORY_MALFORMED_DISTRIBUTION = 10;
|
||||||
|
|
||||||
|
// Specific fetch errors.
|
||||||
|
private static final int CODE_CATEGORY_FETCH_SOCKET_ERROR = 11;
|
||||||
|
private static final int CODE_CATEGORY_FETCH_SSL_ERROR = 12;
|
||||||
|
private static final int CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE = 13;
|
||||||
|
private static final int CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE = 14;
|
||||||
|
|
||||||
|
// Corresponds to the high value in Histograms.json.
|
||||||
|
private static final long MAX_DOWNLOAD_TIME_MSEC = 40000; // 40 seconds.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as a drop-off point for ReferrerReceiver. Checked when we process
|
||||||
|
* first-run distribution.
|
||||||
|
*
|
||||||
|
* This is `protected` so that test code can clear it between runs.
|
||||||
|
*/
|
||||||
|
@RobocopTarget
|
||||||
|
protected static volatile ReferrerDescriptor referrer;
|
||||||
|
|
||||||
private static Distribution instance;
|
private static Distribution instance;
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
@ -70,6 +138,7 @@ public final class Distribution {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RobocopTarget
|
||||||
public static class DistributionDescriptor {
|
public static class DistributionDescriptor {
|
||||||
public final boolean valid;
|
public final boolean valid;
|
||||||
public final String id;
|
public final String id;
|
||||||
|
@ -140,6 +209,7 @@ public final class Distribution {
|
||||||
* Use <code>Context.getPackageResourcePath</code> to find an implicit
|
* Use <code>Context.getPackageResourcePath</code> to find an implicit
|
||||||
* package path. Reuses the existing Distribution if one exists.
|
* package path. Reuses the existing Distribution if one exists.
|
||||||
*/
|
*/
|
||||||
|
@RobocopTarget
|
||||||
public static void init(final Context context) {
|
public static void init(final Context context) {
|
||||||
Distribution.init(Distribution.getInstance(context));
|
Distribution.init(Distribution.getInstance(context));
|
||||||
}
|
}
|
||||||
|
@ -166,6 +236,17 @@ public final class Distribution {
|
||||||
this(context, context.getPackageResourcePath(), null);
|
this(context, context.getPackageResourcePath(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called by ReferrerReceiver when we receive a post-install
|
||||||
|
* notification from Google Play.
|
||||||
|
*
|
||||||
|
* @param ref a parsed referrer value from the store-supplied intent.
|
||||||
|
*/
|
||||||
|
public static void onReceivedReferrer(ReferrerDescriptor ref) {
|
||||||
|
// Track the referrer object for distribution handling.
|
||||||
|
referrer = ref;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to grab a file in the distribution directory.
|
* Helper to grab a file in the distribution directory.
|
||||||
*
|
*
|
||||||
|
@ -214,9 +295,11 @@ public final class Distribution {
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(LOGTAG, "Error getting distribution descriptor file.", e);
|
Log.e(LOGTAG, "Error getting distribution descriptor file.", e);
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
|
||||||
return null;
|
return null;
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
Log.e(LOGTAG, "Error parsing preferences.json", e);
|
Log.e(LOGTAG, "Error parsing preferences.json", e);
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,12 +315,14 @@ public final class Distribution {
|
||||||
return new JSONArray(getFileContents(bookmarks));
|
return new JSONArray(getFileContents(bookmarks));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(LOGTAG, "Error getting bookmarks", e);
|
Log.e(LOGTAG, "Error getting bookmarks", e);
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
|
||||||
|
return null;
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
Log.e(LOGTAG, "Error parsing bookmarks.json", e);
|
Log.e(LOGTAG, "Error parsing bookmarks.json", e);
|
||||||
}
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Don't call from the main thread.
|
* Don't call from the main thread.
|
||||||
|
@ -245,9 +330,12 @@ public final class Distribution {
|
||||||
* Postcondition: if this returns true, distributionDir will have been
|
* Postcondition: if this returns true, distributionDir will have been
|
||||||
* set and populated.
|
* set and populated.
|
||||||
*
|
*
|
||||||
|
* This method is *only* protected for use from testDistribution.
|
||||||
|
*
|
||||||
* @return true if we've set a distribution.
|
* @return true if we've set a distribution.
|
||||||
*/
|
*/
|
||||||
private boolean doInit() {
|
@RobocopTarget
|
||||||
|
protected boolean doInit() {
|
||||||
ThreadUtils.assertNotOnUiThread();
|
ThreadUtils.assertNotOnUiThread();
|
||||||
|
|
||||||
// Bail if we've already tried to initialize the distribution, and
|
// Bail if we've already tried to initialize the distribution, and
|
||||||
|
@ -274,8 +362,9 @@ public final class Distribution {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We try the APK, then the system directory.
|
// We try the install intent, then the APK, then the system directory.
|
||||||
final boolean distributionSet =
|
final boolean distributionSet =
|
||||||
|
checkIntentDistribution() ||
|
||||||
checkAPKDistribution() ||
|
checkAPKDistribution() ||
|
||||||
checkSystemDistribution();
|
checkSystemDistribution();
|
||||||
|
|
||||||
|
@ -286,6 +375,153 @@ public final class Distribution {
|
||||||
return distributionSet;
|
return distributionSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If applicable, download and select the distribution specified in
|
||||||
|
* the referrer intent.
|
||||||
|
*
|
||||||
|
* @return true if a referrer-supplied distribution was selected.
|
||||||
|
*/
|
||||||
|
private boolean checkIntentDistribution() {
|
||||||
|
if (referrer == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
URI uri = getReferredDistribution(referrer);
|
||||||
|
if (uri == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long start = SystemClock.uptimeMillis();
|
||||||
|
Log.v(LOGTAG, "Downloading referred distribution: " + uri);
|
||||||
|
|
||||||
|
try {
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
|
||||||
|
|
||||||
|
connection.setRequestProperty(HTTP.USER_AGENT, GeckoAppShell.getGeckoInterface().getDefaultUAString());
|
||||||
|
connection.setRequestProperty("Accept", EXPECTED_CONTENT_TYPE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final JarInputStream distro;
|
||||||
|
try {
|
||||||
|
distro = fetchDistribution(uri, connection);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(LOGTAG, "Error fetching distribution from network.", e);
|
||||||
|
recordFetchTelemetry(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long end = SystemClock.uptimeMillis();
|
||||||
|
final long duration = end - start;
|
||||||
|
Log.d(LOGTAG, "Distro fetch took " + duration + "ms; result? " + (distro != null));
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_DOWNLOAD_TIME_MS, clamp(MAX_DOWNLOAD_TIME_MSEC, duration));
|
||||||
|
|
||||||
|
if (distro == null) {
|
||||||
|
// Nothing to do.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to copy distribution files from the fetched stream.
|
||||||
|
try {
|
||||||
|
Log.d(LOGTAG, "Copying files from fetched zip.");
|
||||||
|
if (copyFilesFromStream(distro)) {
|
||||||
|
// We always copy to the data dir, and we only copy files from
|
||||||
|
// a 'distribution' subdirectory. Track our dist dir now that
|
||||||
|
// we know it.
|
||||||
|
this.distributionDir = new File(getDataDir(), DISTRIBUTION_PATH);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
Log.e(LOGTAG, "Security exception copying files. Corrupt or malicious?", e);
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_SECURITY_EXCEPTION);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(LOGTAG, "Error copying files from distribution.", e);
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_POST_FETCH_EXCEPTION);
|
||||||
|
} finally {
|
||||||
|
distro.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(LOGTAG, "Error copying distribution files from network.", e);
|
||||||
|
recordFetchTelemetry(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int clamp(long v, long c) {
|
||||||
|
return (int) Math.min(c, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the provided URI, returning a {@link JarInputStream} if the response body
|
||||||
|
* is appropriate.
|
||||||
|
*
|
||||||
|
* Protected to allow for mocking.
|
||||||
|
*
|
||||||
|
* @return the entity body as a stream, or null on failure.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("static-method")
|
||||||
|
@RobocopTarget
|
||||||
|
protected JarInputStream fetchDistribution(URI uri, HttpURLConnection connection) throws IOException {
|
||||||
|
final int status = connection.getResponseCode();
|
||||||
|
|
||||||
|
Log.d(LOGTAG, "Distribution fetch: " + status);
|
||||||
|
// We record HTTP statuses as 2xx, 3xx, 4xx, 5xx => 2, 3, 4, 5.
|
||||||
|
final int value;
|
||||||
|
if (status > 599 || status < 100) {
|
||||||
|
Log.wtf(LOGTAG, "Unexpected HTTP status code: " + status);
|
||||||
|
value = CODE_CATEGORY_STATUS_OUT_OF_RANGE;
|
||||||
|
} else {
|
||||||
|
value = status / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, value);
|
||||||
|
|
||||||
|
if (status != 200) {
|
||||||
|
Log.w(LOGTAG, "Got status " + status + " fetching distribution.");
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String contentType = connection.getContentType();
|
||||||
|
if (contentType == null || !contentType.startsWith(EXPECTED_CONTENT_TYPE)) {
|
||||||
|
Log.w(LOGTAG, "Malformed response: invalid Content-Type.");
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JarInputStream(new BufferedInputStream(connection.getInputStream()), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void recordFetchTelemetry(final Exception exception) {
|
||||||
|
if (exception == null) {
|
||||||
|
// Should never happen.
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception instanceof UnknownHostException) {
|
||||||
|
// Unknown host => we're offline.
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_OFFLINE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception instanceof SSLException) {
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SSL_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exception instanceof ProtocolException ||
|
||||||
|
exception instanceof SocketException) {
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_SOCKET_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_FETCH_EXCEPTION);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute tasks that wanted to run when we were done loading
|
* Execute tasks that wanted to run when we were done loading
|
||||||
* the distribution. These tasks are expected to call {@link #exists()}
|
* the distribution. These tasks are expected to call {@link #exists()}
|
||||||
|
@ -308,7 +544,7 @@ public final class Distribution {
|
||||||
// We always copy to the data dir, and we only copy files from
|
// We always copy to the data dir, and we only copy files from
|
||||||
// a 'distribution' subdirectory. Track our dist dir now that
|
// a 'distribution' subdirectory. Track our dist dir now that
|
||||||
// we know it.
|
// we know it.
|
||||||
this.distributionDir = new File(getDataDir(), "distribution/");
|
this.distributionDir = new File(getDataDir(), DISTRIBUTION_PATH);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -330,6 +566,41 @@ public final class Distribution {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpack distribution files from a downloaded jar stream.
|
||||||
|
*
|
||||||
|
* The caller is responsible for closing the provided stream.
|
||||||
|
*/
|
||||||
|
private boolean copyFilesFromStream(JarInputStream jar) throws FileNotFoundException, IOException {
|
||||||
|
final byte[] buffer = new byte[1024];
|
||||||
|
boolean distributionSet = false;
|
||||||
|
JarEntry entry;
|
||||||
|
while ((entry = jar.getNextJarEntry()) != null) {
|
||||||
|
final String name = entry.getName();
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
// We'll let getDataFile deal with creating the directory hierarchy.
|
||||||
|
// Yes, we can do better, but it can wait.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name.startsWith(DISTRIBUTION_PATH)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
File outFile = getDataFile(name);
|
||||||
|
if (outFile == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
distributionSet = true;
|
||||||
|
|
||||||
|
writeStream(jar, outFile, entry.getTime(), buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return distributionSet;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies the /distribution folder out of the APK and into the app's data directory.
|
* Copies the /distribution folder out of the APK and into the app's data directory.
|
||||||
* Returns true if distribution files were found and copied.
|
* Returns true if distribution files were found and copied.
|
||||||
|
@ -352,7 +623,7 @@ public final class Distribution {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name.startsWith("distribution/")) {
|
if (!name.startsWith(DISTRIBUTION_PATH)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -413,6 +684,29 @@ public final class Distribution {
|
||||||
return outFile;
|
return outFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private URI getReferredDistribution(ReferrerDescriptor descriptor) {
|
||||||
|
final String content = descriptor.content;
|
||||||
|
if (content == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We restrict here to avoid injection attacks. After all,
|
||||||
|
// we're downloading a distribution payload based on intent input.
|
||||||
|
if (!content.matches("^[a-zA-Z0-9]+$")) {
|
||||||
|
Log.e(LOGTAG, "Invalid referrer content: " + content);
|
||||||
|
Telemetry.HistogramAdd(HISTOGRAM_REFERRER_INVALID, 1);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URI(FETCH_PROTOCOL, FETCH_HOSTNAME, FETCH_PATH + content + FETCH_EXTENSION, null);
|
||||||
|
} catch (URISyntaxException e) {
|
||||||
|
// This should never occur.
|
||||||
|
Log.wtf(LOGTAG, "Invalid URI with content " + content + "!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After calling this method, either <code>distributionDir</code>
|
* After calling this method, either <code>distributionDir</code>
|
||||||
* will be set, or there is no distribution in use.
|
* will be set, or there is no distribution in use.
|
||||||
|
@ -432,7 +726,7 @@ public final class Distribution {
|
||||||
// the APK, or it exists in /system/.
|
// the APK, or it exists in /system/.
|
||||||
// Look in each location in turn.
|
// Look in each location in turn.
|
||||||
// (This could be optimized by caching the path in shared prefs.)
|
// (This could be optimized by caching the path in shared prefs.)
|
||||||
File copied = new File(getDataDir(), "distribution/");
|
File copied = new File(getDataDir(), DISTRIBUTION_PATH);
|
||||||
if (copied.exists()) {
|
if (copied.exists()) {
|
||||||
return this.distributionDir = copied;
|
return this.distributionDir = copied;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/* 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.distribution;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.mozglue.RobocopTarget;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates access to values encoded in the "referrer" extra of an install intent.
|
||||||
|
*
|
||||||
|
* This object is immutable.
|
||||||
|
*
|
||||||
|
* Example input:
|
||||||
|
*
|
||||||
|
* "utm_source=campsource&utm_medium=campmed&utm_term=term%2Bhere&utm_content=content&utm_campaign=name"
|
||||||
|
*/
|
||||||
|
@RobocopTarget
|
||||||
|
public class ReferrerDescriptor {
|
||||||
|
public final String source;
|
||||||
|
public final String medium;
|
||||||
|
public final String term;
|
||||||
|
public final String content;
|
||||||
|
public final String campaign;
|
||||||
|
|
||||||
|
public ReferrerDescriptor(final String referrer) {
|
||||||
|
if (referrer == null) {
|
||||||
|
source = null;
|
||||||
|
medium = null;
|
||||||
|
term = null;
|
||||||
|
content = null;
|
||||||
|
campaign = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Uri u = new Uri.Builder()
|
||||||
|
.scheme("http")
|
||||||
|
.authority("local")
|
||||||
|
.path("/")
|
||||||
|
.encodedQuery(referrer).build();
|
||||||
|
|
||||||
|
source = u.getQueryParameter("utm_source");
|
||||||
|
medium = u.getQueryParameter("utm_medium");
|
||||||
|
term = u.getQueryParameter("utm_term");
|
||||||
|
content = u.getQueryParameter("utm_content");
|
||||||
|
campaign = u.getQueryParameter("utm_campaign");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "{s: " + source + ", m: " + medium + ", t: " + term + ", c: " + content + ", c: " + campaign + "}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
/* -*- 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.distribution;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.mozilla.gecko.GeckoAppShell;
|
||||||
|
import org.mozilla.gecko.GeckoEvent;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
public class ReferrerReceiver extends BroadcastReceiver {
|
||||||
|
private static final String LOGTAG = "GeckoReferrerReceiver";
|
||||||
|
|
||||||
|
private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the install intent has this source, we'll track the campaign ID.
|
||||||
|
*/
|
||||||
|
private static final String MOZILLA_UTM_SOURCE = "mozilla";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the install intent has this campaign, we'll load the specified distribution.
|
||||||
|
*/
|
||||||
|
private static final String DISTRIBUTION_UTM_CAMPAIGN = "distribution";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
Log.v(LOGTAG, "Received intent " + intent);
|
||||||
|
if (!ACTION_INSTALL_REFERRER.equals(intent.getAction())) {
|
||||||
|
// This should never happen.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReferrerDescriptor referrer = new ReferrerDescriptor(intent.getStringExtra("referrer"));
|
||||||
|
|
||||||
|
// Track the referrer object for distribution handling.
|
||||||
|
if (TextUtils.equals(referrer.campaign, DISTRIBUTION_UTM_CAMPAIGN)) {
|
||||||
|
Distribution.onReceivedReferrer(referrer);
|
||||||
|
} else {
|
||||||
|
Log.d(LOGTAG, "Not downloading distribution: non-matching campaign.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a Mozilla campaign, pass the campaign along to Gecko.
|
||||||
|
if (TextUtils.equals(referrer.source, MOZILLA_UTM_SOURCE)) {
|
||||||
|
propagateMozillaCampaign(referrer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void propagateMozillaCampaign(ReferrerDescriptor referrer) {
|
||||||
|
if (referrer.campaign == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final JSONObject data = new JSONObject();
|
||||||
|
data.put("id", "playstore");
|
||||||
|
data.put("version", referrer.campaign);
|
||||||
|
String payload = data.toString();
|
||||||
|
|
||||||
|
// Try to make sure the prefs are written as a group.
|
||||||
|
final GeckoEvent event = GeckoEvent.createBroadcastEvent("Campaign:Set", payload);
|
||||||
|
GeckoAppShell.sendEventToGecko(event);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.e(LOGTAG, "Error propagating campaign identifier.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -156,6 +156,8 @@ gbjar.sources += [
|
||||||
'db/TabsProvider.java',
|
'db/TabsProvider.java',
|
||||||
'db/TopSitesCursorWrapper.java',
|
'db/TopSitesCursorWrapper.java',
|
||||||
'distribution/Distribution.java',
|
'distribution/Distribution.java',
|
||||||
|
'distribution/ReferrerDescriptor.java',
|
||||||
|
'distribution/ReferrerReceiver.java',
|
||||||
'DoorHangerPopup.java',
|
'DoorHangerPopup.java',
|
||||||
'DynamicToolbar.java',
|
'DynamicToolbar.java',
|
||||||
'EditBookmarkDialog.java',
|
'EditBookmarkDialog.java',
|
||||||
|
@ -354,7 +356,6 @@ gbjar.sources += [
|
||||||
'prompts/PromptService.java',
|
'prompts/PromptService.java',
|
||||||
'prompts/TabInput.java',
|
'prompts/TabInput.java',
|
||||||
'ReaderModeUtils.java',
|
'ReaderModeUtils.java',
|
||||||
'ReferrerReceiver.java',
|
|
||||||
'Restarter.java',
|
'Restarter.java',
|
||||||
'ScrollAnimator.java',
|
'ScrollAnimator.java',
|
||||||
'ServiceNotificationClient.java',
|
'ServiceNotificationClient.java',
|
||||||
|
|
|
@ -2,19 +2,29 @@ package org.mozilla.gecko.tests;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.jar.JarInputStream;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.mozilla.gecko.Actions;
|
import org.mozilla.gecko.Actions;
|
||||||
|
import org.mozilla.gecko.AppConstants;
|
||||||
import org.mozilla.gecko.db.BrowserContract;
|
import org.mozilla.gecko.db.BrowserContract;
|
||||||
import org.mozilla.gecko.distribution.Distribution;
|
import org.mozilla.gecko.distribution.Distribution;
|
||||||
|
import org.mozilla.gecko.distribution.ReferrerDescriptor;
|
||||||
|
import org.mozilla.gecko.mozglue.RobocopTarget;
|
||||||
import org.mozilla.gecko.util.ThreadUtils;
|
import org.mozilla.gecko.util.ThreadUtils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests distribution customization.
|
* Tests distribution customization.
|
||||||
|
@ -28,6 +38,38 @@ import android.content.SharedPreferences;
|
||||||
* engine.xml
|
* engine.xml
|
||||||
*/
|
*/
|
||||||
public class testDistribution extends ContentProviderTest {
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
@RobocopTarget
|
||||||
|
public static void clearReferrerDescriptorForTesting() {
|
||||||
|
referrer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@RobocopTarget
|
||||||
|
public static ReferrerDescriptor getReferrerDescriptorForTesting() {
|
||||||
|
return referrer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final String MOCK_PACKAGE = "mock-package.zip";
|
private static final String MOCK_PACKAGE = "mock-package.zip";
|
||||||
private static final int PREF_REQUEST_ID = 0x7357;
|
private static final int PREF_REQUEST_ID = 0x7357;
|
||||||
|
|
||||||
|
@ -65,7 +107,7 @@ public class testDistribution extends ContentProviderTest {
|
||||||
mAsserter.dumpLog("Background task completed. Proceeding.");
|
mAsserter.dumpLog("Background task completed. Proceeding.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testDistribution() {
|
public void testDistribution() throws Exception {
|
||||||
mActivity = getActivity();
|
mActivity = getActivity();
|
||||||
|
|
||||||
String mockPackagePath = getMockPackagePath();
|
String mockPackagePath = getMockPackagePath();
|
||||||
|
@ -87,6 +129,90 @@ public class testDistribution extends ContentProviderTest {
|
||||||
setTestLocale("es-MX");
|
setTestLocale("es-MX");
|
||||||
initDistribution(mockPackagePath);
|
initDistribution(mockPackagePath);
|
||||||
checkLocalizedPreferences("es-MX");
|
checkLocalizedPreferences("es-MX");
|
||||||
|
|
||||||
|
// Test the (stubbed) download interaction.
|
||||||
|
setTestLocale("en-US");
|
||||||
|
clearDistributionPref();
|
||||||
|
doTestValidReferrerIntent();
|
||||||
|
|
||||||
|
clearDistributionPref();
|
||||||
|
doTestInvalidReferrerIntent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doTestValidReferrerIntent() throws Exception {
|
||||||
|
// Send the faux-download intent.
|
||||||
|
// 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 Intent intent = new Intent(ACTION_INSTALL_REFERRER);
|
||||||
|
intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER);
|
||||||
|
intent.putExtra("referrer", ref);
|
||||||
|
mActivity.sendBroadcast(intent);
|
||||||
|
|
||||||
|
// Wait for the intent to be processed.
|
||||||
|
final TestableDistribution distribution = new TestableDistribution(mActivity);
|
||||||
|
|
||||||
|
final Object wait = new Object();
|
||||||
|
distribution.addOnDistributionReadyCallback(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
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 (wait) {
|
||||||
|
wait.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
distribution.go();
|
||||||
|
synchronized (wait) {
|
||||||
|
wait.wait(WAIT_TIMEOUT_MSEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
// Send the faux-download intent.
|
||||||
|
// 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 Intent intent = new Intent(ACTION_INSTALL_REFERRER);
|
||||||
|
intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, CLASS_REFERRER_RECEIVER);
|
||||||
|
intent.putExtra("referrer", ref);
|
||||||
|
mActivity.sendBroadcast(intent);
|
||||||
|
|
||||||
|
// Wait for the intent to be processed.
|
||||||
|
final TestableDistribution distribution = new TestableDistribution(mActivity);
|
||||||
|
|
||||||
|
final Object wait = new Object();
|
||||||
|
distribution.addOnDistributionReadyCallback(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mAsserter.ok(!distribution.exists(), "Not processed.", "No download because campaign was wrong.");
|
||||||
|
ReferrerDescriptor referrerValue = TestableDistribution.getReferrerDescriptorForTesting();
|
||||||
|
mAsserter.is(referrerValue, null, "No referrer.");
|
||||||
|
synchronized (wait) {
|
||||||
|
wait.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
distribution.go();
|
||||||
|
synchronized (wait) {
|
||||||
|
wait.wait(WAIT_TIMEOUT_MSEC);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the distribution from the mock package.
|
// Initialize the distribution from the mock package.
|
||||||
|
@ -288,12 +414,16 @@ public class testDistribution extends ContentProviderTest {
|
||||||
return mockPackagePath;
|
return mockPackagePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clears the distribution pref to return distribution state to STATE_UNKNOWN
|
/**
|
||||||
|
* Clears the distribution pref to return distribution state to STATE_UNKNOWN,
|
||||||
|
* and wipes the in-memory referrer pigeonhole.
|
||||||
|
*/
|
||||||
private void clearDistributionPref() {
|
private void clearDistributionPref() {
|
||||||
mAsserter.dumpLog("Clearing distribution pref.");
|
mAsserter.dumpLog("Clearing distribution pref.");
|
||||||
SharedPreferences settings = mActivity.getSharedPreferences("GeckoApp", Activity.MODE_PRIVATE);
|
SharedPreferences settings = mActivity.getSharedPreferences("GeckoApp", Activity.MODE_PRIVATE);
|
||||||
String keyName = mActivity.getPackageName() + ".distribution_state";
|
String keyName = mActivity.getPackageName() + ".distribution_state";
|
||||||
settings.edit().remove(keyName).commit();
|
settings.edit().remove(keyName).commit();
|
||||||
|
TestableDistribution.clearReferrerDescriptorForTesting();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -11,6 +11,7 @@ jar.sources += [
|
||||||
'src/harness/BrowserInstrumentationTestRunner.java',
|
'src/harness/BrowserInstrumentationTestRunner.java',
|
||||||
'src/harness/BrowserTestListener.java',
|
'src/harness/BrowserTestListener.java',
|
||||||
'src/tests/BrowserTestCase.java',
|
'src/tests/BrowserTestCase.java',
|
||||||
|
'src/tests/TestDistribution.java',
|
||||||
'src/tests/TestGeckoSharedPrefs.java',
|
'src/tests/TestGeckoSharedPrefs.java',
|
||||||
'src/tests/TestJarReader.java',
|
'src/tests/TestJarReader.java',
|
||||||
'src/tests/TestRawResource.java',
|
'src/tests/TestRawResource.java',
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/* 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.browser.tests;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.distribution.ReferrerDescriptor;
|
||||||
|
|
||||||
|
public class TestDistribution extends BrowserTestCase {
|
||||||
|
private static final String TEST_REFERRER_STRING = "utm_source=campsource&utm_medium=campmed&utm_term=term%2Bhere&utm_content=content&utm_campaign=name";
|
||||||
|
private static final String TEST_MALFORMED_REFERRER_STRING = "utm_source=campsource&utm_medium=campmed&utm_term=term%2";
|
||||||
|
|
||||||
|
public void testReferrerParsing() {
|
||||||
|
ReferrerDescriptor good = new ReferrerDescriptor(TEST_REFERRER_STRING);
|
||||||
|
assertEquals("campsource", good.source);
|
||||||
|
assertEquals("campmed", good.medium);
|
||||||
|
assertEquals("term+here", good.term);
|
||||||
|
assertEquals("content", good.content);
|
||||||
|
assertEquals("name", good.campaign);
|
||||||
|
|
||||||
|
// Uri.Builder is permissive.
|
||||||
|
ReferrerDescriptor bad = new ReferrerDescriptor(TEST_MALFORMED_REFERRER_STRING);
|
||||||
|
assertEquals("campsource", bad.source);
|
||||||
|
assertEquals("campmed", bad.medium);
|
||||||
|
assertFalse("term+here".equals(bad.term));
|
||||||
|
assertNull(bad.content);
|
||||||
|
assertNull(bad.campaign);
|
||||||
|
|
||||||
|
ReferrerDescriptor ugly = new ReferrerDescriptor(null);
|
||||||
|
assertNull(ugly.source);
|
||||||
|
assertNull(ugly.medium);
|
||||||
|
assertNull(ugly.term);
|
||||||
|
assertNull(ugly.content);
|
||||||
|
assertNull(ugly.campaign);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2959,6 +2959,28 @@
|
||||||
"extended_statistics_ok": true,
|
"extended_statistics_ok": true,
|
||||||
"description": "PLACES: Time to calculate the md5 hash for a backup"
|
"description": "PLACES: Time to calculate the md5 hash for a backup"
|
||||||
},
|
},
|
||||||
|
"FENNEC_DISTRIBUTION_REFERRER_INVALID": {
|
||||||
|
"expires_in_version": "never",
|
||||||
|
"kind": "flag",
|
||||||
|
"description": "Whether the referrer intent specified an invalid distribution name",
|
||||||
|
"cpp_guard": "ANDROID"
|
||||||
|
},
|
||||||
|
"FENNEC_DISTRIBUTION_CODE_CATEGORY": {
|
||||||
|
"expires_in_version": "never",
|
||||||
|
"kind": "enumerated",
|
||||||
|
"n_values": 20,
|
||||||
|
"description": "First digit of HTTP result code, or error category, during distribution download",
|
||||||
|
"cpp_guard": "ANDROID"
|
||||||
|
},
|
||||||
|
"FENNEC_DISTRIBUTION_DOWNLOAD_TIME_MS": {
|
||||||
|
"expires_in_version": "never",
|
||||||
|
"kind": "exponential",
|
||||||
|
"low": 100,
|
||||||
|
"high": "40000",
|
||||||
|
"n_buckets": 30,
|
||||||
|
"description": "Time taken to download a specified distribution file (msec)",
|
||||||
|
"cpp_guard": "ANDROID"
|
||||||
|
},
|
||||||
"FENNEC_FAVICONS_COUNT": {
|
"FENNEC_FAVICONS_COUNT": {
|
||||||
"expires_in_version": "never",
|
"expires_in_version": "never",
|
||||||
"kind": "exponential",
|
"kind": "exponential",
|
||||||
|
|
|
@ -39,10 +39,9 @@ const UDPSocket = CC("@mozilla.org/network/udp-socket;1",
|
||||||
"nsIUDPSocket",
|
"nsIUDPSocket",
|
||||||
"init");
|
"init");
|
||||||
|
|
||||||
// TODO Bug 1027456: May need to reserve these with IANA
|
|
||||||
const SCAN_PORT = 50624;
|
const SCAN_PORT = 50624;
|
||||||
const UPDATE_PORT = 50625;
|
const UPDATE_PORT = 50625;
|
||||||
const ADDRESS = "224.0.0.200";
|
const ADDRESS = "224.0.0.115";
|
||||||
const REPLY_TIMEOUT = 5000;
|
const REPLY_TIMEOUT = 5000;
|
||||||
|
|
||||||
const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
|
const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
|
||||||
|
@ -158,6 +157,8 @@ function Discovery() {
|
||||||
this._onRemoteUpdate = this._onRemoteUpdate.bind(this);
|
this._onRemoteUpdate = this._onRemoteUpdate.bind(this);
|
||||||
this._purgeMissingDevices = this._purgeMissingDevices.bind(this);
|
this._purgeMissingDevices = this._purgeMissingDevices.bind(this);
|
||||||
|
|
||||||
|
Services.obs.addObserver(this, "network-active-changed", false);
|
||||||
|
|
||||||
this._getSystemInfo();
|
this._getSystemInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,6 +296,35 @@ Discovery.prototype = {
|
||||||
this._transports.update = null;
|
this._transports.update = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
observe: function(subject, topic, data) {
|
||||||
|
if (topic !== "network-active-changed") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let activeNetwork = subject;
|
||||||
|
if (!activeNetwork) {
|
||||||
|
log("No active network");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeNetwork = activeNetwork.QueryInterface(Ci.nsINetworkInterface);
|
||||||
|
log("Active network changed to: " + activeNetwork.type);
|
||||||
|
// UDP sockets go down when the device goes offline, so we'll restart them
|
||||||
|
// when the active network goes back to WiFi.
|
||||||
|
if (activeNetwork.type === Ci.nsINetworkInterface.NETWORK_TYPE_WIFI) {
|
||||||
|
this._restartListening();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_restartListening: function() {
|
||||||
|
if (this._transports.scan) {
|
||||||
|
this._stopListeningForScan();
|
||||||
|
this._startListeningForScan();
|
||||||
|
}
|
||||||
|
if (this._transports.update) {
|
||||||
|
this._stopListeningForUpdate();
|
||||||
|
this._startListeningForUpdate();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When sending message, we can use either transport, so just pick the first
|
* When sending message, we can use either transport, so just pick the first
|
||||||
* one currently alive.
|
* one currently alive.
|
||||||
|
|
|
@ -363,7 +363,7 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
|
||||||
let { caller, args, window, name } = functionCall.details;
|
let { caller, args, window, name } = functionCall.details;
|
||||||
let source = caller;
|
let source = caller;
|
||||||
let dest = args[0];
|
let dest = args[0];
|
||||||
let isAudioParam = dest instanceof window.AudioParam;
|
let isAudioParam = dest ? getConstructorName(dest) === "AudioParam" : false;
|
||||||
|
|
||||||
// audionode.connect(param)
|
// audionode.connect(param)
|
||||||
if (name === "connect" && isAudioParam) {
|
if (name === "connect" && isAudioParam) {
|
||||||
|
@ -433,8 +433,9 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
|
||||||
},
|
},
|
||||||
"connect-param": {
|
"connect-param": {
|
||||||
type: "connectParam",
|
type: "connectParam",
|
||||||
source: Arg(0, "audionode"),
|
source: Option(0, "audionode"),
|
||||||
param: Arg(1, "string")
|
dest: Option(0, "audionode"),
|
||||||
|
param: Option(0, "string")
|
||||||
},
|
},
|
||||||
"change-param": {
|
"change-param": {
|
||||||
type: "changeParam",
|
type: "changeParam",
|
||||||
|
@ -461,12 +462,30 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
|
||||||
// Ensure AudioNode is wrapped.
|
// Ensure AudioNode is wrapped.
|
||||||
node = new XPCNativeWrapper(node);
|
node = new XPCNativeWrapper(node);
|
||||||
|
|
||||||
|
this._instrumentParams(node);
|
||||||
|
|
||||||
let actor = new AudioNodeActor(this.conn, node);
|
let actor = new AudioNodeActor(this.conn, node);
|
||||||
this.manage(actor);
|
this.manage(actor);
|
||||||
this._nativeToActorID.set(node.id, actor.actorID);
|
this._nativeToActorID.set(node.id, actor.actorID);
|
||||||
return actor;
|
return actor;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes an XrayWrapper node, and attaches the node's `nativeID`
|
||||||
|
* to the AudioParams as `_parentID`, as well as the the type of param
|
||||||
|
* as a string on `_paramName`.
|
||||||
|
*/
|
||||||
|
_instrumentParams: function (node) {
|
||||||
|
let type = getConstructorName(node);
|
||||||
|
Object.keys(NODE_PROPERTIES[type])
|
||||||
|
.filter(isAudioParam.bind(null, node))
|
||||||
|
.forEach(paramName => {
|
||||||
|
let param = node[paramName];
|
||||||
|
param._parentID = node.id;
|
||||||
|
param._paramName = paramName;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes an AudioNode and returns the stored actor for it.
|
* Takes an AudioNode and returns the stored actor for it.
|
||||||
* In some cases, we won't have an actor stored (for example,
|
* In some cases, we won't have an actor stored (for example,
|
||||||
|
@ -505,10 +524,15 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when an audio node is connected to an audio param.
|
* Called when an audio node is connected to an audio param.
|
||||||
* Implement in bug 986705
|
|
||||||
*/
|
*/
|
||||||
_onConnectParam: function (source, dest) {
|
_onConnectParam: function (source, param) {
|
||||||
// TODO bug 986705
|
let sourceActor = this._getActorByNativeID(source.id);
|
||||||
|
let destActor = this._getActorByNativeID(param._parentID);
|
||||||
|
emit(this, "connect-param", {
|
||||||
|
source: sourceActor,
|
||||||
|
dest: destActor,
|
||||||
|
param: param._paramName
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Загрузка…
Ссылка в новой задаче