Bug 1534451 - Send Mobile Activation Telemetry ping. r=JanH

Differential Revision: https://phabricator.services.mozilla.com/D29668

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Vlad Baicu 2019-05-15 14:00:12 +00:00
Родитель a38783d2fe
Коммит 32c011ff8e
9 изменённых файлов: 363 добавлений и 41 удалений

Просмотреть файл

@ -147,6 +147,7 @@ android {
if (!mozconfig.substs.MOZ_ANDROID_GCM) {
exclude 'org/mozilla/gecko/gcm/**/*.java'
exclude 'org/mozilla/gecko/push/**/*.java'
exclude 'org/mozilla/gecko/advertising/**'
}
if (!mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
@ -177,6 +178,7 @@ android {
if (!mozconfig.substs.MOZ_ANDROID_GCM) {
exclude 'org/mozilla/gecko/gcm/**/*.java'
exclude 'org/mozilla/gecko/push/**/*.java'
exclude 'org/mozilla/gecko/advertising/**'
}
}
resources {
@ -244,6 +246,8 @@ dependencies {
implementation "com.google.android.gms:play-services-basement:$google_play_services_version"
implementation "com.google.android.gms:play-services-base:$google_play_services_version"
implementation "com.google.android.gms:play-services-gcm:$google_play_services_version"
implementation "com.google.android.gms:play-services-ads-identifier:$google_play_services_version"
implementation "org.mindrot:jbcrypt:0.4"
}
if (mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {

Просмотреть файл

@ -138,6 +138,7 @@ import org.mozilla.gecko.tabs.TabsPanel;
import org.mozilla.gecko.telemetry.TelemetryCorePingDelegate;
import org.mozilla.gecko.telemetry.TelemetryUploadService;
import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
import org.mozilla.gecko.telemetry.TelemetryActivationPingDelegate;
import org.mozilla.gecko.toolbar.AutocompleteHandler;
import org.mozilla.gecko.toolbar.BrowserToolbar;
import org.mozilla.gecko.toolbar.BrowserToolbar.CommitEventSource;
@ -317,6 +318,7 @@ public class BrowserApp extends GeckoApp
private final DynamicToolbar mDynamicToolbar = new DynamicToolbar();
private final TelemetryCorePingDelegate mTelemetryCorePingDelegate = new TelemetryCorePingDelegate();
private final TelemetryActivationPingDelegate mTelemetryActivationPingDelegate = new TelemetryActivationPingDelegate();
private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
new ScreenshotDelegate(),
@ -324,6 +326,7 @@ public class BrowserApp extends GeckoApp
new ReaderViewBookmarkPromotion(),
new PostUpdateHandler(),
mTelemetryCorePingDelegate,
mTelemetryActivationPingDelegate,
new OfflineTabStatusDelegate(),
new AdjustBrowserAppDelegate(mTelemetryCorePingDelegate)
));

Просмотреть файл

@ -0,0 +1,35 @@
package org.mozilla.gecko.advertising;
import android.content.Context;
import android.util.Log;
import com.google.android.gms.ads.identifier.AdvertisingIdClient;
import org.mindrot.jbcrypt.BCrypt;
import org.mozilla.gecko.annotation.ReflectionTarget;
@ReflectionTarget
public class AdvertisingUtil {
private static final String LOG_TAG = AdvertisingUtil.class.getCanonicalName();
/* Use the same SALT for all BCrypt hashings. We want the SALT to be stable for all Fennec users but it should differ from the one from Fenix.
* Generated using Bcrypt.gensalt(). */
private static final String BCRYPT_SALT = "$2a$10$ZfglUfcbmTyaBbAQ7SL9OO";
/**
* Retrieves the advertising ID hashed with BCrypt. Requires Google Play Services. Note: This method must not run on
* the main thread.
*/
@ReflectionTarget
public static String getAdvertisingId(Context caller) {
try {
AdvertisingIdClient.Info info = AdvertisingIdClient.getAdvertisingIdInfo(caller);
String advertisingId = info.getId();
return advertisingId != null ? BCrypt.hashpw(advertisingId, BCRYPT_SALT) : null;
} catch (Throwable t) {
Log.e(LOG_TAG, "Error retrieving advertising ID. " + t.getMessage());
}
return null;
}
}

Просмотреть файл

@ -0,0 +1,99 @@
package org.mozilla.gecko.telemetry;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.WorkerThread;
import android.util.Log;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.delegates.BrowserAppDelegate;
import org.mozilla.gecko.telemetry.pingbuilders.TelemetryActivationPingBuilder;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.ThreadUtils;
import java.io.IOException;
import java.lang.reflect.Method;
/**
* An activity-lifecycle delegate for uploading the activation ping.
*/
public class TelemetryActivationPingDelegate extends BrowserAppDelegate {
private static final String LOGTAG = StringUtils.safeSubstring(
"Gecko" + TelemetryActivationPingDelegate.class.getSimpleName(), 0, 23);
private TelemetryDispatcher telemetryDispatcher; // lazy
@Override
public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
super.onCreate(browserApp, savedInstanceState);
uploadActivationPing(browserApp);
}
private void uploadActivationPing(final BrowserApp activity) {
if (!AppConstants.MOZ_ANDROID_GCM) {
return;
}
if (TelemetryActivationPingBuilder.activationPingAlreadySent(activity)) {
return;
}
ThreadUtils.postToBackgroundThread(() -> {
if (activity == null) {
return;
}
if (!TelemetryUploadService.isUploadEnabledByAppConfig(activity)) {
Log.d(LOGTAG, "Activation ping upload disabled by app config. Returning.");
return;
}
String identifier = null;
try {
final Class<?> clazz = Class.forName("org.mozilla.gecko.advertising.AdvertisingUtil");
final Method getAdvertisingId = clazz.getMethod("getAdvertisingId", Context.class);
identifier = (String) getAdvertisingId.invoke(null, activity);
} catch (Exception e) {
Log.w(LOGTAG, "Unable to get identifier: " + e);
}
final GeckoProfile profile = GeckoThread.getActiveProfile();
String clientID = null;
try {
clientID = profile.getClientId();
} catch (final IOException e) {
Log.w(LOGTAG, "Unable to get client ID: " + e);
if (identifier == null) {
//Activation ping is mandatory to be sent with either the identifier or the clientID.
Log.d(LOGTAG, "Activation ping failed to send - both identifier and clientID were unable to be retrieved.");
return;
}
}
final TelemetryActivationPingBuilder pingBuilder = new TelemetryActivationPingBuilder(activity);
if (identifier != null) {
pingBuilder.setIdentifier(identifier);
} else {
pingBuilder.setClientID(clientID);
}
getTelemetryDispatcher().queuePingForUpload(activity, pingBuilder);
});
}
@WorkerThread // via constructor
private TelemetryDispatcher getTelemetryDispatcher() {
if (telemetryDispatcher == null) {
final GeckoProfile profile = GeckoThread.getActiveProfile();
final String profilePath = profile.getDir().getAbsolutePath();
final String profileName = profile.getName();
telemetryDispatcher = new TelemetryDispatcher(profilePath, profileName);
}
return telemetryDispatcher;
}
}

Просмотреть файл

@ -21,11 +21,11 @@ import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.adjust.AttributionHelperListener;
import org.mozilla.gecko.telemetry.measurements.CampaignIdMeasurements;
import org.mozilla.gecko.delegates.BrowserAppDelegateWithReference;
import org.mozilla.gecko.distribution.DistributionStoreCallback;
import org.mozilla.gecko.search.SearchEngineManager;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.telemetry.measurements.CampaignIdMeasurements;
import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
import org.mozilla.gecko.telemetry.measurements.SessionMeasurements;
import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder;
@ -139,47 +139,43 @@ public class TelemetryCorePingDelegate extends BrowserAppDelegateWithReference
// the first launch of the activity doesn't trigger profile init too early.
//
// Additionally, getAndIncrementSequenceNumber must be called from a worker thread.
ThreadUtils.postToBackgroundThread(new Runnable() {
@WorkerThread
@Override
public void run() {
final BrowserApp activity = getBrowserApp();
if (activity == null) {
return;
}
final GeckoProfile profile = GeckoThread.getActiveProfile();
if (!TelemetryUploadService.isUploadEnabledByProfileConfig(activity, profile)) {
Log.d(LOGTAG, "Core ping upload disabled by profile config. Returning.");
return;
}
final String clientID;
final boolean hadCanaryClientId;
try {
clientID = profile.getClientId();
hadCanaryClientId = profile.getIfHadCanaryClientId();
} catch (final IOException e) {
Log.w(LOGTAG, "Unable to get client ID properties to generate core ping: " + e);
return;
}
// Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
final SharedPreferences sharedPrefs = getSharedPreferences(activity);
final SessionMeasurements.SessionMeasurementsContainer sessionMeasurementsContainer =
sessionMeasurements.getAndResetSessionMeasurements(activity);
final TelemetryCorePingBuilder pingBuilder = new TelemetryCorePingBuilder(activity)
.setClientID(clientID)
.setHadCanaryClientId(hadCanaryClientId)
.setDefaultSearchEngine(TelemetryCorePingBuilder.getEngineIdentifier(engine))
.setProfileCreationDate(TelemetryCorePingBuilder.getProfileCreationDate(activity, profile))
.setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs))
.setSessionCount(sessionMeasurementsContainer.sessionCount)
.setSessionDuration(sessionMeasurementsContainer.elapsedSeconds);
maybeSetOptionalMeasurements(activity, sharedPrefs, pingBuilder);
getTelemetryDispatcher(activity).queuePingForUpload(activity, pingBuilder);
ThreadUtils.postToBackgroundThread(() -> {
final BrowserApp activity = getBrowserApp();
if (activity == null) {
return;
}
final GeckoProfile profile = GeckoThread.getActiveProfile();
if (!TelemetryUploadService.isUploadEnabledByProfileConfig(activity, profile)) {
Log.d(LOGTAG, "Core ping upload disabled by profile config. Returning.");
return;
}
final String clientID;
final boolean hadCanaryClientId;
try {
clientID = profile.getClientId();
hadCanaryClientId = profile.getIfHadCanaryClientId();
} catch (final IOException e) {
Log.w(LOGTAG, "Unable to get client ID properties to generate core ping: " + e);
return;
}
// Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
final SharedPreferences sharedPrefs = getSharedPreferences(activity);
final SessionMeasurements.SessionMeasurementsContainer sessionMeasurementsContainer =
sessionMeasurements.getAndResetSessionMeasurements(activity);
final TelemetryCorePingBuilder pingBuilder = new TelemetryCorePingBuilder(activity)
.setClientID(clientID)
.setHadCanaryClientId(hadCanaryClientId)
.setDefaultSearchEngine(TelemetryCorePingBuilder.getEngineIdentifier(engine))
.setProfileCreationDate(TelemetryCorePingBuilder.getProfileCreationDate(activity, profile))
.setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs))
.setSessionCount(sessionMeasurementsContainer.sessionCount)
.setSessionDuration(sessionMeasurementsContainer.elapsedSeconds);
maybeSetOptionalMeasurements(activity, sharedPrefs, pingBuilder);
getTelemetryDispatcher(activity).queuePingForUpload(activity, pingBuilder);
});
}

Просмотреть файл

@ -9,6 +9,8 @@ package org.mozilla.gecko.telemetry;
import android.content.Context;
import android.support.annotation.WorkerThread;
import android.util.Log;
import org.mozilla.gecko.telemetry.pingbuilders.TelemetryActivationPingBuilder;
import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder;
import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCrashPingBuilder;
import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadScheduler;
@ -90,6 +92,14 @@ public class TelemetryDispatcher {
queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler);
}
/**
* Queues the given ping for upload and potentially schedules upload. This method can be called from any thread.
*/
public void queuePingForUpload(final Context context, final TelemetryActivationPingBuilder pingBuilder) {
final TelemetryOutgoingPing ping = pingBuilder.build();
queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler);
}
/**
* Queues the given crash ping for upload and potentially schedules upload. This method can be called from any thread.
*/

Просмотреть файл

@ -19,6 +19,7 @@ import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.telemetry.pingbuilders.TelemetryActivationPingBuilder;
import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
import org.mozilla.gecko.util.DateUtil;
import org.mozilla.gecko.util.NetworkUtils;
@ -125,6 +126,8 @@ public class TelemetryUploadService extends JobIntentService {
if (delegate.hadConnectionError()) {
break;
}
checkPingsPersistence(context, ping.getDocID());
}
final boolean wereAllUploadsSuccessful = !delegate.hadConnectionError();
@ -136,6 +139,23 @@ public class TelemetryUploadService extends JobIntentService {
return wereAllUploadsSuccessful;
}
/**
* Check if we have any pings that need to persist their succesful upload status in order to prevent further attempts.
* E.g. {@link TelemetryActivationPingBuilder}
* @param context
*/
private static void checkPingsPersistence(Context context, String successfulUploadID) {
final String activationID = TelemetryActivationPingBuilder.getActivationPingId(context);
if (activationID == null) {
return;
}
if (activationID.equals(successfulUploadID)) {
TelemetryActivationPingBuilder.setActivationPingSent(context, true);
}
}
private static void uploadPayload(final String url, final ExtendedJSONObject payload, final ResultDelegate delegate) {
final BaseResource resource;
try {

Просмотреть файл

@ -0,0 +1,128 @@
package org.mozilla.gecko.telemetry.pingbuilders;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.support.annotation.NonNull;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.distribution.DistributionStoreCallback;
import org.mozilla.gecko.telemetry.TelemetryOutgoingPing;
import org.mozilla.gecko.util.DateUtil;
import org.mozilla.gecko.util.StringUtils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
/**
* Builds a {@link TelemetryOutgoingPing} representing a activation ping.
*
*/
public class TelemetryActivationPingBuilder extends TelemetryPingBuilder {
private static final String LOGTAG = StringUtils.safeSubstring(TelemetryActivationPingBuilder.class.getSimpleName(), 0, 23);
//Using MOZ_APP_BASENAME would be more elegant but according to the server side schema we need to be sure that we always send the "Fennec" value.
private static final String APP_NAME_VALUE = "Fennec";
private static final String PREFS_ACTIVATION_ID = "activation_ping_id";
private static final String PREFS_ACTIVATION_SENT = "activation_ping_sent";
private static final String NAME = "activation";
private static final int VERSION_VALUE = 1;
private static final String IDENTIFIER = "identifier";
private static final String CLIENT_ID = "clientId";
private static final String MANUFACTURER = "manufacturer";
private static final String MODEL = "model";
private static final String DISTRIBUTION_ID = "distribution_id";
private static final String LOCALE = "locale";
private static final String OS_ATTR = "os";
private static final String OS_VERSION = "osversion";
private static final String PING_CREATION_DATE = "created";
private static final String TIMEZONE_OFFSET = "tz";
private static final String APP_NAME = "app_name";
private static final String CHANNEL = "channel";
public TelemetryActivationPingBuilder(final Context context) {
super(VERSION_VALUE, true);
initPayloadConstants(context);
}
private void initPayloadConstants(final Context context) {
payload.put(MANUFACTURER, Build.MANUFACTURER);
payload.put(MODEL, Build.MODEL);
payload.put(LOCALE, Locales.getLanguageTag(Locale.getDefault()));
payload.put(OS_ATTR, TelemetryPingBuilder.OS_NAME);
payload.put(OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
final Calendar nowCalendar = Calendar.getInstance();
final DateFormat pingCreationDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
payload.put(PING_CREATION_DATE, pingCreationDateFormat.format(nowCalendar.getTime()));
payload.put(TIMEZONE_OFFSET, DateUtil.getTimezoneOffsetInMinutesForGivenDate(nowCalendar));
payload.put(APP_NAME, APP_NAME_VALUE);
payload.put(CHANNEL, AppConstants.ANDROID_PACKAGE_NAME);
SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
final String distributionId = prefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null);
if (distributionId != null) {
payload.put(DISTRIBUTION_ID, distributionId);
}
prefs.edit().putString(PREFS_ACTIVATION_ID, docID).apply();
}
public static boolean activationPingAlreadySent(Context context) {
return GeckoSharedPrefs.forApp(context).getBoolean(PREFS_ACTIVATION_SENT, false);
}
public static void setActivationPingSent(Context context, boolean value) {
SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
prefs.edit().putBoolean(PREFS_ACTIVATION_SENT, value).apply();
prefs.edit().remove(PREFS_ACTIVATION_ID).apply();
}
public static String getActivationPingId(Context context) {
return GeckoSharedPrefs.forApp(context).getString(PREFS_ACTIVATION_ID, null);
}
@Override
public String getDocType() {
return NAME;
}
@Override
public String[] getMandatoryFields() {
return new String[] {
MANUFACTURER,
MODEL,
LOCALE,
OS_ATTR,
OS_VERSION,
PING_CREATION_DATE,
TIMEZONE_OFFSET,
APP_NAME,
CHANNEL
};
}
public TelemetryActivationPingBuilder setIdentifier(@NonNull final String identifier) {
if (identifier == null) {
throw new IllegalArgumentException("Expected non-null identifier");
}
payload.put(IDENTIFIER, identifier);
return this;
}
public TelemetryActivationPingBuilder setClientID(@NonNull final String clientID) {
if (clientID == null) {
throw new IllegalArgumentException("Expected non-null clientID");
}
payload.put(CLIENT_ID, clientID);
return this;
}
}

Просмотреть файл

@ -25,6 +25,9 @@ abstract class TelemetryPingBuilder {
// In the server url, the initial path directly after the "scheme://host:port/"
private static final String SERVER_INITIAL_PATH = "submit/telemetry";
// Modern pings now use a structured ingestion where we capture the schema version as one of the URI parameters.
private static final String SERVER_INITIAL_PATH_MODERN = "submit/mobile";
// By default Fennec ping's use the old telemetry version, this can be overridden
private static final int DEFAULT_TELEMETRY_VERSION = 1;
@ -48,6 +51,12 @@ abstract class TelemetryPingBuilder {
payload = new ExtendedJSONObject();
}
public TelemetryPingBuilder(int version, boolean modernPing) {
docID = UUID.randomUUID().toString();
serverPath = modernPing ? getModernTelemetryServerPath(getDocType(), docID, version) : getTelemetryServerPath(getDocType(), docID, version);
payload = new ExtendedJSONObject();
}
/**
* @return the name of the ping (e.g. "core")
*/
@ -100,4 +109,22 @@ abstract class TelemetryPingBuilder {
appBuildId +
(version == UNIFIED_TELEMETRY_VERSION ? "?v=4" : "");
}
/**
* Returns a url of the format:
* http://hostname/submit/mobile/docType/appVersion/docId/
*
* User for modern structured ingestion.
*
* @param docType The name of the ping (e.g. "main")
* @param docID A UUID that identifies the ping
* @param version The ping format version
* @return a url at which to POST the telemetry data to
*/
private static String getModernTelemetryServerPath(final String docType, final String docID, int version) {
return SERVER_INITIAL_PATH_MODERN + '/' +
docType + '/' +
version + '/' +
docID;
}
}