diff options
author | Matt A. Tobin <email@mattatobin.com> | 2019-04-23 15:32:23 -0400 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2019-04-23 15:32:23 -0400 |
commit | abe80cc31d5a40ebed743085011fbcda0c1a9a10 (patch) | |
tree | fb3762f06b84745b182af281abb107b95a9fcf01 /mobile/android/base/java/org/mozilla/gecko/telemetry | |
parent | 63295d0087eb58a6eb34cad324c4c53d1b220491 (diff) | |
download | UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar.gz UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar.lz UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.tar.xz UXP-abe80cc31d5a40ebed743085011fbcda0c1a9a10.zip |
Issue #1053 - Drop support Android and remove Fennec - Part 1a: Remove mobile/android
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/telemetry')
15 files changed, 0 insertions, 1771 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java deleted file mode 100644 index 6ed4bb0d4..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java +++ /dev/null @@ -1,16 +0,0 @@ -/* 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.telemetry; - -import org.mozilla.gecko.AppConstants; - -public class TelemetryConstants { - // To test, set this to true & change "toolkit.telemetry.server" in about:config. - public static final boolean UPLOAD_ENABLED = AppConstants.MOZILLA_OFFICIAL; // Disabled for developer builds. - - public static final String USER_AGENT = - "Firefox-Android-Telemetry/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")"; - -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java deleted file mode 100644 index fae674b2d..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryCorePingDelegate.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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.telemetry; - -import android.content.Context; -import android.content.SharedPreferences; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.util.Log; -import org.mozilla.gecko.BrowserApp; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.GeckoSharedPrefs; -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.SearchCountMeasurements; -import org.mozilla.gecko.telemetry.measurements.SessionMeasurements; -import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.ThreadUtils; - -import java.io.IOException; - -/** - * An activity-lifecycle delegate for uploading the core ping. - */ -public class TelemetryCorePingDelegate extends BrowserAppDelegateWithReference - implements SearchEngineManager.SearchEngineCallback, AttributionHelperListener { - private static final String LOGTAG = StringUtils.safeSubstring( - "Gecko" + TelemetryCorePingDelegate.class.getSimpleName(), 0, 23); - - private static final String PREF_IS_FIRST_RUN = "telemetry-isFirstRun"; - - private TelemetryDispatcher telemetryDispatcher; // lazy - private final SessionMeasurements sessionMeasurements = new SessionMeasurements(); - - @Override - public void onStart(final BrowserApp browserApp) { - TelemetryPreferences.initPreferenceObserver(browserApp, browserApp.getProfile().getName()); - - // We don't upload in onCreate because that's only called when the Activity needs to be instantiated - // and it's possible the system will never free the Activity from memory. - // - // We don't upload in onResume/onPause because that will be called each time the Activity is obscured, - // including by our own Activities/dialogs, and there is no reason to upload each time we're unobscured. - // - // We're left with onStart/onStop and we upload in onStart because onStop is not guaranteed to be called - // and we want to upload the first run ASAP (e.g. to get install data before the app may crash). - uploadPing(browserApp); - } - - @Override - public void onStop(final BrowserApp browserApp) { - // We've decided to upload primarily in onStart (see note there). However, if it's the first run, - // it's possible a user used fennec and decided never to return to it again - it'd be great to get - // their session information before they decided to give it up so we upload here on first run. - // - // Caveats: - // * onStop is not guaranteed to be called in low memory conditions so it's possible we won't upload, - // but it's better than it was before. - // * Besides first run (because of this call), we can never get the user's *last* session data. - // - // If we are really interested in the user's last session data, we could consider uploading in onStop - // but it's less robust (see discussion in bug 1277091). - final SharedPreferences sharedPrefs = getSharedPreferences(browserApp); - if (sharedPrefs.getBoolean(PREF_IS_FIRST_RUN, true)) { - sharedPrefs.edit() - .putBoolean(PREF_IS_FIRST_RUN, false) - .apply(); - uploadPing(browserApp); - } - } - - private void uploadPing(final BrowserApp browserApp) { - final SearchEngineManager searchEngineManager = browserApp.getSearchEngineManager(); - searchEngineManager.getEngine(this); - } - - @Override - public void onResume(BrowserApp browserApp) { - sessionMeasurements.recordSessionStart(); - } - - @Override - public void onPause(BrowserApp browserApp) { - // onStart/onStop is ideal over onResume/onPause. However, onStop is not guaranteed to be called and - // dealing with that possibility adds a lot of complexity that we don't want to handle at this point. - sessionMeasurements.recordSessionEnd(browserApp); - } - - @WorkerThread // via constructor - private TelemetryDispatcher getTelemetryDispatcher(final BrowserApp browserApp) { - if (telemetryDispatcher == null) { - final GeckoProfile profile = browserApp.getProfile(); - final String profilePath = profile.getDir().getAbsolutePath(); - final String profileName = profile.getName(); - telemetryDispatcher = new TelemetryDispatcher(profilePath, profileName); - } - return telemetryDispatcher; - } - - private SharedPreferences getSharedPreferences(final BrowserApp activity) { - return GeckoSharedPrefs.forProfileName(activity, activity.getProfile().getName()); - } - - // via SearchEngineCallback - may be called from any thread. - @Override - public void execute(@Nullable final org.mozilla.gecko.search.SearchEngine engine) { - // Don't waste resources queueing to the background thread if we don't have a reference. - if (getBrowserApp() == null) { - return; - } - - // The containing method can be called from onStart: queue this work so that - // 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 = activity.getProfile(); - if (!TelemetryUploadService.isUploadEnabledByProfileConfig(activity, profile)) { - Log.d(LOGTAG, "Core ping upload disabled by profile config. Returning."); - return; - } - - final String clientID; - try { - clientID = profile.getClientId(); - } catch (final IOException e) { - Log.w(LOGTAG, "Unable to get client ID 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) - .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); - } - }); - } - - private void maybeSetOptionalMeasurements(final Context context, final SharedPreferences sharedPrefs, - final TelemetryCorePingBuilder pingBuilder) { - final String distributionId = sharedPrefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null); - if (distributionId != null) { - pingBuilder.setOptDistributionID(distributionId); - } - - final ExtendedJSONObject searchCounts = SearchCountMeasurements.getAndZeroSearch(sharedPrefs); - if (searchCounts.size() > 0) { - pingBuilder.setOptSearchCounts(searchCounts); - } - - final String campaignId = CampaignIdMeasurements.getCampaignIdFromPrefs(context); - if (campaignId != null) { - pingBuilder.setOptCampaignId(campaignId); - } - } - - @Override - public void onCampaignIdChanged(String campaignId) { - CampaignIdMeasurements.updateCampaignIdPref(getBrowserApp(), campaignId); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java deleted file mode 100644 index c702bb92c..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.telemetry; - -import android.content.Context; -import android.support.annotation.WorkerThread; -import android.util.Log; -import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder; -import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadScheduler; -import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadAllPingsImmediatelyScheduler; -import org.mozilla.gecko.telemetry.stores.TelemetryJSONFilePingStore; -import org.mozilla.gecko.telemetry.stores.TelemetryPingStore; -import org.mozilla.gecko.util.ThreadUtils; - -import java.io.File; -import java.io.IOException; - -/** - * The entry-point for Java-based telemetry. This class handles: - * * Initializing the Stores & Schedulers. - * * Queueing upload requests for a given ping. - * - * To test Telemetry , see {@link TelemetryConstants} & - * https://wiki.mozilla.org/Mobile/Fennec/Android/Java_telemetry. - * - * The full architecture is: - * - * Fennec -(PingBuilder)-> Dispatcher -2-> Scheduler -> UploadService - * | 1 | - * Store <-------------------------- - * - * The store acts as a single store of truth and contains a list of all - * pings waiting to be uploaded. The dispatcher will queue a ping to upload - * by writing it to the store. Later, the UploadService will try to upload - * this queued ping by reading directly from the store. - * - * To implement a new ping type, you should: - * 1) Implement a {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder} for your ping type. - * 2) Re-use a ping store in .../stores/ or implement a new one: {@link TelemetryPingStore}. The - * type of store may be affected by robustness requirements (e.g. do you have data in addition to - * pings that need to be atomically updated when a ping is stored?) and performance requirements. - * 3) Re-use an upload scheduler in .../schedulers/ or implement a new one: {@link TelemetryUploadScheduler}. - * 4) Initialize your Store & (if new) Scheduler in the constructor of this class - * 5) Add a queuePingForUpload method for your PingBuilder class (see - * {@link #queuePingForUpload(Context, TelemetryCorePingBuilder)}) - * 6) In Fennec, where you want to store a ping and attempt upload, create a PingBuilder and - * pass it to the new queuePingForUpload method. - */ -public class TelemetryDispatcher { - private static final String LOGTAG = "Gecko" + TelemetryDispatcher.class.getSimpleName(); - - private static final String STORE_CONTAINER_DIR_NAME = "telemetry_java"; - private static final String CORE_STORE_DIR_NAME = "core"; - - private final TelemetryJSONFilePingStore coreStore; - - private final TelemetryUploadAllPingsImmediatelyScheduler uploadAllPingsImmediatelyScheduler; - - @WorkerThread // via TelemetryJSONFilePingStore - public TelemetryDispatcher(final String profilePath, final String profileName) { - final String storePath = profilePath + File.separator + STORE_CONTAINER_DIR_NAME; - - // There are measurements in the core ping (e.g. seq #) that would ideally be atomically updated - // when the ping is stored. However, for simplicity, we use the json store and accept the possible - // loss of data (see bug 1243585 comment 16+ for more). - coreStore = new TelemetryJSONFilePingStore(new File(storePath, CORE_STORE_DIR_NAME), profileName); - - uploadAllPingsImmediatelyScheduler = new TelemetryUploadAllPingsImmediatelyScheduler(); - } - - private void queuePingForUpload(final Context context, final TelemetryPing ping, final TelemetryPingStore store, - final TelemetryUploadScheduler scheduler) { - final QueuePingRunnable runnable = new QueuePingRunnable(context, ping, store, scheduler); - ThreadUtils.postToBackgroundThread(runnable); // TODO: Investigate how busy this thread is. See if we want another. - } - - /** - * 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 TelemetryCorePingBuilder pingBuilder) { - final TelemetryPing ping = pingBuilder.build(); - queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler); - } - - private static class QueuePingRunnable implements Runnable { - private final Context applicationContext; - private final TelemetryPing ping; - private final TelemetryPingStore store; - private final TelemetryUploadScheduler scheduler; - - public QueuePingRunnable(final Context context, final TelemetryPing ping, final TelemetryPingStore store, - final TelemetryUploadScheduler scheduler) { - this.applicationContext = context.getApplicationContext(); - this.ping = ping; - this.store = store; - this.scheduler = scheduler; - } - - @Override - public void run() { - // We block while storing the ping so the scheduled upload is guaranteed to have the newly-stored value. - try { - store.storePing(ping); - } catch (final IOException e) { - // Don't log exception to avoid leaking profile path. - Log.e(LOGTAG, "Unable to write ping to disk. Continuing with upload attempt"); - } - - if (scheduler.isReadyToUpload(store)) { - scheduler.scheduleUpload(applicationContext, store); - } - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java deleted file mode 100644 index b6ee9c2d8..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java +++ /dev/null @@ -1,34 +0,0 @@ -/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.telemetry; - -import org.mozilla.gecko.sync.ExtendedJSONObject; - -/** - * Container for telemetry data and the data necessary to upload it. - * - * The doc ID is used by a Store to manipulate its internal pings and should - * be the same value found in the urlPath. - * - * If you want to create one of these, consider extending - * {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder} - * or one of its descendants. - */ -public class TelemetryPing { - private final String urlPath; - private final ExtendedJSONObject payload; - private final String docID; - - public TelemetryPing(final String urlPath, final ExtendedJSONObject payload, final String docID) { - this.urlPath = urlPath; - this.payload = payload; - this.docID = docID; - } - - public String getURLPath() { return urlPath; } - public ExtendedJSONObject getPayload() { return payload; } - public String getDocID() { return docID; } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java deleted file mode 100644 index 329f5b803..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPreferences.java +++ /dev/null @@ -1,73 +0,0 @@ -/* 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.telemetry; - -import android.content.Context; -import android.content.SharedPreferences; -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.PrefsHelper.PrefHandler; - -import java.lang.ref.WeakReference; - -/** - * Manages getting and setting any preferences related to telemetry. - * - * This class persists any Gecko preferences beyond shutdown so that these values - * can be accessed on the next run before Gecko is started as we expect Telemetry - * to run before Gecko is available. - */ -public class TelemetryPreferences { - private TelemetryPreferences() {} - - private static final String GECKO_PREF_SERVER_URL = "toolkit.telemetry.server"; - private static final String SHARED_PREF_SERVER_URL = "telemetry-serverUrl"; - - // Defaults are a mirror of about:config defaults so we can access them before Gecko is available. - private static final String DEFAULT_SERVER_URL = "https://incoming.telemetry.mozilla.org"; - - private static final String[] OBSERVED_PREFS = { - GECKO_PREF_SERVER_URL, - }; - - public static String getServerSchemeHostPort(final Context context, final String profileName) { - return getSharedPrefs(context, profileName).getString(SHARED_PREF_SERVER_URL, DEFAULT_SERVER_URL); - } - - public static void initPreferenceObserver(final Context context, final String profileName) { - final PrefHandler prefHandler = new TelemetryPrefHandler(context, profileName); - PrefsHelper.addObserver(OBSERVED_PREFS, prefHandler); // gets preference value when gecko starts. - } - - private static SharedPreferences getSharedPrefs(final Context context, final String profileName) { - return GeckoSharedPrefs.forProfileName(context, profileName); - } - - private static class TelemetryPrefHandler extends PrefsHelper.PrefHandlerBase { - private final WeakReference<Context> contextWeakReference; - private final String profileName; - - private TelemetryPrefHandler(final Context context, final String profileName) { - contextWeakReference = new WeakReference<>(context); - this.profileName = profileName; - } - - @Override - public void prefValue(final String pref, final String value) { - final Context context = contextWeakReference.get(); - if (context == null) { - return; - } - - if (!pref.equals(GECKO_PREF_SERVER_URL)) { - throw new IllegalStateException("Unknown preference: " + pref); - } - - getSharedPrefs(context, profileName).edit() - .putString(SHARED_PREF_SERVER_URL, value) - .apply(); - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java deleted file mode 100644 index 543281174..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java +++ /dev/null @@ -1,347 +0,0 @@ -/* 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.telemetry; - -import android.app.IntentService; -import android.content.Context; -import android.content.Intent; -import android.util.Log; -import ch.boye.httpclientandroidlib.HttpHeaders; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.ClientProtocolException; -import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; -import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.preferences.GeckoPreferences; -import org.mozilla.gecko.restrictions.Restrictable; -import org.mozilla.gecko.restrictions.Restrictions; -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.stores.TelemetryPingStore; -import org.mozilla.gecko.util.DateUtil; -import org.mozilla.gecko.util.NetworkUtils; -import org.mozilla.gecko.util.StringUtils; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.security.GeneralSecurityException; -import java.util.Calendar; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -/** - * The service that handles retrieving a list of telemetry pings to upload from the given - * {@link TelemetryPingStore}, uploading those payloads to the associated server, and reporting - * back to the Store which uploads were a success. - */ -public class TelemetryUploadService extends IntentService { - private static final String LOGTAG = StringUtils.safeSubstring("Gecko" + TelemetryUploadService.class.getSimpleName(), 0, 23); - private static final String WORKER_THREAD_NAME = LOGTAG + "Worker"; - - public static final String ACTION_UPLOAD = "upload"; - public static final String EXTRA_STORE = "store"; - - // TelemetryUploadService can run in a background thread so for future proofing, we set it volatile. - private static volatile boolean isDisabled = false; - - public static void setDisabled(final boolean isDisabled) { - TelemetryUploadService.isDisabled = isDisabled; - if (isDisabled) { - Log.d(LOGTAG, "Telemetry upload disabled (env var?"); - } - } - - public TelemetryUploadService() { - super(WORKER_THREAD_NAME); - - // Intent redelivery can fail hard (e.g. we OOM as we try to upload, the Intent gets redelivered, repeat) - // so for simplicity, we avoid it. We expect the upload service to eventually get called again by the caller. - setIntentRedelivery(false); - } - - /** - * Handles a ping with the mandatory extras: - * * EXTRA_STORE: A {@link TelemetryPingStore} where the pings to upload are located - */ - @Override - public void onHandleIntent(final Intent intent) { - Log.d(LOGTAG, "Service started"); - - if (!isReadyToUpload(this, intent)) { - return; - } - - final TelemetryPingStore store = intent.getParcelableExtra(EXTRA_STORE); - final boolean wereAllUploadsSuccessful = uploadPendingPingsFromStore(this, store); - store.maybePrunePings(); - Log.d(LOGTAG, "Service finished: upload and prune attempts completed"); - - if (!wereAllUploadsSuccessful) { - // If we had an upload failure, we should stop the IntentService and drop any - // pending Intents in the queue so we don't waste resources (e.g. battery) - // trying to upload when there's likely to be another connection failure. - Log.d(LOGTAG, "Clearing Intent queue due to connection failures"); - stopSelf(); - } - } - - /** - * @return true if all pings were uploaded successfully, false otherwise. - */ - private static boolean uploadPendingPingsFromStore(final Context context, final TelemetryPingStore store) { - final List<TelemetryPing> pingsToUpload = store.getAllPings(); - if (pingsToUpload.isEmpty()) { - return true; - } - - final String serverSchemeHostPort = TelemetryPreferences.getServerSchemeHostPort(context, store.getProfileName()); - final HashSet<String> successfulUploadIDs = new HashSet<>(pingsToUpload.size()); // used for side effects. - final PingResultDelegate delegate = new PingResultDelegate(successfulUploadIDs); - for (final TelemetryPing ping : pingsToUpload) { - // TODO: It'd be great to re-use the same HTTP connection for each upload request. - delegate.setDocID(ping.getDocID()); - final String url = serverSchemeHostPort + "/" + ping.getURLPath(); - uploadPayload(url, ping.getPayload(), delegate); - - // There are minimal gains in trying to upload if we already failed one attempt. - if (delegate.hadConnectionError()) { - break; - } - } - - final boolean wereAllUploadsSuccessful = !delegate.hadConnectionError(); - if (wereAllUploadsSuccessful) { - // We don't log individual successful uploads to avoid log spam. - Log.d(LOGTAG, "Telemetry upload success!"); - } - store.onUploadAttemptComplete(successfulUploadIDs); - return wereAllUploadsSuccessful; - } - - private static void uploadPayload(final String url, final ExtendedJSONObject payload, final ResultDelegate delegate) { - final BaseResource resource; - try { - resource = new BaseResource(url); - } catch (final URISyntaxException e) { - Log.w(LOGTAG, "URISyntaxException for server URL when creating BaseResource: returning."); - return; - } - - delegate.setResource(resource); - resource.delegate = delegate; - resource.setShouldCompressUploadedEntity(true); - resource.setShouldChunkUploadsHint(false); // Telemetry servers don't support chunking. - - // We're in a background thread so we don't have any reason to do this asynchronously. - // If we tried, onStartCommand would return and IntentService might stop itself before we finish. - resource.postBlocking(payload); - } - - private static boolean isReadyToUpload(final Context context, final Intent intent) { - // Sanity check: is upload enabled? Generally, the caller should check this before starting the service. - // Since we don't have the profile here, we rely on the caller to check the enabled state for the profile. - if (!isUploadEnabledByAppConfig(context)) { - Log.w(LOGTAG, "Upload is not available by configuration; returning"); - return false; - } - - if (!NetworkUtils.isConnected(context)) { - Log.w(LOGTAG, "Network is not connected; returning"); - return false; - } - - if (!isIntentValid(intent)) { - Log.w(LOGTAG, "Received invalid Intent; returning"); - return false; - } - - if (!ACTION_UPLOAD.equals(intent.getAction())) { - Log.w(LOGTAG, "Unknown action: " + intent.getAction() + ". Returning"); - return false; - } - - return true; - } - - /** - * Determines if the telemetry upload feature is enabled via the application configuration. Prefer to use - * {@link #isUploadEnabledByProfileConfig(Context, GeckoProfile)} if the profile is available as it takes into - * account more information. - * - * You may wish to also check if the network is connected when calling this method. - * - * Note that this method logs debug statements when upload is disabled. - */ - public static boolean isUploadEnabledByAppConfig(final Context context) { - if (!TelemetryConstants.UPLOAD_ENABLED) { - Log.d(LOGTAG, "Telemetry upload feature is compile-time disabled"); - return false; - } - - if (isDisabled) { - Log.d(LOGTAG, "Telemetry upload feature is disabled by intent (in testing?)"); - return false; - } - - if (!GeckoPreferences.getBooleanPref(context, GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true)) { - Log.d(LOGTAG, "Telemetry upload opt-out"); - return false; - } - - if (Restrictions.isRestrictedProfile(context) && - !Restrictions.isAllowed(context, Restrictable.HEALTH_REPORT)) { - Log.d(LOGTAG, "Telemetry upload feature disabled by admin profile"); - return false; - } - - return true; - } - - /** - * Determines if the telemetry upload feature is enabled via profile & application level configurations. This is the - * preferred method. - * - * You may wish to also check if the network is connected when calling this method. - * - * Note that this method logs debug statements when upload is disabled. - */ - public static boolean isUploadEnabledByProfileConfig(final Context context, final GeckoProfile profile) { - if (profile.inGuestMode()) { - Log.d(LOGTAG, "Profile is in guest mode"); - return false; - } - - return isUploadEnabledByAppConfig(context); - } - - private static boolean isIntentValid(final Intent intent) { - // Intent can be null. Bug 1025937. - if (intent == null) { - Log.d(LOGTAG, "Received null intent"); - return false; - } - - if (intent.getParcelableExtra(EXTRA_STORE) == null) { - Log.d(LOGTAG, "Received invalid store in Intent"); - return false; - } - - return true; - } - - /** - * Logs on success & failure and appends the set ID to the given Set on success. - * - * Note: you *must* set the ping ID before attempting upload or we'll throw! - * - * We use mutation on the set ID and the successful upload array to avoid object allocation. - */ - private static class PingResultDelegate extends ResultDelegate { - // We persist pings and don't need to worry about losing data so we keep these - // durations short to save resources (e.g. battery). - private static final int SOCKET_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(30); - private static final int CONNECTION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(30); - - /** The store ID of the ping currently being uploaded. Use {@link #getDocID()} to access it. */ - private String docID = null; - private final Set<String> successfulUploadIDs; - - private boolean hadConnectionError = false; - - public PingResultDelegate(final Set<String> successfulUploadIDs) { - super(); - this.successfulUploadIDs = successfulUploadIDs; - } - - @Override - public int socketTimeout() { - return SOCKET_TIMEOUT_MILLIS; - } - - @Override - public int connectionTimeout() { - return CONNECTION_TIMEOUT_MILLIS; - } - - private String getDocID() { - if (docID == null) { - throw new IllegalStateException("Expected ping ID to have been updated before retrieval"); - } - return docID; - } - - public void setDocID(final String id) { - docID = id; - } - - @Override - public String getUserAgent() { - return TelemetryConstants.USER_AGENT; - } - - @Override - public void handleHttpResponse(final HttpResponse response) { - final int status = response.getStatusLine().getStatusCode(); - switch (status) { - case 200: - case 201: - successfulUploadIDs.add(getDocID()); - break; - default: - Log.w(LOGTAG, "Telemetry upload failure. HTTP status: " + status); - hadConnectionError = true; - } - } - - @Override - public void handleHttpProtocolException(final ClientProtocolException e) { - // We don't log the exception to prevent leaking user data. - Log.w(LOGTAG, "HttpProtocolException when trying to upload telemetry"); - hadConnectionError = true; - } - - @Override - public void handleHttpIOException(final IOException e) { - // We don't log the exception to prevent leaking user data. - Log.w(LOGTAG, "HttpIOException when trying to upload telemetry"); - hadConnectionError = true; - } - - @Override - public void handleTransportException(final GeneralSecurityException e) { - // We don't log the exception to prevent leaking user data. - Log.w(LOGTAG, "Transport exception when trying to upload telemetry"); - hadConnectionError = true; - } - - private boolean hadConnectionError() { - return hadConnectionError; - } - - @Override - public void addHeaders(final HttpRequestBase request, final DefaultHttpClient client) { - super.addHeaders(request, client); - request.addHeader(HttpHeaders.DATE, DateUtil.getDateInHTTPFormat(Calendar.getInstance().getTime())); - } - } - - /** - * A hack because I want to set the resource after the Delegate is constructed. - * Be sure to call {@link #setResource(Resource)}! - */ - private static abstract class ResultDelegate extends BaseResourceDelegate { - public ResultDelegate() { - super(null); - } - - protected void setResource(final Resource resource) { - this.resource = resource; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java deleted file mode 100644 index 61229b21b..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/CampaignIdMeasurements.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * 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.telemetry.measurements; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.text.TextUtils; - -import org.mozilla.gecko.GeckoSharedPrefs; -import org.mozilla.gecko.adjust.AttributionHelperListener; - -/** - * A class to retrieve and store the campaign Id pref that is used when the Adjust SDK gives us - * new attribution from the {@link AttributionHelperListener}. - */ -public class CampaignIdMeasurements { - private static final String PREF_CAMPAIGN_ID = "measurements-campaignId"; - - public static String getCampaignIdFromPrefs(@NonNull final Context context) { - return GeckoSharedPrefs.forProfile(context) - .getString(PREF_CAMPAIGN_ID, null); - } - - public static void updateCampaignIdPref(@NonNull final Context context, @NonNull final String campaignId) { - if (TextUtils.isEmpty(campaignId)) { - return; - } - GeckoSharedPrefs.forProfile(context) - .edit() - .putString(PREF_CAMPAIGN_ID, campaignId) - .apply(); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java deleted file mode 100644 index c08ad6c02..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.telemetry.measurements; - -import android.content.SharedPreferences; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import org.mozilla.gecko.sync.ExtendedJSONObject; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * A place to store and retrieve the number of times a user has searched with a specific engine from a - * specific location. This is designed for use as a telemetry core ping measurement. - * - * The implementation works by storing a preference for each engine-location pair and incrementing them - * each time {@link #incrementSearch(SharedPreferences, String, String)} is called. In order to - * retrieve the full set of keys later, we store all the available key names in another preference. - * - * When we retrieve the keys in {@link #getAndZeroSearch(SharedPreferences)} (using the set of keys - * preference), the values saved to the preferences are returned and the preferences are removed - * (i.e. zeroed) from Shared Preferences. The reason we remove the preferences (rather than actually - * zeroing them) is to avoid bloating shared preferences if 1) the set of engines ever changes or - * 2) we remove this feature. - * - * Since we increment a value on each successive search, which doesn't take up more space, we don't - * have to worry about using excess disk space if the measurements are never zeroed (e.g. telemetry - * upload is disabled). In the worst case, we overflow the integer and may return negative values. - * - * This class is thread-safe by locking access to its public methods. When this class was written, incrementing & - * retrieval were called from multiple threads so rather than enforcing the callers keep their threads straight, it - * was simpler to lock all access. - */ -public class SearchCountMeasurements { - /** The set of "engine + where" keys we've stored; used for retrieving stored engines. */ - @VisibleForTesting static final String PREF_SEARCH_KEYSET = "measurements-search-count-keyset"; - private static final String PREF_SEARCH_PREFIX = "measurements-search-count-engine-"; // + "engine.where" - - private SearchCountMeasurements() {} - - public static synchronized void incrementSearch(@NonNull final SharedPreferences prefs, - @NonNull final String engineIdentifier, @NonNull final String where) { - final String engineWhereStr = engineIdentifier + "." + where; - final String key = getEngineSearchCountKey(engineWhereStr); - - final int count = prefs.getInt(key, 0); - prefs.edit().putInt(key, count + 1).apply(); - - unionKeyToSearchKeyset(prefs, engineWhereStr); - } - - /** - * @param key Engine of the form, "engine.where" - */ - private static void unionKeyToSearchKeyset(@NonNull final SharedPreferences prefs, @NonNull final String key) { - final Set<String> keysFromPrefs = prefs.getStringSet(PREF_SEARCH_KEYSET, Collections.<String>emptySet()); - if (keysFromPrefs.contains(key)) { - return; - } - - // String set returned by shared prefs cannot be modified so we copy. - final Set<String> keysToSave = new HashSet<>(keysFromPrefs); - keysToSave.add(key); - prefs.edit().putStringSet(PREF_SEARCH_KEYSET, keysToSave).apply(); - } - - /** - * Gets and zeroes search counts. - * - * We return ExtendedJSONObject for now because that's the format needed by the core telemetry ping. - */ - public static synchronized ExtendedJSONObject getAndZeroSearch(@NonNull final SharedPreferences prefs) { - final ExtendedJSONObject out = new ExtendedJSONObject(); - final SharedPreferences.Editor editor = prefs.edit(); - - final Set<String> keysFromPrefs = prefs.getStringSet(PREF_SEARCH_KEYSET, Collections.<String>emptySet()); - for (final String engineWhereStr : keysFromPrefs) { - final String key = getEngineSearchCountKey(engineWhereStr); - out.put(engineWhereStr, prefs.getInt(key, 0)); - editor.remove(key); - } - editor.remove(PREF_SEARCH_KEYSET) - .apply(); - return out; - } - - /** - * @param engineWhereStr string of the form "engine.where" - * @return the key for the engines' search counts in shared preferences - */ - @VisibleForTesting static String getEngineSearchCountKey(final String engineWhereStr) { - return PREF_SEARCH_PREFIX + engineWhereStr; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java deleted file mode 100644 index 6f7d2127a..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * 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.telemetry.measurements; - -import android.content.Context; -import android.content.SharedPreferences; -import android.support.annotation.UiThread; -import android.support.annotation.VisibleForTesting; -import org.mozilla.gecko.GeckoSharedPrefs; - -import java.util.concurrent.TimeUnit; - -/** - * A class to measure the number of user sessions & their durations. It was created for use with the - * telemetry core ping. A session is the time between {@link #recordSessionStart()} and - * {@link #recordSessionEnd(Context)}. - * - * This class is thread-safe, provided the thread annotations are followed. Under the hood, this class uses - * SharedPreferences & because there is no atomic getAndSet operation, we synchronize access to it. - */ -public class SessionMeasurements { - @VisibleForTesting static final String PREF_SESSION_COUNT = "measurements-session-count"; - @VisibleForTesting static final String PREF_SESSION_DURATION = "measurements-session-duration"; - - private boolean sessionStarted = false; - private long timeAtSessionStartNano = -1; - - @UiThread // we assume this will be called on the same thread as session end so we don't have to synchronize sessionStarted. - public void recordSessionStart() { - if (sessionStarted) { - throw new IllegalStateException("Trying to start session but it is already started"); - } - sessionStarted = true; - timeAtSessionStartNano = getSystemTimeNano(); - } - - @UiThread // we assume this will be called on the same thread as session start so we don't have to synchronize sessionStarted. - public void recordSessionEnd(final Context context) { - if (!sessionStarted) { - throw new IllegalStateException("Expected session to be started before session end is called"); - } - sessionStarted = false; - - final long sessionElapsedSeconds = TimeUnit.NANOSECONDS.toSeconds(getSystemTimeNano() - timeAtSessionStartNano); - final SharedPreferences sharedPrefs = getSharedPreferences(context); - synchronized (this) { - final int sessionCount = sharedPrefs.getInt(PREF_SESSION_COUNT, 0); - final long totalElapsedSeconds = sharedPrefs.getLong(PREF_SESSION_DURATION, 0); - sharedPrefs.edit() - .putInt(PREF_SESSION_COUNT, sessionCount + 1) - .putLong(PREF_SESSION_DURATION, totalElapsedSeconds + sessionElapsedSeconds) - .apply(); - } - } - - /** - * Gets the session measurements since the last time the measurements were last retrieved. - */ - public synchronized SessionMeasurementsContainer getAndResetSessionMeasurements(final Context context) { - final SharedPreferences sharedPrefs = getSharedPreferences(context); - final int sessionCount = sharedPrefs.getInt(PREF_SESSION_COUNT, 0); - final long totalElapsedSeconds = sharedPrefs.getLong(PREF_SESSION_DURATION, 0); - sharedPrefs.edit() - .putInt(PREF_SESSION_COUNT, 0) - .putLong(PREF_SESSION_DURATION, 0) - .apply(); - return new SessionMeasurementsContainer(sessionCount, totalElapsedSeconds); - } - - @VisibleForTesting SharedPreferences getSharedPreferences(final Context context) { - return GeckoSharedPrefs.forProfile(context); - } - - /** - * Returns (roughly) the system uptime in nanoseconds. A less coupled implementation would - * take this value from the caller of recordSession*, however, we do this internally to ensure - * the caller uses both a time system consistent between the start & end calls and uses the - * appropriate time system (i.e. not wall time, which can change when the clock is changed). - */ - @VisibleForTesting long getSystemTimeNano() { // TODO: necessary? - return System.nanoTime(); - } - - public static final class SessionMeasurementsContainer { - /** The number of sessions. */ - public final int sessionCount; - /** The number of seconds elapsed in ALL sessions included in {@link #sessionCount}. */ - public final long elapsedSeconds; - - private SessionMeasurementsContainer(final int sessionCount, final long elapsedSeconds) { - this.sessionCount = sessionCount; - this.elapsedSeconds = elapsedSeconds; - } - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java deleted file mode 100644 index 3f5480f37..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * 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.telemetry.pingbuilders; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Build; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.text.TextUtils; - -import android.util.Log; -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.GeckoProfile; -import org.mozilla.gecko.Locales; -import org.mozilla.gecko.search.SearchEngine; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.telemetry.TelemetryPing; -import org.mozilla.gecko.util.DateUtil; -import org.mozilla.gecko.Experiments; -import org.mozilla.gecko.util.StringUtils; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Locale; -import java.util.concurrent.TimeUnit; - -/** - * Builds a {@link TelemetryPing} representing a core ping. - * - * See https://gecko.readthedocs.org/en/latest/toolkit/components/telemetry/telemetry/core-ping.html - * for details on the core ping. - */ -public class TelemetryCorePingBuilder extends TelemetryPingBuilder { - private static final String LOGTAG = StringUtils.safeSubstring(TelemetryCorePingBuilder.class.getSimpleName(), 0, 23); - - // For legacy reasons, this preference key is not namespaced with "core". - private static final String PREF_SEQ_COUNT = "telemetry-seqCount"; - - private static final String NAME = "core"; - private static final int VERSION_VALUE = 7; // For version history, see toolkit/components/telemetry/docs/core-ping.rst - private static final String OS_VALUE = "Android"; - - private static final String ARCHITECTURE = "arch"; - private static final String CAMPAIGN_ID = "campaignId"; - private static final String CLIENT_ID = "clientId"; - private static final String DEFAULT_SEARCH_ENGINE = "defaultSearch"; - private static final String DEVICE = "device"; - private static final String DISTRIBUTION_ID = "distributionId"; - private static final String EXPERIMENTS = "experiments"; - 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 PROFILE_CREATION_DATE = "profileDate"; - private static final String SEARCH_COUNTS = "searches"; - private static final String SEQ = "seq"; - private static final String SESSION_COUNT = "sessions"; - private static final String SESSION_DURATION = "durations"; - private static final String TIMEZONE_OFFSET = "tz"; - private static final String VERSION_ATTR = "v"; - - public TelemetryCorePingBuilder(final Context context) { - initPayloadConstants(context); - } - - private void initPayloadConstants(final Context context) { - payload.put(VERSION_ATTR, VERSION_VALUE); - payload.put(OS_ATTR, OS_VALUE); - - // We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the - // manufacturer because we're less likely to have manufacturers with similar names than we are for a - // manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6). - final String deviceDescriptor = - StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19); - - final Calendar nowCalendar = Calendar.getInstance(); - final DateFormat pingCreationDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.US); - - payload.put(ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH); - payload.put(DEVICE, deviceDescriptor); - payload.put(LOCALE, Locales.getLanguageTag(Locale.getDefault())); - payload.put(OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons. - payload.put(PING_CREATION_DATE, pingCreationDateFormat.format(nowCalendar.getTime())); - payload.put(TIMEZONE_OFFSET, DateUtil.getTimezoneOffsetInMinutesForGivenDate(nowCalendar)); - payload.putArray(EXPERIMENTS, Experiments.getActiveExperiments(context)); - } - - @Override - public String getDocType() { - return NAME; - } - - @Override - public String[] getMandatoryFields() { - return new String[] { - ARCHITECTURE, - CLIENT_ID, - DEFAULT_SEARCH_ENGINE, - DEVICE, - LOCALE, - OS_ATTR, - OS_VERSION, - PING_CREATION_DATE, - PROFILE_CREATION_DATE, - SEQ, - TIMEZONE_OFFSET, - VERSION_ATTR, - }; - } - - public TelemetryCorePingBuilder setClientID(@NonNull final String clientID) { - if (clientID == null) { - throw new IllegalArgumentException("Expected non-null clientID"); - } - payload.put(CLIENT_ID, clientID); - return this; - } - - /** - * @param engine the default search engine identifier, or null if there is an error. - */ - public TelemetryCorePingBuilder setDefaultSearchEngine(@Nullable final String engine) { - if (engine != null && engine.isEmpty()) { - throw new IllegalArgumentException("Received empty string. Expected identifier or null."); - } - payload.put(DEFAULT_SEARCH_ENGINE, engine); - return this; - } - - public TelemetryCorePingBuilder setOptDistributionID(@NonNull final String distributionID) { - if (distributionID == null) { - throw new IllegalArgumentException("Expected non-null distribution ID"); - } - payload.put(DISTRIBUTION_ID, distributionID); - return this; - } - - /** - * @param searchCounts non-empty JSON with {"engine.where": <int-count>} - */ - public TelemetryCorePingBuilder setOptSearchCounts(@NonNull final ExtendedJSONObject searchCounts) { - if (searchCounts == null) { - throw new IllegalStateException("Expected non-null search counts"); - } else if (searchCounts.size() == 0) { - throw new IllegalStateException("Expected non-empty search counts"); - } - - payload.put(SEARCH_COUNTS, searchCounts); - return this; - } - - public TelemetryCorePingBuilder setOptCampaignId(final String campaignId) { - if (campaignId == null) { - throw new IllegalStateException("Expected non-null campaign ID."); - } - payload.put(CAMPAIGN_ID, campaignId); - return this; - } - - /** - * @param date The profile creation date in days to the unix epoch (not millis!), or null if there is an error. - */ - public TelemetryCorePingBuilder setProfileCreationDate(@Nullable final Long date) { - if (date != null && date < 0) { - throw new IllegalArgumentException("Expect positive date value. Received: " + date); - } - payload.put(PROFILE_CREATION_DATE, date); - return this; - } - - /** - * @param seq a positive sequence number. - */ - public TelemetryCorePingBuilder setSequenceNumber(final int seq) { - if (seq < 0) { - // Since this is an increasing value, it's possible we can overflow into negative values and get into a - // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server. - Log.w(LOGTAG, "Expected positive sequence number. Received: " + seq); - } - payload.put(SEQ, seq); - return this; - } - - public TelemetryCorePingBuilder setSessionCount(final int sessionCount) { - if (sessionCount < 0) { - // Since this is an increasing value, it's possible we can overflow into negative values and get into a - // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server. - Log.w(LOGTAG, "Expected positive session count. Received: " + sessionCount); - } - payload.put(SESSION_COUNT, sessionCount); - return this; - } - - public TelemetryCorePingBuilder setSessionDuration(final long sessionDuration) { - if (sessionDuration < 0) { - // Since this is an increasing value, it's possible we can overflow into negative values and get into a - // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server. - Log.w(LOGTAG, "Expected positive session duration. Received: " + sessionDuration); - } - payload.put(SESSION_DURATION, sessionDuration); - return this; - } - - /** - * Gets the sequence number from shared preferences and increments it in the prefs. This method - * is not thread safe. - */ - @WorkerThread // synchronous shared prefs write. - public static int getAndIncrementSequenceNumber(final SharedPreferences sharedPrefsForProfile) { - final int seq = sharedPrefsForProfile.getInt(PREF_SEQ_COUNT, 1); - - sharedPrefsForProfile.edit().putInt(PREF_SEQ_COUNT, seq + 1).apply(); - return seq; - } - - /** - * @return the profile creation date in the format expected by - * {@link TelemetryCorePingBuilder#setProfileCreationDate(Long)}. - */ - @WorkerThread - public static Long getProfileCreationDate(final Context context, final GeckoProfile profile) { - final long profileMillis = profile.getAndPersistProfileCreationDate(context); - if (profileMillis < 0) { - return null; - } - return (long) Math.floor((double) profileMillis / TimeUnit.DAYS.toMillis(1)); - } - - /** - * @return the search engine identifier in the format expected by the core ping. - */ - @Nullable - public static String getEngineIdentifier(@Nullable final SearchEngine searchEngine) { - if (searchEngine == null) { - return null; - } - final String identifier = searchEngine.getIdentifier(); - return TextUtils.isEmpty(identifier) ? null : identifier; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java deleted file mode 100644 index 57fa0fd8b..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryPingBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.telemetry.pingbuilders; - -import org.mozilla.gecko.AppConstants; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.telemetry.TelemetryPing; - -import java.util.Set; -import java.util.UUID; - -/** - * A generic Builder for {@link TelemetryPing} instances. Each overriding class is - * expected to create a specific type of ping (e.g. "core"). - * - * This base class handles the common ping operations under the hood: - * * Validating mandatory fields - * * Forming the server url - */ -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"; - - private final String serverPath; - protected final ExtendedJSONObject payload; - private final String docID; - - public TelemetryPingBuilder() { - docID = UUID.randomUUID().toString(); - serverPath = getTelemetryServerPath(getDocType(), docID); - payload = new ExtendedJSONObject(); - } - - /** - * @return the name of the ping (e.g. "core") - */ - public abstract String getDocType(); - - /** - * @return the fields that are mandatory for the resultant ping to be uploaded to - * the server. These will be validated before the ping is built. - */ - public abstract String[] getMandatoryFields(); - - public TelemetryPing build() { - validatePayload(); - return new TelemetryPing(serverPath, payload, docID); - } - - private void validatePayload() { - final Set<String> keySet = payload.keySet(); - for (final String mandatoryField : getMandatoryFields()) { - if (!keySet.contains(mandatoryField)) { - throw new IllegalArgumentException("Builder does not contain mandatory field: " + - mandatoryField); - } - } - } - - /** - * Returns a url of the format: - * http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID - * - * @param docType The name of the ping (e.g. "main") - * @return a url at which to POST the telemetry data to - */ - private static String getTelemetryServerPath(final String docType, final String docID) { - final String appName = AppConstants.MOZ_APP_BASENAME; - final String appVersion = AppConstants.MOZ_APP_VERSION; - final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL; - final String appBuildId = AppConstants.MOZ_APP_BUILDID; - - // The compiler will optimize a single String concatenation into a StringBuilder statement. - // If you change this `return`, be sure to keep it as a single statement to keep it optimized! - return SERVER_INITIAL_PATH + '/' + - docID + '/' + - docType + '/' + - appName + '/' + - appVersion + '/' + - appUpdateChannel + '/' + - appBuildId; - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java deleted file mode 100644 index 047a646c3..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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.telemetry.schedulers; - -import android.content.Context; -import android.content.Intent; -import org.mozilla.gecko.telemetry.stores.TelemetryPingStore; -import org.mozilla.gecko.telemetry.TelemetryUploadService; - -/** - * Schedules an upload with all pings to be sent immediately. - */ -public class TelemetryUploadAllPingsImmediatelyScheduler implements TelemetryUploadScheduler { - - @Override - public boolean isReadyToUpload(final TelemetryPingStore store) { - // We're ready since we don't have any conditions to wait on (e.g. on wifi, accumulated X pings). - return true; - } - - @Override - public void scheduleUpload(final Context applicationContext, final TelemetryPingStore store) { - final Intent i = new Intent(TelemetryUploadService.ACTION_UPLOAD); - i.setClass(applicationContext, TelemetryUploadService.class); - i.putExtra(TelemetryUploadService.EXTRA_STORE, store); - applicationContext.startService(i); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java deleted file mode 100644 index 63305aad5..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/schedulers/TelemetryUploadScheduler.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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.telemetry.schedulers; - -import android.content.Context; -import org.mozilla.gecko.telemetry.stores.TelemetryPingStore; - -/** - * An implementation of this class can investigate the given {@link TelemetryPingStore} to - * decide if it's ready to upload the pings inside that Store (e.g. on wifi? have we - * accumulated X pings?) and can schedule that upload. Typically, the upload will be - * scheduled by sending an {@link android.content.Intent} to the - * {@link org.mozilla.gecko.telemetry.TelemetryUploadService}, either immediately or - * via an external scheduler (e.g. {@link android.app.job.JobScheduler}). - * - * N.B.: If the Store is not ready to upload, an implementation *should not* try to reschedule - * the check to see if it's time to upload - this is expected to be handled by the caller. - */ -public interface TelemetryUploadScheduler { - boolean isReadyToUpload(TelemetryPingStore store); - void scheduleUpload(Context applicationContext, TelemetryPingStore store); -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java deleted file mode 100644 index d52382146..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * 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.telemetry.stores; - -import android.os.Parcel; -import android.os.Parcelable; -import android.support.annotation.VisibleForTesting; -import android.support.annotation.WorkerThread; -import android.util.Log; -import org.json.JSONException; -import org.json.JSONObject; -import org.mozilla.gecko.sync.ExtendedJSONObject; -import org.mozilla.gecko.sync.NonObjectJSONException; -import org.mozilla.gecko.telemetry.TelemetryPing; -import org.mozilla.gecko.util.FileUtils; -import org.mozilla.gecko.util.FileUtils.FileLastModifiedComparator; -import org.mozilla.gecko.util.FileUtils.FilenameRegexFilter; -import org.mozilla.gecko.util.FileUtils.FilenameWhitelistFilter; -import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.UUIDUtil; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FilenameFilter; -import java.io.IOException; -import java.nio.channels.FileLock; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; - -/** - * An implementation of TelemetryPingStore that is backed by JSON files. - * - * This implementation seeks simplicity. Each ping to upload is stored in its own file with its doc ID - * as the filename. The doc ID is sent with a ping to be uploaded and is expected to be returned with - * {@link #onUploadAttemptComplete(Set)} so the associated file can be removed. - * - * During prune, the pings with the oldest modified time will be removed first. Different filesystems will - * handle clock skew (e.g. manual time changes, daylight savings time, changing timezones) in different ways - * and we accept that these modified times may not be consistent - newer data is not more important than - * older data and the choice to delete the oldest data first is largely arbitrary so we don't care if - * the timestamps are occasionally inconsistent. - * - * Using separate files for this store allows for less restrictive concurrency: - * * requires locking: {@link #storePing(TelemetryPing)} writes a new file - * * requires locking: {@link #getAllPings()} reads all files, including those potentially being written, - * hence locking - * * no locking: {@link #maybePrunePings()} deletes the least recently written pings, none of which should - * be currently written - * * no locking: {@link #onUploadAttemptComplete(Set)} deletes the given pings, none of which should be - * currently written - */ -public class TelemetryJSONFilePingStore extends TelemetryPingStore { - private static final String LOGTAG = StringUtils.safeSubstring( - "Gecko" + TelemetryJSONFilePingStore.class.getSimpleName(), 0, 23); - - @VisibleForTesting static final int MAX_PING_COUNT = 40; // TODO: value. - - // We keep the key names short to reduce storage size impact. - @VisibleForTesting static final String KEY_PAYLOAD = "p"; - @VisibleForTesting static final String KEY_URL_PATH = "u"; - - private final File storeDir; - private final FilenameFilter uuidFilenameFilter; - private final FileLastModifiedComparator fileLastModifiedComparator = new FileLastModifiedComparator(); - - @WorkerThread // Writes to disk - public TelemetryJSONFilePingStore(final File storeDir, final String profileName) { - super(profileName); - if (storeDir.exists() && !storeDir.isDirectory()) { - // An alternative is to create a new directory, but we wouldn't - // be able to access it later so it's better to throw. - throw new IllegalStateException("Store dir unexpectedly exists & is not a directory - cannot continue"); - } - - this.storeDir = storeDir; - this.storeDir.mkdirs(); - uuidFilenameFilter = new FilenameRegexFilter(UUIDUtil.UUID_PATTERN); - - if (!this.storeDir.canRead() || !this.storeDir.canWrite() || !this.storeDir.canExecute()) { - throw new IllegalStateException("Cannot read, write, or execute store dir: " + - this.storeDir.canRead() + " " + this.storeDir.canWrite() + " " + this.storeDir.canExecute()); - } - } - - @VisibleForTesting File getPingFile(final String docID) { - return new File(storeDir, docID); - } - - @Override - public void storePing(final TelemetryPing ping) throws IOException { - final String output; - try { - output = new JSONObject() - .put(KEY_PAYLOAD, ping.getPayload()) - .put(KEY_URL_PATH, ping.getURLPath()) - .toString(); - } catch (final JSONException e) { - // Do not log the exception to avoid leaking personal data. - throw new IOException("Unable to create JSON to store to disk"); - } - - final FileOutputStream outputStream = new FileOutputStream(getPingFile(ping.getDocID()), false); - blockForLockAndWriteFileAndCloseStream(outputStream, output); - } - - @Override - public void maybePrunePings() { - final File[] files = storeDir.listFiles(uuidFilenameFilter); - if (files == null) { - return; - } - - if (files.length < MAX_PING_COUNT) { - return; - } - - // It's possible that multiple files will have the same timestamp: in this case they are treated - // as equal by the fileLastModifiedComparator. We therefore have to use a sorted list (as - // opposed to a set, or map). - final ArrayList<File> sortedFiles = new ArrayList<>(Arrays.asList(files)); - Collections.sort(sortedFiles, fileLastModifiedComparator); - deleteSmallestFiles(sortedFiles, files.length - MAX_PING_COUNT); - } - - private void deleteSmallestFiles(final ArrayList<File> files, final int numFilesToRemove) { - final Iterator<File> it = files.iterator(); - int i = 0; - - while (i < numFilesToRemove) { - i += 1; - - // Sorted list so we're iterating over ascending files. - final File file = it.next(); // file count > files to remove so this should not throw. - file.delete(); - } - } - - @Override - public ArrayList<TelemetryPing> getAllPings() { - final File[] fileArray = storeDir.listFiles(uuidFilenameFilter); - if (fileArray == null) { - // Intentionally don't log all info for the store directory to prevent leaking the path. - Log.w(LOGTAG, "listFiles unexpectedly returned null - unable to retrieve pings. Debug: exists? " + - storeDir.exists() + "; directory? " + storeDir.isDirectory()); - return new ArrayList<>(1); - } - - final List<File> files = Arrays.asList(fileArray); - Collections.sort(files, fileLastModifiedComparator); // oldest to newest - final ArrayList<TelemetryPing> out = new ArrayList<>(files.size()); - for (final File file : files) { - final JSONObject obj = lockAndReadJSONFromFile(file); - if (obj == null) { - // We log in the method to get the JSONObject if we return null. - continue; - } - - try { - final String url = obj.getString(KEY_URL_PATH); - final ExtendedJSONObject payload = new ExtendedJSONObject(obj.getString(KEY_PAYLOAD)); - out.add(new TelemetryPing(url, payload, file.getName())); - } catch (final IOException | JSONException | NonObjectJSONException e) { - Log.w(LOGTAG, "Bad json in ping. Ignoring."); - continue; - } - } - return out; - } - - /** - * Logs if there is an error. - * - * @return the JSON object from the given file or null if there is an error. - */ - private JSONObject lockAndReadJSONFromFile(final File file) { - // lockAndReadFileAndCloseStream doesn't handle file size of 0. - if (file.length() == 0) { - Log.w(LOGTAG, "Unexpected empty file: " + file.getName() + ". Ignoring"); - return null; - } - - final FileInputStream inputStream; - try { - inputStream = new FileInputStream(file); - } catch (final FileNotFoundException e) { - // permission problem might also cause same exception. To get more debug information. - String fileInfo = String.format("existence: %b, can write: %b, size: %d.", - file.exists(), file.canWrite(), file.length()); - String msg = String.format( - "Expected file to exist but got exception in thread: %s. File info - %s", - Thread.currentThread().getName(), fileInfo); - throw new IllegalStateException(msg); - } - - final JSONObject obj; - try { - // Potential optimization: re-use the same buffer for reading from files. - obj = lockAndReadFileAndCloseStream(inputStream, (int) file.length()); - } catch (final IOException | JSONException e) { - // We couldn't read this file so let's just skip it. These potentially - // corrupted files should be removed when the data is pruned. - Log.w(LOGTAG, "Error when reading file: " + file.getName() + " Likely corrupted. Ignoring"); - return null; - } - - if (obj == null) { - Log.d(LOGTAG, "Could not read given file: " + file.getName() + " File is locked. Ignoring"); - } - return obj; - } - - @Override - public void onUploadAttemptComplete(final Set<String> successfulRemoveIDs) { - if (successfulRemoveIDs.isEmpty()) { - return; - } - - final File[] files = storeDir.listFiles(new FilenameWhitelistFilter(successfulRemoveIDs)); - for (final File file : files) { - file.delete(); - } - } - - /** - * Locks the given {@link FileOutputStream} and writes the given String. This method will close the given stream. - * - * Note: this method blocks until a file lock can be acquired. - */ - private static void blockForLockAndWriteFileAndCloseStream(final FileOutputStream outputStream, final String str) - throws IOException { - try { - final FileLock lock = outputStream.getChannel().lock(0, Long.MAX_VALUE, false); - if (lock != null) { - // The file lock is released when the stream is closed. If we try to redundantly close it, we get - // a ClosedChannelException. To be safe, we could catch that every time but there is a performance - // hit to exception handling so instead we assume the file lock will be closed. - FileUtils.writeStringToOutputStreamAndCloseStream(outputStream, str); - } - } finally { - outputStream.close(); // redundant: closed when the stream is closed, but let's be safe. - } - } - - /** - * Locks the given {@link FileInputStream} and reads the data. This method will close the given stream. - * - * Note: this method returns null when a lock could not be acquired. - */ - private static JSONObject lockAndReadFileAndCloseStream(final FileInputStream inputStream, final int fileSize) - throws IOException, JSONException { - try { - final FileLock lock = inputStream.getChannel().tryLock(0, Long.MAX_VALUE, true); // null when lock not acquired - if (lock == null) { - return null; - } - // The file lock is released when the stream is closed. If we try to redundantly close it, we get - // a ClosedChannelException. To be safe, we could catch that every time but there is a performance - // hit to exception handling so instead we assume the file lock will be closed. - return new JSONObject(FileUtils.readStringFromInputStreamAndCloseStream(inputStream, fileSize)); - } finally { - inputStream.close(); // redundant: closed when the stream is closed, but let's be safe. - } - } - - public static final Parcelable.Creator<TelemetryJSONFilePingStore> CREATOR = new Parcelable.Creator<TelemetryJSONFilePingStore>() { - @Override - public TelemetryJSONFilePingStore createFromParcel(final Parcel source) { - final String storeDirPath = source.readString(); - final String profileName = source.readString(); - return new TelemetryJSONFilePingStore(new File(storeDirPath), profileName); - } - - @Override - public TelemetryJSONFilePingStore[] newArray(final int size) { - return new TelemetryJSONFilePingStore[size]; - } - }; - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(final Parcel dest, final int flags) { - dest.writeString(storeDir.getAbsolutePath()); - dest.writeString(getProfileName()); - } -} diff --git a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java deleted file mode 100644 index 7d781cf26..000000000 --- a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryPingStore.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.telemetry.stores; - -import android.os.Parcelable; -import org.mozilla.gecko.telemetry.TelemetryPing; - -import java.io.IOException; -import java.util.List; -import java.util.Set; - -/** - * Persistent storage for TelemetryPings that are queued for upload. - * - * An implementation of this class is expected to be thread-safe. Additionally, - * multiple instances can be created and run simultaneously so they must be able - * to synchronize state (or be stateless!). - * - * The pings in {@link #getAllPings()} and {@link #maybePrunePings()} are returned in the - * same order in order to guarantee consistent results. - */ -public abstract class TelemetryPingStore implements Parcelable { - private final String profileName; - - public TelemetryPingStore(final String profileName) { - this.profileName = profileName; - } - - /** - * @return the profile name associated with this store. - */ - public String getProfileName() { - return profileName; - } - - /** - * @return a list of all the telemetry pings in the store that are ready for upload, ascending oldest to newest. - */ - public abstract List<TelemetryPing> getAllPings(); - - /** - * Save a ping to the store. - * - * @param ping the ping to store - * @throws IOException for underlying store access errors - */ - public abstract void storePing(TelemetryPing ping) throws IOException; - - /** - * Removes telemetry pings from the store if there are too many pings or they take up too much space. - * Pings should be removed from oldest to newest. - */ - public abstract void maybePrunePings(); - - /** - * Removes the successfully uploaded pings from the database and performs another other actions necessary - * for when upload is completed. - * - * @param successfulRemoveIDs doc ids of pings that were successfully uploaded - */ - public abstract void onUploadAttemptComplete(Set<String> successfulRemoveIDs); -} |