Bug 1183320 - Remove FHR from android/services r=rnewman

This commit is contained in:
Mark Finkle 2016-01-11 23:08:12 -05:00
parent 9433e40e51
commit 00f8f44c83
65 changed files with 2 additions and 11864 deletions

View File

@ -16,7 +16,6 @@
android:targetSdkVersion="22"/>
#include ../services/manifests/FxAccountAndroidManifest_permissions.xml.in
#include ../services/manifests/HealthReportAndroidManifest_permissions.xml.in
#include ../services/manifests/SyncAndroidManifest_permissions.xml.in
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
@ -357,7 +356,6 @@
</receiver>
#include ../services/manifests/FxAccountAndroidManifest_activities.xml.in
#include ../services/manifests/HealthReportAndroidManifest_activities.xml.in
#include ../services/manifests/SyncAndroidManifest_activities.xml.in
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
#include ../search/manifests/SearchAndroidManifest_activities.xml.in
@ -465,7 +463,6 @@
#include ../services/manifests/FxAccountAndroidManifest_services.xml.in
#include ../services/manifests/HealthReportAndroidManifest_services.xml.in
#include ../services/manifests/SyncAndroidManifest_services.xml.in
<service

View File

@ -765,10 +765,6 @@ sync_thirdparty_java_files = [
sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozilla/gecko/' + x for x in [
'background/BackgroundService.java',
'background/bagheera/BagheeraClient.java',
'background/bagheera/BagheeraRequestDelegate.java',
'background/bagheera/BoundedByteArrayEntity.java',
'background/bagheera/DeflateHelper.java',
'background/common/DateUtils.java',
'background/common/EditorBranch.java',
'background/common/GlobalConstants.java',
@ -805,31 +801,6 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil
'background/fxa/profile/FxAccountProfileClient10.java',
'background/fxa/QuickPasswordStretcher.java',
'background/fxa/SkewHandler.java',
'background/healthreport/AndroidConfigurationProvider.java',
'background/healthreport/Environment.java',
'background/healthreport/EnvironmentBuilder.java',
'background/healthreport/EnvironmentV1.java',
'background/healthreport/EnvironmentV2.java',
'background/healthreport/HealthReportBroadcastReceiver.java',
'background/healthreport/HealthReportBroadcastService.java',
'background/healthreport/HealthReportConstants.java',
'background/healthreport/HealthReportDatabases.java',
'background/healthreport/HealthReportDatabaseStorage.java',
'background/healthreport/HealthReportExportedBroadcastReceiver.java',
'background/healthreport/HealthReportGenerator.java',
'background/healthreport/HealthReportProvider.java',
'background/healthreport/HealthReportStorage.java',
'background/healthreport/HealthReportUtils.java',
'background/healthreport/ProfileInformationCache.java',
'background/healthreport/prune/HealthReportPruneService.java',
'background/healthreport/prune/PrunePolicy.java',
'background/healthreport/prune/PrunePolicyDatabaseStorage.java',
'background/healthreport/prune/PrunePolicyStorage.java',
'background/healthreport/upload/AndroidSubmissionClient.java',
'background/healthreport/upload/HealthReportUploadService.java',
'background/healthreport/upload/ObsoleteDocumentTracker.java',
'background/healthreport/upload/SubmissionClient.java',
'background/healthreport/upload/SubmissionPolicy.java',
'background/nativecode/NativeCrypto.java',
'background/preferences/PreferenceFragment.java',
'background/preferences/PreferenceManagerCompat.java',

View File

@ -1,41 +0,0 @@
<provider android:name="org.mozilla.gecko.background.healthreport.HealthReportProvider"
android:authorities="@ANDROID_PACKAGE_NAME@.health"
android:exported="false">
</provider>
<!-- HealthReportBroadcastReceiver$ExportedReceiver is a thin receiver
whose purpose is to start the background service in response to
system events. It's exported so that it can receive system events.
Such events cannot specify Health Report settings.
-->
<receiver
android:name="org.mozilla.gecko.background.healthreport.HealthReportExportedBroadcastReceiver"
android:exported="true">
<intent-filter>
<!-- Startup. -->
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
<intent-filter>
<!-- SD card remounted. -->
<action android:name="android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE" />
</intent-filter>
</receiver>
<!-- HealthReportBroadcastReceiver is a thin receiver whose purpose is
to start the background service in response to events internal to
Health Report. Such events can specify Health Report settings, so
these intents must come from a trusted source; hence, this receiver
is not exported.
-->
<receiver
android:name="org.mozilla.gecko.background.healthreport.HealthReportBroadcastReceiver"
android:exported="false">
<intent-filter >
<!-- Toggle Health Report upload service alarm (based on preferences value) -->
<action android:name="@ANDROID_PACKAGE_NAME@.HEALTHREPORT_UPLOAD_PREF" />
</intent-filter>
<intent-filter >
<!-- Enable Health Report prune service alarm -->
<action android:name="@ANDROID_PACKAGE_NAME@.HEALTHREPORT_PRUNE" />
</intent-filter>
</receiver>

View File

@ -1,5 +0,0 @@
<!-- So we can start our service. -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- So we can receive messages from Fennec. -->
<uses-permission android:name="@ANDROID_PACKAGE_NAME@.permission.PER_ANDROID_PACKAGE" />

View File

@ -1,17 +0,0 @@
<!-- BroadcastService responds to external events and starts
the other background services. We don't export any of
these services, since they are only started by components
internal to the Fennec package.
-->
<service
android:exported="false"
android:name="org.mozilla.gecko.background.healthreport.HealthReportBroadcastService" >
</service>
<service
android:exported="false"
android:name="org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService" >
</service>
<service
android:exported="false"
android:name="org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService" >
</service>

View File

@ -29,3 +29,5 @@
<permission
android:name="@ANDROID_PACKAGE_NAME@.permission.PER_ANDROID_PACKAGE"
android:protectionLevel="signature"/>
<uses-permission android:name="@ANDROID_PACKAGE_NAME@.permission.PER_ANDROID_PACKAGE" />

View File

@ -1,258 +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.background.bagheera;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
import org.mozilla.gecko.sync.net.Resource;
import ch.boye.httpclientandroidlib.HttpEntity;
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 ch.boye.httpclientandroidlib.protocol.HTTP;
/**
* Provides encapsulated access to a Bagheera document server.
* The two permitted operations are:
* * Delete a document.
* * Upload a document, optionally deleting an expired document.
*/
public class BagheeraClient {
protected final String serverURI;
protected final Executor executor;
protected static final Pattern URI_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$");
protected static String PROTOCOL_VERSION = "1.0";
protected static String SUBMIT_PATH = "/submit/";
/**
* Instantiate a new client pointing at the provided server.
* {@link #deleteDocument(String, String, BagheeraRequestDelegate)} and
* {@link #uploadJSONDocument(String, String, String, String, BagheeraRequestDelegate)}
* both accept delegate arguments; the {@link Executor} provided to this
* constructor will be used to invoke callbacks on those delegates.
*
* @param serverURI
* the destination server URI.
* @param executor
* the executor which will be used to invoke delegate callbacks.
*/
public BagheeraClient(final String serverURI, final Executor executor) {
if (serverURI == null) {
throw new IllegalArgumentException("Must provide a server URI.");
}
if (executor == null) {
throw new IllegalArgumentException("Must provide a non-null executor.");
}
this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
this.executor = executor;
}
/**
* Instantiate a new client pointing at the provided server.
* Delegate callbacks will be invoked on a new background thread.
*
* See {@link #BagheeraClient(String, Executor)} for more details.
*
* @param serverURI
* the destination server URI.
*/
public BagheeraClient(final String serverURI) {
this(serverURI, Executors.newSingleThreadExecutor());
}
/**
* Delete the specified document from the server.
* The delegate's callbacks will be invoked by the BagheeraClient's executor.
*/
public void deleteDocument(final String namespace,
final String id,
final BagheeraRequestDelegate delegate) throws URISyntaxException {
if (namespace == null) {
throw new IllegalArgumentException("Must provide namespace.");
}
if (id == null) {
throw new IllegalArgumentException("Must provide id.");
}
final BaseResource resource = makeResource(namespace, id);
resource.delegate = new BagheeraResourceDelegate(resource, namespace, id, delegate);
resource.delete();
}
/**
* Upload a JSON document to a Bagheera server. The delegate's callbacks will
* be invoked in tasks run by the client's executor.
*
* @param namespace
* the namespace, such as "test"
* @param id
* the document ID, which is typically a UUID.
* @param payload
* a document, typically JSON-encoded.
* @param oldIDs
* an optional collection of IDs which denote documents to supersede. Can be null or empty.
* @param delegate
* the delegate whose methods should be invoked on success or
* failure.
*/
public void uploadJSONDocument(final String namespace,
final String id,
final String payload,
Collection<String> oldIDs,
final BagheeraRequestDelegate delegate) throws URISyntaxException {
if (namespace == null) {
throw new IllegalArgumentException("Must provide namespace.");
}
if (id == null) {
throw new IllegalArgumentException("Must provide id.");
}
if (payload == null) {
throw new IllegalArgumentException("Must provide payload.");
}
final BaseResource resource = makeResource(namespace, id);
final HttpEntity deflatedBody = DeflateHelper.deflateBody(payload);
resource.delegate = new BagheeraUploadResourceDelegate(resource, namespace, id, oldIDs, delegate);
resource.post(deflatedBody);
}
public static boolean isValidURIComponent(final String in) {
return URI_PATTERN.matcher(in).matches();
}
protected BaseResource makeResource(final String namespace, final String id) throws URISyntaxException {
if (!isValidURIComponent(namespace)) {
throw new URISyntaxException(namespace, "Illegal namespace name. Must be alphanumeric + [_-].");
}
if (!isValidURIComponent(id)) {
throw new URISyntaxException(id, "Illegal id value. Must be alphanumeric + [_-].");
}
final String uri = this.serverURI + PROTOCOL_VERSION + SUBMIT_PATH +
namespace + "/" + id;
return new BaseResource(uri);
}
public class BagheeraResourceDelegate extends BaseResourceDelegate {
private static final int DEFAULT_SOCKET_TIMEOUT_MSEC = 5 * 60 * 1000; // Five minutes.
protected final BagheeraRequestDelegate delegate;
protected final String namespace;
protected final String id;
public BagheeraResourceDelegate(final Resource resource,
final String namespace,
final String id,
final BagheeraRequestDelegate delegate) {
super(resource);
this.namespace = namespace;
this.id = id;
this.delegate = delegate;
}
@Override
public String getUserAgent() {
return delegate.getUserAgent();
}
@Override
public int socketTimeout() {
return DEFAULT_SOCKET_TIMEOUT_MSEC;
}
@Override
public void handleHttpResponse(HttpResponse response) {
final int status = response.getStatusLine().getStatusCode();
switch (status) {
case 200:
case 201:
invokeHandleSuccess(status, response);
return;
default:
invokeHandleFailure(status, response);
}
}
protected void invokeHandleError(final Exception e) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleError(e);
}
});
}
protected void invokeHandleFailure(final int status, final HttpResponse response) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleFailure(status, namespace, response);
}
});
}
protected void invokeHandleSuccess(final int status, final HttpResponse response) {
executor.execute(new Runnable() {
@Override
public void run() {
delegate.handleSuccess(status, namespace, id, response);
}
});
}
@Override
public void handleHttpProtocolException(final ClientProtocolException e) {
invokeHandleError(e);
}
@Override
public void handleHttpIOException(IOException e) {
invokeHandleError(e);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
invokeHandleError(e);
}
}
public final class BagheeraUploadResourceDelegate extends BagheeraResourceDelegate {
private static final String HEADER_OBSOLETE_DOCUMENT = "X-Obsolete-Document";
private static final String COMPRESSED_CONTENT_TYPE = "application/json+zlib; charset=utf-8";
protected final Collection<String> obsoleteDocumentIDs;
public BagheeraUploadResourceDelegate(Resource resource,
String namespace,
String id,
Collection<String> obsoleteDocumentIDs,
BagheeraRequestDelegate delegate) {
super(resource, namespace, id, delegate);
this.obsoleteDocumentIDs = obsoleteDocumentIDs;
}
@Override
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
super.addHeaders(request, client);
request.setHeader(HTTP.CONTENT_TYPE, COMPRESSED_CONTENT_TYPE);
if (this.obsoleteDocumentIDs != null && this.obsoleteDocumentIDs.size() > 0) {
request.addHeader(HEADER_OBSOLETE_DOCUMENT, Utils.toCommaSeparatedString(this.obsoleteDocumentIDs));
}
}
}
}

View File

@ -1,15 +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.background.bagheera;
import ch.boye.httpclientandroidlib.HttpResponse;
public interface BagheeraRequestDelegate {
void handleSuccess(int status, String namespace, String id, HttpResponse response);
void handleError(Exception e);
void handleFailure(int status, String namespace, HttpResponse response);
public String getUserAgent();
}

View File

@ -1,88 +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.background.bagheera;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import ch.boye.httpclientandroidlib.entity.AbstractHttpEntity;
import ch.boye.httpclientandroidlib.entity.ByteArrayEntity;
/**
* An entity that acts like {@link ByteArrayEntity}, but exposes a window onto
* the byte array that is a subsection of the array. The purpose of this is to
* allow a smaller entity to be created without having to resize the source
* array.
*/
public class BoundedByteArrayEntity extends AbstractHttpEntity implements
Cloneable {
protected final byte[] content;
protected final int start;
protected final int end;
protected final int length;
/**
* Create a new entity that behaves exactly like a {@link ByteArrayEntity}
* created with a copy of <code>b</code> truncated to (
* <code>end - start</code>) bytes, starting at <code>start</code>.
*
* @param b the byte array to use.
* @param start the start index.
* @param end the end index.
*/
public BoundedByteArrayEntity(final byte[] b, final int start, final int end) {
if (b == null) {
throw new IllegalArgumentException("Source byte array may not be null.");
}
if (end < start ||
start < 0 ||
end < 0 ||
start > b.length ||
end > b.length) {
throw new IllegalArgumentException("Bounds out of range.");
}
this.content = b;
this.start = start;
this.end = end;
this.length = end - start;
}
@Override
public boolean isRepeatable() {
return true;
}
@Override
public long getContentLength() {
return this.length;
}
@Override
public InputStream getContent() {
return new ByteArrayInputStream(this.content, this.start, this.length);
}
@Override
public void writeTo(final OutputStream outstream) throws IOException {
if (outstream == null) {
throw new IllegalArgumentException("Output stream may not be null.");
}
outstream.write(this.content);
outstream.flush();
}
@Override
public boolean isStreaming() {
return false;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}

View File

@ -1,77 +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.background.bagheera;
import java.io.UnsupportedEncodingException;
import java.util.zip.Deflater;
import ch.boye.httpclientandroidlib.HttpEntity;
public class DeflateHelper {
/**
* Conservative upper bound for zlib size, equivalent to the first few lines
* in zlib's deflateBound function.
*
* Includes zlib header.
*
* @param sourceLen
* the number of bytes to compress.
* @return the number of bytes to allocate for the compressed output.
*/
public static int deflateBound(final int sourceLen) {
return sourceLen + ((sourceLen + 7) >> 3) + ((sourceLen + 63) >> 6) + 5 + 6;
}
/**
* Deflate the input into the output array, returning the number of bytes
* written to output.
*/
public static int deflate(byte[] input, byte[] output) {
final Deflater deflater = new Deflater();
deflater.setInput(input);
deflater.finish();
final int length = deflater.deflate(output);
deflater.end();
return length;
}
/**
* Deflate the input, returning an HttpEntity that offers an accurate window
* on the output.
*
* Note that this method does not trim the output array. (Test code can use
* TestDeflation#deflateTrimmed(byte[]).)
*
* Trimming would be more efficient for long-term space use, but we expect this
* entity to be transient.
*
* Note also that deflate can require <b>more</b> space than the input.
* {@link #deflateBound(int)} tells us the most it will use.
*
* @param bytes the input to deflate.
* @return the deflated input as an entity.
*/
public static HttpEntity deflateBytes(final byte[] bytes) {
// We would like to use DeflaterInputStream here, but it's minSDK=9, and we
// still target 8. It would also force us to use chunked Transfer-Encoding,
// so perhaps it's for the best!
final byte[] out = new byte[deflateBound(bytes.length)];
final int outLength = deflate(bytes, out);
return new BoundedByteArrayEntity(out, 0, outLength);
}
public static HttpEntity deflateBody(final String payload) {
final byte[] bytes;
try {
bytes = payload.getBytes("UTF-8");
} catch (UnsupportedEncodingException ex) {
// This will never happen. Thanks, Java!
throw new RuntimeException(ex);
}
return deflateBytes(bytes);
}
}

View File

@ -23,15 +23,6 @@ public class GlobalConstants {
public static final int SHARED_PREFERENCES_MODE = 0;
// These are used to ask Fennec (via reflection) to send
// us a pref notification. This avoids us having to guess
// Fennec's prefs branch and pref name.
// Eventually Fennec might listen to startup notifications and
// do this automatically, but this will do for now. See Bug 800244.
public static String GECKO_PREFERENCES_CLASS = "org.mozilla.gecko.preferences.GeckoPreferences";
public static String GECKO_BROADCAST_HEALTHREPORT_UPLOAD_PREF_METHOD = "broadcastHealthReportUploadPref";
public static String GECKO_BROADCAST_HEALTHREPORT_PRUNE_METHOD = "broadcastHealthReportPrune";
// Common time values.
public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY;

View File

@ -1,76 +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.background.healthreport;
import org.mozilla.gecko.background.healthreport.Environment.UIType;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
import org.mozilla.gecko.util.HardwareUtils;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.DisplayMetrics;
public class AndroidConfigurationProvider implements ConfigurationProvider {
private static final float MILLIMETERS_PER_INCH = 25.4f;
private final Configuration configuration;
private final DisplayMetrics displayMetrics;
public AndroidConfigurationProvider(final Context context) {
final Resources resources = context.getResources();
this.configuration = resources.getConfiguration();
this.displayMetrics = resources.getDisplayMetrics();
HardwareUtils.init(context);
}
@Override
public boolean hasHardwareKeyboard() {
return configuration.keyboard != Configuration.KEYBOARD_NOKEYS;
}
@Override
public UIType getUIType() {
if (HardwareUtils.isLargeTablet()) {
return UIType.LARGE_TABLET;
}
if (HardwareUtils.isSmallTablet()) {
return UIType.SMALL_TABLET;
}
return UIType.DEFAULT;
}
@Override
public int getUIModeType() {
return configuration.uiMode & Configuration.UI_MODE_TYPE_MASK;
}
@Override
public int getScreenLayoutSize() {
return configuration.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
}
/**
* Calculate screen horizontal width, in millimeters.
* This is approximate, will be wrong on some devices, and
* most likely doesn't include screen area that the app doesn't own.
* http://stackoverflow.com/questions/2193457/is-there-a-way-to-determine-android-physical-screen-height-in-cm-or-inches
*/
@Override
public int getScreenXInMM() {
return Math.round((displayMetrics.widthPixels / displayMetrics.xdpi) * MILLIMETERS_PER_INCH);
}
/**
* @see #getScreenXInMM() for caveats.
*/
@Override
public int getScreenYInMM() {
return Math.round((displayMetrics.heightPixels / displayMetrics.ydpi) * MILLIMETERS_PER_INCH);
}
}

View File

@ -1,98 +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.background.healthreport;
/**
* This captures all of the details that define an 'environment' for FHR's purposes.
* Whenever this format changes, it'll be changing with a build ID, so no migration
* of values is needed.
*
* Unless you remove the build descriptors from the set, of course.
*
* Or store these in a database.
*
* Instances of this class should be considered "effectively immutable": control their
* scope such that clear creation/sharing boundaries exist. Once you've populated and
* registered an <code>Environment</code>, don't do so again; start from scratch.
*
*/
public abstract class Environment extends EnvironmentV2 {
// Version 2 adds osLocale, appLocale, acceptLangSet, and distribution.
// Version 3 adds device characteristics.
public static final int CURRENT_VERSION = 3;
public static enum UIType {
// Corresponds to the typical phone interface.
DEFAULT("default"),
// Corresponds to a device for which Fennec is displaying the large tablet UI.
LARGE_TABLET("largetablet"),
// Corresponds to a device for which Fennec is displaying the small tablet UI.
SMALL_TABLET("smalltablet");
private final String label;
private UIType(final String label) {
this.label = label;
}
public String toString() {
return this.label;
}
public static UIType fromLabel(final String label) {
for (UIType type : UIType.values()) {
if (type.label.equals(label)) {
return type;
}
}
throw new IllegalArgumentException("Bad enum value: " + label);
}
}
public UIType uiType = UIType.DEFAULT;
/**
* Mask of Configuration#uiMode. E.g., UI_MODE_TYPE_CAR.
*/
public int uiMode = 0; // UI_MODE_TYPE_UNDEFINED = 0
/**
* Computed physical dimensions in millimeters.
*/
public int screenXInMM;
public int screenYInMM;
/**
* One of the Configuration#SCREENLAYOUT_SIZE_* constants.
*/
public int screenLayout = 0; // SCREENLAYOUT_SIZE_UNDEFINED = 0
public boolean hasHardwareKeyboard;
public Environment() {
this(Environment.HashAppender.class);
}
public Environment(Class<? extends EnvironmentAppender> appenderClass) {
super(appenderClass);
version = CURRENT_VERSION;
}
@Override
protected void appendHash(EnvironmentAppender appender) {
super.appendHash(appender);
// v3.
appender.append(hasHardwareKeyboard ? 1 : 0);
appender.append(uiType.toString());
appender.append(uiMode);
appender.append(screenLayout);
appender.append(screenXInMM);
appender.append(screenYInMM);
}
}

View File

@ -1,189 +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.background.healthreport;
import java.util.Iterator;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.SysInfo;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.Environment.UIType;
import android.content.ContentProvider;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
/**
* Construct a HealthReport environment from the current running system.
*/
public class EnvironmentBuilder {
private static final String LOG_TAG = "GeckoEnvBuilder";
public static ContentProviderClient getContentProviderClient(Context context) {
ContentResolver cr = context.getContentResolver();
return cr.acquireContentProviderClient(HealthReportConstants.HEALTH_AUTHORITY);
}
/**
* Fetch the storage object associated with the provided
* {@link ContentProviderClient}. If no storage instance can be found --
* perhaps because the {@link ContentProvider} is running in a different
* process -- returns <code>null</code>. On success, the returned
* {@link HealthReportDatabaseStorage} instance is owned by the underlying
* {@link HealthReportProvider} and thus does not need to be closed by the
* caller.
*
* If the provider is not a {@link HealthReportProvider}, throws a
* {@link ClassCastException}, because that would be disastrous.
*/
public static HealthReportDatabaseStorage getStorage(ContentProviderClient cpc,
String profilePath) {
ContentProvider pr = cpc.getLocalContentProvider();
if (pr == null) {
Logger.error(LOG_TAG, "Unable to retrieve local content provider. Running in a different process?");
return null;
}
try {
return ((HealthReportProvider) pr).getProfileStorage(profilePath);
} catch (ClassCastException ex) {
Logger.error(LOG_TAG, "ContentProvider not a HealthReportProvider!", ex);
throw ex;
}
}
public static interface ProfileInformationProvider {
public boolean isBlocklistEnabled();
public boolean isTelemetryEnabled();
public boolean isAcceptLangUserSet();
public long getProfileCreationTime();
public String getDistributionString();
public String getOSLocale();
public String getAppLocale();
public JSONObject getAddonsJSON();
}
public static interface ConfigurationProvider {
public boolean hasHardwareKeyboard();
public UIType getUIType();
public int getUIModeType();
public int getScreenLayoutSize();
public int getScreenXInMM();
public int getScreenYInMM();
}
protected static void populateEnvironment(Environment e,
ProfileInformationProvider info,
ConfigurationProvider config) {
e.cpuCount = SysInfo.getCPUCount();
e.memoryMB = SysInfo.getMemSize();
e.appName = AppConstants.MOZ_APP_NAME;
e.appID = AppConstants.MOZ_APP_ID;
e.appVersion = AppConstants.MOZ_APP_VERSION;
e.appBuildID = AppConstants.MOZ_APP_BUILDID;
e.updateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
e.vendor = AppConstants.MOZ_APP_VENDOR;
e.platformVersion = AppConstants.MOZILLA_VERSION;
e.platformBuildID = AppConstants.MOZ_APP_BUILDID;
e.xpcomabi = AppConstants.TARGET_XPCOM_ABI;
e.os = "Android";
e.architecture = SysInfo.getArchABI(); // Not just "arm".
e.sysName = SysInfo.getName();
e.sysVersion = SysInfo.getReleaseVersion();
e.profileCreation = (int) (info.getProfileCreationTime() / GlobalConstants.MILLISECONDS_PER_DAY);
// Corresponds to Gecko pref "extensions.blocklist.enabled".
e.isBlocklistEnabled = (info.isBlocklistEnabled() ? 1 : 0);
// Corresponds to Gecko pref "toolkit.telemetry.enabled".
e.isTelemetryEnabled = (info.isTelemetryEnabled() ? 1 : 0);
e.extensionCount = 0;
e.pluginCount = 0;
e.themeCount = 0;
JSONObject addons = info.getAddonsJSON();
if (addons != null) {
@SuppressWarnings("unchecked")
Iterator<String> it = addons.keys();
while (it.hasNext()) {
String key = it.next();
try {
JSONObject addon = addons.getJSONObject(key);
String type = addon.optString("type");
Logger.pii(LOG_TAG, "Add-on " + key + " is a " + type);
if ("extension".equals(type)) {
++e.extensionCount;
} else if ("plugin".equals(type)) {
++e.pluginCount;
} else if ("theme".equals(type)) {
++e.themeCount;
} else if ("service".equals(type)) {
// Later.
} else {
Logger.debug(LOG_TAG, "Unknown add-on type: " + type);
}
} catch (Exception ex) {
Logger.warn(LOG_TAG, "Failed to process add-on " + key, ex);
}
}
}
e.addons = addons;
// v2 environment fields.
e.distribution = info.getDistributionString();
e.osLocale = info.getOSLocale();
e.appLocale = info.getAppLocale();
e.acceptLangSet = info.isAcceptLangUserSet() ? 1 : 0;
// v3 environment fields.
e.hasHardwareKeyboard = config.hasHardwareKeyboard();
e.uiType = config.getUIType();
e.uiMode = config.getUIModeType();
e.screenLayout = config.getScreenLayoutSize();
e.screenXInMM = config.getScreenXInMM();
e.screenYInMM = config.getScreenYInMM();
}
/**
* Returns an {@link Environment} not linked to a storage instance, but
* populated with current field values.
*
* @param info a source of profile data
* @return the new {@link Environment}
*/
public static Environment getCurrentEnvironment(ProfileInformationProvider info, ConfigurationProvider config) {
Environment e = new Environment() {
@Override
public int register() {
return 0;
}
};
populateEnvironment(e, info, config);
return e;
}
/**
* @return the current environment's ID in the provided storage layer
*/
public static int registerCurrentEnvironment(final HealthReportStorage storage,
final ProfileInformationProvider info,
final ConfigurationProvider config) {
Environment e = storage.getEnvironment();
populateEnvironment(e, info, config);
e.register();
Logger.debug(LOG_TAG, "Registering current environment: " + e.getHash() + " = " + e.id);
return e.id;
}
}

View File

@ -1,270 +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.background.healthreport;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.SortedSet;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.nativecode.NativeCrypto;
public abstract class EnvironmentV1 {
private static final String LOG_TAG = "GeckoEnvironment";
private static final int VERSION = 1;
protected final Class<? extends EnvironmentAppender> appenderClass;
protected volatile String hash = null;
protected volatile int id = -1;
public int version = VERSION;
// org.mozilla.profile.age.
public int profileCreation;
// org.mozilla.sysinfo.sysinfo.
public int cpuCount;
public int memoryMB;
public String architecture;
public String sysName;
public String sysVersion; // Kernel.
// geckoAppInfo.
public String vendor;
public String appName;
public String appID;
public String appVersion;
public String appBuildID;
public String platformVersion;
public String platformBuildID;
public String os;
public String xpcomabi;
public String updateChannel;
// appinfo.
public int isBlocklistEnabled;
public int isTelemetryEnabled;
// org.mozilla.addons.active.
public JSONObject addons = null;
// org.mozilla.addons.counts.
public int extensionCount;
public int pluginCount;
public int themeCount;
/**
* We break out this interface in order to allow for testing -- pass in your
* own appender that just records strings, for example.
*/
public static abstract class EnvironmentAppender {
public abstract void append(String s);
public abstract void append(int v);
}
public static class HashAppender extends EnvironmentAppender {
private final StringBuilder builder;
public HashAppender() throws NoSuchAlgorithmException {
builder = new StringBuilder();
}
@Override
public void append(String s) {
builder.append((s == null) ? "null" : s);
}
@Override
public void append(int profileCreation) {
append(Integer.toString(profileCreation, 10));
}
@Override
public String toString() {
// We *could* use ASCII85 but the savings would be negated by the
// inclusion of JSON-unsafe characters like double-quote.
final byte[] inputBytes;
try {
inputBytes = builder.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
Logger.warn(LOG_TAG, "Invalid charset String passed to getBytes", e);
return null;
}
// Note to the security-minded reader: we deliberately use SHA-1 here, not
// a stronger hash. These identifiers don't strictly need a cryptographic
// hash function, because there is negligible value in attacking the hash.
// We use SHA-1 because it's *shorter* -- the exact same reason that Git
// chose SHA-1.
final byte[] hash = NativeCrypto.sha1(inputBytes);
return new Base64(-1, null, false).encodeAsString(hash);
}
}
/**
* Ensure that the {@link Environment} has been registered with its
* storage layer, and can be used to annotate events.
*
* It's safe to call this method more than once, and each time you'll
* get the same ID.
*
* @return the integer ID to use in subsequent DB insertions.
*/
public abstract int register();
protected EnvironmentAppender getAppender() {
EnvironmentAppender appender = null;
try {
appender = appenderClass.newInstance();
} catch (InstantiationException | IllegalAccessException ex) {
// Should never happen, but...
Logger.warn(LOG_TAG, "Could not compute hash.", ex);
}
return appender;
}
protected void appendHash(EnvironmentAppender appender) {
appender.append(profileCreation);
appender.append(cpuCount);
appender.append(memoryMB);
appender.append(architecture);
appender.append(sysName);
appender.append(sysVersion);
appender.append(vendor);
appender.append(appName);
appender.append(appID);
appender.append(appVersion);
appender.append(appBuildID);
appender.append(platformVersion);
appender.append(platformBuildID);
appender.append(os);
appender.append(xpcomabi);
appender.append(updateChannel);
appender.append(isBlocklistEnabled);
appender.append(isTelemetryEnabled);
appender.append(extensionCount);
appender.append(pluginCount);
appender.append(themeCount);
// We need sorted values.
if (addons != null) {
appendSortedAddons(getNonIgnoredAddons(), appender);
}
}
/**
* Compute the stable hash of the configured environment.
*
* @return the hash in base34, or null if there was a problem.
*/
public String getHash() {
// It's never unset, so we only care about partial reads. volatile is enough.
if (hash != null) {
return hash;
}
EnvironmentAppender appender = getAppender();
if (appender == null) {
return null;
}
appendHash(appender);
return hash = appender.toString();
}
public EnvironmentV1(Class<? extends EnvironmentAppender> appenderClass) {
super();
this.appenderClass = appenderClass;
}
public JSONObject getNonIgnoredAddons() {
if (addons == null) {
return null;
}
JSONObject out = new JSONObject();
@SuppressWarnings("unchecked")
Iterator<String> keys = addons.keys();
while (keys.hasNext()) {
try {
final String key = keys.next();
final Object obj = addons.get(key);
if (obj != null &&
obj instanceof JSONObject &&
((JSONObject) obj).optBoolean("ignore", false)) {
continue;
}
out.put(key, obj);
} catch (JSONException ex) {
// Do nothing.
}
}
return out;
}
/**
* Take a collection of add-on descriptors, appending a consistent string
* to the provided builder.
*/
public static void appendSortedAddons(JSONObject addons, final EnvironmentAppender builder) {
final SortedSet<String> keys = HealthReportUtils.sortedKeySet(addons);
// For each add-on, produce a consistent, sorted mapping of its descriptor.
for (String key : keys) {
try {
JSONObject addon = addons.getJSONObject(key);
// Now produce the output for this add-on.
builder.append(key);
builder.append("={");
for (String addonKey : HealthReportUtils.sortedKeySet(addon)) {
builder.append(addonKey);
builder.append("==");
try {
builder.append(addon.get(addonKey).toString());
} catch (JSONException e) {
builder.append("_e_");
}
}
builder.append("}");
} catch (Exception e) {
// Muffle.
Logger.warn(LOG_TAG, "Invalid add-on for ID " + key);
}
}
}
public void setJSONForAddons(byte[] json) throws Exception {
setJSONForAddons(new String(json, "UTF-8"));
}
public void setJSONForAddons(String json) throws Exception {
if (json == null || "null".equals(json)) {
addons = null;
return;
}
addons = new JSONObject(json);
}
public void setJSONForAddons(JSONObject json) {
addons = json;
}
/**
* Includes ignored add-ons.
*/
public String getNormalizedAddonsJSON() {
// We trust that our input will already be normalized. If that assumption
// is invalidated, then we'll be sorry.
return (addons == null) ? "null" : addons.toString();
}
}

View File

@ -1,30 +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.background.healthreport;
public abstract class EnvironmentV2 extends EnvironmentV1 {
private static final int VERSION = 2;
public String osLocale;
public String appLocale;
public int acceptLangSet;
public String distribution;
public EnvironmentV2(Class<? extends EnvironmentAppender> appenderClass) {
super(appenderClass);
version = VERSION;
}
@Override
protected void appendHash(EnvironmentAppender appender) {
super.appendHash(appender);
// v2.
appender.append(osLocale);
appender.append(appLocale);
appender.append(acceptLangSet);
appender.append(distribution);
}
}

View File

@ -1,31 +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.background.healthreport;
import org.mozilla.gecko.background.common.log.Logger;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* Watch for internal notifications to start Health Report background services.
*/
public class HealthReportBroadcastReceiver extends BroadcastReceiver {
public static final String LOG_TAG = HealthReportBroadcastReceiver.class.getSimpleName();
/**
* Forward the intent (action and extras) to an IntentService to do background processing.
*/
@Override
public void onReceive(Context context, Intent intent) {
Logger.debug(LOG_TAG, "Received intent - forwarding to BroadcastService.");
Intent service = new Intent(context, HealthReportBroadcastService.class);
// It's safe to forward extras since these are internal intents.
service.putExtras(intent);
service.setAction(intent.getAction());
context.startService(service);
}
}

View File

@ -1,260 +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.background.healthreport;
import org.mozilla.gecko.background.BackgroundService;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService;
import org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService;
import org.mozilla.gecko.background.healthreport.upload.ObsoleteDocumentTracker;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
/**
* A service which listens to broadcast intents from the system and from the
* browser, registering or unregistering the background health report services with the
* {@link AlarmManager}.
*/
public class HealthReportBroadcastService extends BackgroundService {
public static final String LOG_TAG = HealthReportBroadcastService.class.getSimpleName();
public static final String WORKER_THREAD_NAME = LOG_TAG + "Worker";
public HealthReportBroadcastService() {
super(WORKER_THREAD_NAME);
}
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(HealthReportConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
}
public long getSubmissionPollInterval() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_SUBMISSION_INTENT_INTERVAL_MSEC, HealthReportConstants.DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC);
}
public void setSubmissionPollInterval(final long interval) {
getSharedPreferences().edit().putLong(HealthReportConstants.PREF_SUBMISSION_INTENT_INTERVAL_MSEC, interval).commit();
}
public long getPrunePollInterval() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_PRUNE_INTENT_INTERVAL_MSEC,
HealthReportConstants.DEFAULT_PRUNE_INTENT_INTERVAL_MSEC);
}
public void setPrunePollInterval(final long interval) {
getSharedPreferences().edit().putLong(HealthReportConstants.PREF_PRUNE_INTENT_INTERVAL_MSEC,
interval).commit();
}
/**
* Set or cancel an alarm to submit data for a profile.
*
* @param context
* Android context.
* @param profileName
* to submit data for.
* @param profilePath
* to submit data for.
* @param enabled
* whether the user has enabled submitting health report data for
* this profile.
* @param serviceEnabled
* whether submitting should be scheduled. If the user turns off
* submitting, <code>enabled</code> could be false but we could need
* to delete so <code>serviceEnabled</code> could be true.
*/
protected void toggleSubmissionAlarm(final Context context, String profileName, String profilePath,
boolean enabled, boolean serviceEnabled) {
final Class<?> serviceClass = HealthReportUploadService.class;
Logger.info(LOG_TAG, (serviceEnabled ? "R" : "Unr") + "egistering " +
serviceClass.getSimpleName() + ".");
// PendingIntents are compared without reference to their extras. Therefore
// even though we pass the profile details to the action, different
// profiles will share the *same* pending intent. In a multi-profile future,
// this will need to be addressed. See Bug 882182.
final Intent service = new Intent(context, serviceClass);
service.setAction("upload"); // PendingIntents "lose" their extras if no action is set.
service.putExtra("uploadEnabled", enabled);
service.putExtra("profileName", profileName);
service.putExtra("profilePath", profilePath);
final PendingIntent pending = PendingIntent.getService(context, 0, service, PendingIntent.FLAG_CANCEL_CURRENT);
if (!serviceEnabled) {
cancelAlarm(pending);
return;
}
final long pollInterval = getSubmissionPollInterval();
scheduleAlarm(pollInterval, pending);
}
@Override
protected void onHandleIntent(Intent intent) {
Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
// Intent can be null. Bug 1025937.
if (intent == null) {
Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
return;
}
// The same intent can be handled by multiple methods so do not short-circuit evaluate.
boolean handled = attemptHandleIntentForUpload(intent);
handled = attemptHandleIntentForPrune(intent) || handled;
if (!handled) {
Logger.warn(LOG_TAG, "Unhandled intent with action " + intent.getAction() + ".");
}
}
/**
* Attempts to handle the given intent for FHR document upload. If it cannot, false is returned.
*
* @param intent must be non-null.
*/
private boolean attemptHandleIntentForUpload(final Intent intent) {
if (HealthReportConstants.UPLOAD_FEATURE_DISABLED) {
Logger.debug(LOG_TAG, "Health report upload feature is compile-time disabled; not handling intent.");
return false;
}
final String action = intent.getAction();
Logger.debug(LOG_TAG, "Health report upload feature is compile-time enabled; attempting to " +
"handle intent with action " + action + ".");
if (HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF.equals(action)) {
handleUploadPrefIntent(intent);
return true;
}
if (Intent.ACTION_BOOT_COMPLETED.equals(action) ||
Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
BackgroundService.reflectContextToFennec(this,
GlobalConstants.GECKO_PREFERENCES_CLASS,
GlobalConstants.GECKO_BROADCAST_HEALTHREPORT_UPLOAD_PREF_METHOD);
return true;
}
return false;
}
/**
* Handle the intent sent by the browser when it wishes to notify us
* of the value of the user preference. Look at the value and toggle the
* alarm service accordingly.
*
* @param intent must be non-null.
*/
private void handleUploadPrefIntent(Intent intent) {
if (!intent.hasExtra("enabled")) {
Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF + " intent without enabled. Ignoring.");
return;
}
final boolean enabled = intent.getBooleanExtra("enabled", true);
Logger.debug(LOG_TAG, intent.getStringExtra("branch") + "/" +
intent.getStringExtra("pref") + " = " +
(intent.hasExtra("enabled") ? enabled : ""));
String profileName = intent.getStringExtra("profileName");
String profilePath = intent.getStringExtra("profilePath");
if (profileName == null || profilePath == null) {
Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF + " intent without profilePath or profileName. Ignoring.");
return;
}
Logger.pii(LOG_TAG, "Updating health report upload alarm for profile " + profileName + " at " +
profilePath + ".");
final SharedPreferences sharedPrefs = getSharedPreferences();
final ObsoleteDocumentTracker tracker = new ObsoleteDocumentTracker(sharedPrefs);
final boolean hasObsoleteIds = tracker.hasObsoleteIds();
if (!enabled) {
final Editor editor = sharedPrefs.edit();
editor.remove(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID);
if (hasObsoleteIds) {
Logger.debug(LOG_TAG, "Health report upload disabled; scheduling deletion of " + tracker.numberOfObsoleteIds() + " documents.");
tracker.limitObsoleteIds();
} else {
// Primarily intended for debugging and testing.
Logger.debug(LOG_TAG, "Health report upload disabled and no deletes to schedule: clearing prefs.");
editor.remove(HealthReportConstants.PREF_FIRST_RUN);
editor.remove(HealthReportConstants.PREF_NEXT_SUBMISSION);
}
editor.commit();
}
// The user can toggle us off or on, or we can have obsolete documents to
// remove.
final boolean serviceEnabled = hasObsoleteIds || enabled;
toggleSubmissionAlarm(this, profileName, profilePath, enabled, serviceEnabled);
}
/**
* Attempts to handle the given intent for FHR data pruning. If it cannot, false is returned.
*
* @param intent must be non-null.
*/
private boolean attemptHandleIntentForPrune(final Intent intent) {
final String action = intent.getAction();
Logger.debug(LOG_TAG, "Prune: Attempting to handle intent with action, " + action + ".");
if (HealthReportConstants.ACTION_HEALTHREPORT_PRUNE.equals(action)) {
handlePruneIntent(intent);
return true;
}
if (Intent.ACTION_BOOT_COMPLETED.equals(action) ||
Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
BackgroundService.reflectContextToFennec(this,
GlobalConstants.GECKO_PREFERENCES_CLASS,
GlobalConstants.GECKO_BROADCAST_HEALTHREPORT_PRUNE_METHOD);
return true;
}
return false;
}
/**
* @param intent must be non-null.
*/
private void handlePruneIntent(final Intent intent) {
final String profileName = intent.getStringExtra("profileName");
final String profilePath = intent.getStringExtra("profilePath");
if (profileName == null || profilePath == null) {
Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_PRUNE + " intent " +
"without profilePath or profileName. Ignoring.");
return;
}
final Class<?> serviceClass = HealthReportPruneService.class;
final Intent service = new Intent(this, serviceClass);
service.setAction("prune"); // Intents without actions have their extras removed.
service.putExtra("profileName", profileName);
service.putExtra("profilePath", profilePath);
final PendingIntent pending = PendingIntent.getService(this, 0, service,
PendingIntent.FLAG_CANCEL_CURRENT);
// Set a regular alarm to start PruneService. Since the various actions that PruneService can
// take occur on irregular intervals, we can be more efficient by only starting the Service
// when one of these time limits runs out. However, subsequent Service invocations must then
// be registered by the PruneService itself, which would fail if the PruneService crashes.
// Thus, we set this regular (and slightly inefficient) alarm.
Logger.info(LOG_TAG, "Registering " + serviceClass.getSimpleName() + ".");
final long pollInterval = getPrunePollInterval();
scheduleAlarm(pollInterval, pending);
}
}

View File

@ -1,128 +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.background.healthreport;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.AppConstants;
public class HealthReportConstants {
public static final String HEALTH_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".health";
public static final String GLOBAL_LOG_TAG = "GeckoHealth";
public static final String USER_AGENT = "Firefox-Android-HealthReport/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
/**
* The earliest allowable value for the last ping time, corresponding to May 2nd 2013.
* Used for sanity checks.
*/
public static final long EARLIEST_LAST_PING = 1367500000000L;
// Not `final` so we have the option to turn this on at runtime with a magic addon.
public static boolean UPLOAD_FEATURE_DISABLED = false;
// Android SharedPreferences branch where global (not per-profile) uploader
// settings are stored.
public static final String PREFS_BRANCH = "background";
// How frequently the submission and prune policies are ticked over. This is how frequently our
// intent is scheduled to be called by the Android Alarm Manager, not how frequently we
// actually submit. These values are set as preferences rather than constants so that testing
// addons can change their values.
public static final String PREF_SUBMISSION_INTENT_INTERVAL_MSEC = "healthreport_submission_intent_interval_msec";
public static final long DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC = GlobalConstants.MILLISECONDS_PER_DAY / 24;
public static final String PREF_PRUNE_INTENT_INTERVAL_MSEC = "healthreport_prune_intent_interval_msec";
public static final long DEFAULT_PRUNE_INTENT_INTERVAL_MSEC = GlobalConstants.MILLISECONDS_PER_DAY;
public static final String ACTION_HEALTHREPORT_UPLOAD_PREF = AppConstants.ANDROID_PACKAGE_NAME + ".HEALTHREPORT_UPLOAD_PREF";
public static final String ACTION_HEALTHREPORT_PRUNE = AppConstants.ANDROID_PACKAGE_NAME + ".HEALTHREPORT_PRUNE";
public static final String PREF_MINIMUM_TIME_BETWEEN_UPLOADS = "healthreport_time_between_uploads";
public static final long DEFAULT_MINIMUM_TIME_BETWEEN_UPLOADS = GlobalConstants.MILLISECONDS_PER_DAY;
public static final String PREF_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION = "healthreport_time_before_first_submission";
public static final long DEFAULT_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION = GlobalConstants.MILLISECONDS_PER_DAY;
public static final String PREF_MINIMUM_TIME_AFTER_FAILURE = "healthreport_time_after_failure";
public static final long DEFAULT_MINIMUM_TIME_AFTER_FAILURE = DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC;
public static final String PREF_MAXIMUM_FAILURES_PER_DAY = "healthreport_maximum_failures_per_day";
public static final long DEFAULT_MAXIMUM_FAILURES_PER_DAY = 2;
// Authoritative.
public static final String PREF_FIRST_RUN = "healthreport_first_run";
public static final String PREF_NEXT_SUBMISSION = "healthreport_next_submission";
public static final String PREF_CURRENT_DAY_FAILURE_COUNT = "healthreport_current_day_failure_count";
public static final String PREF_CURRENT_DAY_RESET_TIME = "healthreport_current_day_reset_time";
// Forensic.
public static final String PREF_LAST_UPLOAD_REQUESTED = "healthreport_last_upload_requested";
public static final String PREF_LAST_UPLOAD_SUCCEEDED = "healthreport_last_upload_succeeded";
public static final String PREF_LAST_UPLOAD_FAILED = "healthreport_last_upload_failed";
// Preferences for deleting obsolete documents.
public static final String PREF_MINIMUM_TIME_BETWEEN_DELETES = "healthreport_time_between_deletes";
public static final long DEFAULT_MINIMUM_TIME_BETWEEN_DELETES = DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC;
public static final String PREF_OBSOLETE_DOCUMENT_IDS_TO_DELETION_ATTEMPTS_REMAINING = "healthreport_obsolete_document_ids_to_deletions_remaining";
// We don't want to try to delete forever, but we also don't want to orphan
// obsolete document IDs from devices that fail to reach the server for a few
// days. This tries to delete document IDs for at least one week (of upload
// failures). Note that if the device is really offline, no upload is
// performed and our count of attempts is not altered.
public static final long DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID = (DEFAULT_MAXIMUM_FAILURES_PER_DAY + 1) * 7;
// If we absolutely know that a document ID reached the server, we really
// don't want to orphan it. This tries to delete document IDs that will
// definitely be orphaned for at least six weeks (of upload failures). Note
// that if the device is really offline, no upload is performed and our count
// of attempts is not altered.
public static final long DELETION_ATTEMPTS_PER_KNOWN_TO_BE_ON_SERVER_DOCUMENT_ID = (DEFAULT_MAXIMUM_FAILURES_PER_DAY + 1) * 7 * 6;
// We don't want to allocate unbounded storage for obsolete IDs, but we also
// don't want to orphan obsolete document IDs from devices that fail to delete
// for a few days. This stores as many IDs as are expected to be generated in
// a month. Note that if the device is really offline, no upload is performed
// and our count of attempts is not altered.
public static final long MAXIMUM_STORED_OBSOLETE_DOCUMENT_IDS = (DEFAULT_MAXIMUM_FAILURES_PER_DAY + 1) * 30;
// Forensic.
public static final String PREF_LAST_DELETE_REQUESTED = "healthreport_last_delete_requested";
public static final String PREF_LAST_DELETE_SUCCEEDED = "healthreport_last_delete_succeeded";
public static final String PREF_LAST_DELETE_FAILED = "healthreport_last_delete_failed";
// Preferences for upload client.
public static final String PREF_LAST_UPLOAD_LOCAL_TIME = "healthreport_last_upload_local_time";
public static final String PREF_LAST_UPLOAD_DOCUMENT_ID = "healthreport_last_upload_document_id";
public static final String PREF_DOCUMENT_SERVER_URI = "healthreport_document_server_uri";
public static final String DEFAULT_DOCUMENT_SERVER_URI = "https://fhr.data.mozilla.com/";
public static final String PREF_DOCUMENT_SERVER_NAMESPACE = "healthreport_document_server_namespace";
public static final String DEFAULT_DOCUMENT_SERVER_NAMESPACE = "metrics";
// One UUID is 36 characters (like e56542e0-e4d2-11e2-a28f-0800200c9a66), so
// we limit the number of obsolete IDs passed so that each request is not a
// large upload (and therefore more likely to fail). We also don't want to
// push Bagheera to make too many deletes, since we don't know how the cluster
// will handle such API usage. This obsoletes 2 days worth of old documents
// at a time.
public static final int MAXIMUM_DELETIONS_PER_POST = ((int) DEFAULT_MAXIMUM_FAILURES_PER_DAY + 1) * 2;
public static final String PREF_PRUNE_BY_SIZE_TIME = "healthreport_prune_by_size_time";
public static final long MINIMUM_TIME_BETWEEN_PRUNE_BY_SIZE_CHECKS_MILLIS =
GlobalConstants.MILLISECONDS_PER_DAY;
public static final int MAX_ENVIRONMENT_COUNT = 50;
public static final int ENVIRONMENT_COUNT_AFTER_PRUNE = 35;
public static final int MAX_EVENT_COUNT = 10000;
public static final int EVENT_COUNT_AFTER_PRUNE = 8000;
public static final String PREF_EXPIRATION_TIME = "healthreport_expiration_time";
public static final long MINIMUM_TIME_BETWEEN_EXPIRATION_CHECKS_MILLIS = GlobalConstants.MILLISECONDS_PER_DAY * 7;
public static final long EVENT_EXISTENCE_DURATION = GlobalConstants.MILLISECONDS_PER_SIX_MONTHS;
public static final String PREF_CLEANUP_TIME = "healthreport_cleanup_time";
public static final long MINIMUM_TIME_BETWEEN_CLEANUP_CHECKS_MILLIS = GlobalConstants.MILLISECONDS_PER_DAY * 30;
}

View File

@ -1,53 +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.background.healthreport;
import java.io.File;
import java.util.HashMap;
import org.mozilla.gecko.background.common.log.Logger;
import android.content.Context;
/**
* Manages a set of per-profile Health Report storage helpers.
*/
public class HealthReportDatabases {
private static final String LOG_TAG = "HealthReportDatabases";
private final Context context;
private final HashMap<File, HealthReportDatabaseStorage> storages = new HashMap<File, HealthReportDatabaseStorage>();
public HealthReportDatabases(final Context context) {
this.context = context;
}
public synchronized HealthReportDatabaseStorage getDatabaseHelperForProfile(final File profileDir) {
if (profileDir == null) {
throw new IllegalArgumentException("No profile provided.");
}
if (this.storages.containsKey(profileDir)) {
return this.storages.get(profileDir);
}
final HealthReportDatabaseStorage helper;
helper = new HealthReportDatabaseStorage(this.context, profileDir);
this.storages.put(profileDir, helper);
return helper;
}
public synchronized void closeDatabaseHelpers() {
for (HealthReportDatabaseStorage helper : storages.values()) {
try {
helper.close();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Failed to close database helper.", e);
}
}
storages.clear();
}
}

View File

@ -1,42 +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.background.healthreport;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.mozilla.gecko.background.common.log.Logger;
/**
* Watch for external system notifications to start Health Report background services.
*
* Some observations:
*
* From the Android documentation: "Also note that as of Android 3.0 the user
* needs to have started the application at least once before your application
* can receive android.intent.action.BOOT_COMPLETED events."
*
* We really do want to launch on BOOT_COMPLETED, since it's possible for a user
* to run Firefox, shut down the phone, then power it on again on the same day.
* We want to submit a health report in this case, even though they haven't
* launched Firefox since boot.
*/
public class HealthReportExportedBroadcastReceiver extends BroadcastReceiver {
public static final String LOG_TAG = HealthReportExportedBroadcastReceiver.class.getSimpleName();
/**
* Forward the intent action to an IntentService to do background processing.
* We intentionally do not forward extras, since there are none needed from
* external events.
*/
@Override
public void onReceive(Context context, Intent intent) {
Logger.debug(LOG_TAG, "Received intent - forwarding to BroadcastService.");
final Intent service = new Intent(context, HealthReportBroadcastService.class);
// We intentionally copy only the intent action.
service.setAction(intent.getAction());
context.startService(service);
}
}

View File

@ -1,711 +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.background.healthreport;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.common.DateUtils.DateFormatter;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
import android.database.Cursor;
import android.util.SparseArray;
public class HealthReportGenerator {
private static final int PAYLOAD_VERSION = 3;
private static final String LOG_TAG = "GeckoHealthGen";
private final HealthReportStorage storage;
private final DateFormatter dateFormatter;
public HealthReportGenerator(HealthReportStorage storage) {
this.storage = storage;
this.dateFormatter = new DateFormatter();
}
@SuppressWarnings("static-method")
protected long now() {
return System.currentTimeMillis();
}
/**
* Ensure that you have initialized the Locale to your satisfaction
* prior to calling this method.
*
* @return null if no environment could be computed, or else the resulting document.
* @throws JSONException if there was an error adding environment data to the resulting document.
*/
public JSONObject generateDocument(long since, long lastPingTime, String profilePath, ConfigurationProvider config) throws JSONException {
Logger.info(LOG_TAG, "Generating FHR document from " + since + "; last ping " + lastPingTime);
Logger.pii(LOG_TAG, "Generating for profile " + profilePath);
ProfileInformationCache cache = new ProfileInformationCache(profilePath);
if (!cache.restoreUnlessInitialized()) {
Logger.warn(LOG_TAG, "Not enough profile information to compute current environment.");
return null;
}
Environment current = EnvironmentBuilder.getCurrentEnvironment(cache, config);
return generateDocument(since, lastPingTime, current);
}
/**
* The document consists of:
*
*<ul>
*<li>Basic metadata: last ping time, current ping time, version.</li>
*<li>A map of environments: <code>current</code> and others named by hash. <code>current</code> is fully specified,
* and others are deltas from current.</li>
*<li>A <code>data</code> object. This includes <code>last</code> and <code>days</code>.</li>
*</ul>
*
* <code>days</code> is a map from date strings to <tt>{hash: {measurement: {_v: version, fields...}}}</tt>.
* @throws JSONException if there was an error adding environment data to the resulting document.
*/
public JSONObject generateDocument(long since, long lastPingTime, Environment currentEnvironment) throws JSONException {
final String currentHash = currentEnvironment.getHash();
Logger.debug(LOG_TAG, "Current environment hash: " + currentHash);
if (currentHash == null) {
Logger.warn(LOG_TAG, "Current hash is null; aborting.");
return null;
}
// We want to map field IDs to some strings as we go.
SparseArray<Environment> envs = storage.getEnvironmentRecordsByID();
JSONObject document = new JSONObject();
if (lastPingTime >= HealthReportConstants.EARLIEST_LAST_PING) {
document.put("lastPingDate", dateFormatter.getDateString(lastPingTime));
}
document.put("thisPingDate", dateFormatter.getDateString(now()));
document.put("version", PAYLOAD_VERSION);
document.put("environments", getEnvironmentsJSON(currentEnvironment, envs));
document.put("data", getDataJSON(currentEnvironment, envs, since));
return document;
}
protected JSONObject getDataJSON(Environment currentEnvironment,
SparseArray<Environment> envs, long since) throws JSONException {
SparseArray<Field> fields = storage.getFieldsByID();
JSONObject days = getDaysJSON(currentEnvironment, envs, fields, since);
JSONObject last = new JSONObject();
JSONObject data = new JSONObject();
data.put("days", days);
data.put("last", last);
return data;
}
protected JSONObject getDaysJSON(Environment currentEnvironment, SparseArray<Environment> envs, SparseArray<Field> fields, long since) throws JSONException {
if (Logger.shouldLogVerbose(LOG_TAG)) {
for (int i = 0; i < envs.size(); ++i) {
Logger.trace(LOG_TAG, "Days environment " + envs.keyAt(i) + ": " + envs.get(envs.keyAt(i)).getHash());
}
}
JSONObject days = new JSONObject();
Cursor cursor = storage.getRawEventsSince(since);
try {
if (!cursor.moveToFirst()) {
return days;
}
// A classic walking partition.
// Columns are "date", "env", "field", "value".
// Note that we care about the type (integer, string) and kind
// (last/counter, discrete) of each field.
// Each field will be accessed once for each date/env pair, so
// Field memoizes these facts.
// We also care about which measurement contains each field.
int lastDate = -1;
int lastEnv = -1;
JSONObject dateObject = null;
JSONObject envObject = null;
while (!cursor.isAfterLast()) {
int cEnv = cursor.getInt(1);
if (cEnv == -1 ||
(cEnv != lastEnv &&
envs.indexOfKey(cEnv) < 0)) {
Logger.warn(LOG_TAG, "Invalid environment " + cEnv + " in cursor. Skipping.");
cursor.moveToNext();
continue;
}
int cDate = cursor.getInt(0);
int cField = cursor.getInt(2);
Logger.trace(LOG_TAG, "Event row: " + cDate + ", " + cEnv + ", " + cField);
boolean dateChanged = cDate != lastDate;
boolean envChanged = cEnv != lastEnv;
if (dateChanged) {
if (dateObject != null) {
days.put(dateFormatter.getDateStringForDay(lastDate), dateObject);
}
dateObject = new JSONObject();
lastDate = cDate;
}
if (dateChanged || envChanged) {
envObject = new JSONObject();
// This is safe because we checked above that cEnv is valid.
dateObject.put(envs.get(cEnv).getHash(), envObject);
lastEnv = cEnv;
}
final Field field = fields.get(cField);
JSONObject measurement = envObject.optJSONObject(field.measurementName);
if (measurement == null) {
// We will never have more than one measurement version within a
// single environment -- to do so involves changing the build ID. And
// even if we did, we have no way to represent it. So just build the
// output object once.
measurement = new JSONObject();
measurement.put("_v", field.measurementVersion);
envObject.put(field.measurementName, measurement);
}
// How we record depends on the type of the field, so we
// break this out into a separate method for clarity.
recordMeasurementFromCursor(field, measurement, cursor);
cursor.moveToNext();
continue;
}
days.put(dateFormatter.getDateStringForDay(lastDate), dateObject);
} finally {
cursor.close();
}
return days;
}
/**
* Return the {@link JSONObject} parsed from the provided index of the given
* cursor, or {@link JSONObject#NULL} if either SQL <code>NULL</code> or
* string <code>"null"</code> is present at that index.
*/
private static Object getJSONAtIndex(Cursor cursor, int index) throws JSONException {
if (cursor.isNull(index)) {
return JSONObject.NULL;
}
final String value = cursor.getString(index);
if ("null".equals(value)) {
return JSONObject.NULL;
}
return new JSONObject(value);
}
protected static void recordMeasurementFromCursor(final Field field,
JSONObject measurement,
Cursor cursor)
throws JSONException {
if (field.isDiscreteField()) {
// Discrete counted. Increment the named counter.
if (field.isCountedField()) {
if (!field.isStringField()) {
throw new IllegalStateException("Unable to handle non-string counted types.");
}
HealthReportUtils.count(measurement, field.fieldName, cursor.getString(3));
return;
}
// Discrete string or integer. Append it.
if (field.isStringField()) {
HealthReportUtils.append(measurement, field.fieldName, cursor.getString(3));
return;
}
if (field.isJSONField()) {
HealthReportUtils.append(measurement, field.fieldName, getJSONAtIndex(cursor, 3));
return;
}
if (field.isIntegerField()) {
HealthReportUtils.append(measurement, field.fieldName, cursor.getLong(3));
return;
}
throw new IllegalStateException("Unknown field type: " + field.flags);
}
// Non-discrete -- must be LAST or COUNTER, so just accumulate the value.
if (field.isStringField()) {
measurement.put(field.fieldName, cursor.getString(3));
return;
}
if (field.isJSONField()) {
measurement.put(field.fieldName, getJSONAtIndex(cursor, 3));
return;
}
measurement.put(field.fieldName, cursor.getLong(3));
}
public static JSONObject getEnvironmentsJSON(Environment currentEnvironment,
SparseArray<Environment> envs) throws JSONException {
JSONObject environments = new JSONObject();
// Always do this, even if it hasn't recorded anything in the DB.
environments.put("current", jsonify(currentEnvironment, null));
String currentHash = currentEnvironment.getHash();
for (int i = 0; i < envs.size(); i++) {
Environment e = envs.valueAt(i);
if (currentHash.equals(e.getHash())) {
continue;
}
environments.put(e.getHash(), jsonify(e, currentEnvironment));
}
return environments;
}
public static JSONObject jsonify(Environment e, Environment current) throws JSONException {
JSONObject age = getProfileAge(e, current);
JSONObject sysinfo = getSysInfo(e, current);
JSONObject gecko = getGeckoInfo(e, current);
JSONObject appinfo = getAppInfo(e, current);
JSONObject counts = getAddonCounts(e, current);
JSONObject config = getDeviceConfig(e, current);
JSONObject out = new JSONObject();
if (age != null)
out.put("org.mozilla.profile.age", age);
if (sysinfo != null)
out.put("org.mozilla.sysinfo.sysinfo", sysinfo);
if (gecko != null)
out.put("geckoAppInfo", gecko);
if (appinfo != null)
out.put("org.mozilla.appInfo.appinfo", appinfo);
if (counts != null)
out.put("org.mozilla.addons.counts", counts);
JSONObject active = getActiveAddons(e, current);
if (active != null)
out.put("org.mozilla.addons.active", active);
if (config != null)
out.put("org.mozilla.device.config", config);
if (current == null) {
out.put("hash", e.getHash());
}
return out;
}
// v3 environment fields.
private static JSONObject getDeviceConfig(Environment e, Environment current) throws JSONException {
JSONObject config = new JSONObject();
int changes = 0;
if (e.version < 3) {
return null;
}
if (current != null && current.version < 3) {
return getDeviceConfig(e, null);
}
if (current == null || current.hasHardwareKeyboard != e.hasHardwareKeyboard) {
config.put("hasHardwareKeyboard", e.hasHardwareKeyboard);
changes++;
}
if (current == null || current.screenLayout != e.screenLayout) {
config.put("screenLayout", e.screenLayout);
changes++;
}
if (current == null || current.screenXInMM != e.screenXInMM) {
config.put("screenXInMM", e.screenXInMM);
changes++;
}
if (current == null || current.screenYInMM != e.screenYInMM) {
config.put("screenYInMM", e.screenYInMM);
changes++;
}
if (current == null || current.uiType != e.uiType) {
config.put("uiType", e.uiType.toString());
changes++;
}
if (current == null || current.uiMode != e.uiMode) {
config.put("uiMode", e.uiMode);
changes++;
}
if (current != null && changes == 0) {
return null;
}
config.put("_v", 1);
return config;
}
private static JSONObject getProfileAge(Environment e, Environment current) throws JSONException {
JSONObject age = new JSONObject();
int changes = 0;
if (current == null || current.profileCreation != e.profileCreation) {
age.put("profileCreation", e.profileCreation);
changes++;
}
if (current != null && changes == 0) {
return null;
}
age.put("_v", 1);
return age;
}
private static JSONObject getSysInfo(Environment e, Environment current) throws JSONException {
JSONObject sysinfo = new JSONObject();
int changes = 0;
if (current == null || current.cpuCount != e.cpuCount) {
sysinfo.put("cpuCount", e.cpuCount);
changes++;
}
if (current == null || current.memoryMB != e.memoryMB) {
sysinfo.put("memoryMB", e.memoryMB);
changes++;
}
if (current == null || !current.architecture.equals(e.architecture)) {
sysinfo.put("architecture", e.architecture);
changes++;
}
if (current == null || !current.sysName.equals(e.sysName)) {
sysinfo.put("name", e.sysName);
changes++;
}
if (current == null || !current.sysVersion.equals(e.sysVersion)) {
sysinfo.put("version", e.sysVersion);
changes++;
}
if (current != null && changes == 0) {
return null;
}
sysinfo.put("_v", 1);
return sysinfo;
}
private static JSONObject getGeckoInfo(Environment e, Environment current) throws JSONException {
JSONObject gecko = new JSONObject();
int changes = 0;
if (current == null || !current.vendor.equals(e.vendor)) {
gecko.put("vendor", e.vendor);
changes++;
}
if (current == null || !current.appName.equals(e.appName)) {
gecko.put("name", e.appName);
changes++;
}
if (current == null || !current.appID.equals(e.appID)) {
gecko.put("id", e.appID);
changes++;
}
if (current == null || !current.appVersion.equals(e.appVersion)) {
gecko.put("version", e.appVersion);
changes++;
}
if (current == null || !current.appBuildID.equals(e.appBuildID)) {
gecko.put("appBuildID", e.appBuildID);
changes++;
}
if (current == null || !current.platformVersion.equals(e.platformVersion)) {
gecko.put("platformVersion", e.platformVersion);
changes++;
}
if (current == null || !current.platformBuildID.equals(e.platformBuildID)) {
gecko.put("platformBuildID", e.platformBuildID);
changes++;
}
if (current == null || !current.os.equals(e.os)) {
gecko.put("os", e.os);
changes++;
}
if (current == null || !current.xpcomabi.equals(e.xpcomabi)) {
gecko.put("xpcomabi", e.xpcomabi);
changes++;
}
if (current == null || !current.updateChannel.equals(e.updateChannel)) {
gecko.put("updateChannel", e.updateChannel);
changes++;
}
if (current != null && changes == 0) {
return null;
}
gecko.put("_v", 1);
return gecko;
}
// Null-safe string comparison.
private static boolean stringsDiffer(final String a, final String b) {
if (a == null) {
return b != null;
}
return !a.equals(b);
}
@SuppressWarnings("fallthrough")
private static JSONObject getAppInfo(Environment e, Environment current) throws JSONException {
JSONObject appinfo = new JSONObject();
Logger.debug(LOG_TAG, "Generating appinfo for v" + e.version + " env " + e.hash);
// Is the environment in question newer than the diff target, or is
// there no diff target?
final boolean outdated = current == null ||
e.version > current.version;
// Is the environment in question a different version (lower or higher),
// or is there no diff target?
final boolean differ = outdated || current.version > e.version;
// Always produce an output object if there's a version mismatch or this
// isn't a diff. Otherwise, track as we go if there's any difference.
boolean changed = differ;
switch (e.version) {
// There's a straightforward correspondence between environment versions
// and appinfo versions.
case 3:
case 2:
appinfo.put("_v", 3);
break;
case 1:
appinfo.put("_v", 2);
break;
default:
Logger.warn(LOG_TAG, "Unknown environment version: " + e.version);
return appinfo;
}
switch (e.version) {
case 3:
case 2:
if (populateAppInfoV2(appinfo, e, current, outdated)) {
changed = true;
}
// Fall through.
case 1:
// There is no older version than v1, so don't check outdated.
if (populateAppInfoV1(e, current, appinfo)) {
changed = true;
}
}
if (!changed) {
return null;
}
return appinfo;
}
private static boolean populateAppInfoV1(Environment e,
Environment current,
JSONObject appinfo)
throws JSONException {
boolean changes = false;
if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) {
appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled);
changes = true;
}
if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) {
appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled);
changes = true;
}
return changes;
}
private static boolean populateAppInfoV2(JSONObject appinfo,
Environment e,
Environment current,
final boolean outdated)
throws JSONException {
boolean changes = false;
if (outdated ||
stringsDiffer(current.osLocale, e.osLocale)) {
appinfo.put("osLocale", e.osLocale);
changes = true;
}
if (outdated ||
stringsDiffer(current.appLocale, e.appLocale)) {
appinfo.put("appLocale", e.appLocale);
changes = true;
}
if (outdated ||
stringsDiffer(current.distribution, e.distribution)) {
appinfo.put("distribution", e.distribution);
changes = true;
}
if (outdated ||
current.acceptLangSet != e.acceptLangSet) {
appinfo.put("acceptLangIsUserSet", e.acceptLangSet);
changes = true;
}
return changes;
}
private static JSONObject getAddonCounts(Environment e, Environment current) throws JSONException {
JSONObject counts = new JSONObject();
int changes = 0;
if (current == null || current.extensionCount != e.extensionCount) {
counts.put("extension", e.extensionCount);
changes++;
}
if (current == null || current.pluginCount != e.pluginCount) {
counts.put("plugin", e.pluginCount);
changes++;
}
if (current == null || current.themeCount != e.themeCount) {
counts.put("theme", e.themeCount);
changes++;
}
if (current != null && changes == 0) {
return null;
}
counts.put("_v", 1);
return counts;
}
/**
* Compute the *tree* difference set between the two objects. If the two
* objects are identical, returns <code>null</code>. If <code>from</code> is
* <code>null</code>, returns <code>to</code>. If <code>to</code> is
* <code>null</code>, behaves as if <code>to</code> were an empty object.
*
* (Note that this method does not check for {@link JSONObject#NULL}, because
* by definition it can't be provided as input to this method.)
*
* This behavior is intended to simplify life for callers: a missing object
* can be viewed as (and behaves as) an empty map, to a useful extent, rather
* than throwing an exception.
*
* @param from
* a JSONObject.
* @param to
* a JSONObject.
* @param includeNull
* if true, keys present in <code>from</code> but not in
* <code>to</code> are included as {@link JSONObject#NULL} in the
* output.
*
* @return a JSONObject, or null if the two objects are identical.
* @throws JSONException
* should not occur, but...
*/
public static JSONObject diff(JSONObject from,
JSONObject to,
boolean includeNull) throws JSONException {
if (from == null) {
return to;
}
if (to == null) {
return diff(from, new JSONObject(), includeNull);
}
JSONObject out = new JSONObject();
HashSet<String> toKeys = includeNull ? new HashSet<String>(to.length())
: null;
@SuppressWarnings("unchecked")
Iterator<String> it = to.keys();
while (it.hasNext()) {
String key = it.next();
// Track these as we go if we'll need them later.
if (includeNull) {
toKeys.add(key);
}
Object value = to.get(key);
if (!from.has(key)) {
// It must be new.
out.put(key, value);
continue;
}
// Not new? Then see if it changed.
Object old = from.get(key);
// Two JSONObjects should be diffed.
if (old instanceof JSONObject && value instanceof JSONObject) {
JSONObject innerDiff = diff(((JSONObject) old), ((JSONObject) value),
includeNull);
// No change? No output.
if (innerDiff == null) {
continue;
}
// Otherwise include the diff.
out.put(key, innerDiff);
continue;
}
// A regular value, or a type change. Only skip if they're the same.
if (value.equals(old)) {
continue;
}
out.put(key, value);
}
// Now -- if requested -- include any removed keys.
if (includeNull) {
Set<String> fromKeys = HealthReportUtils.keySet(from);
fromKeys.removeAll(toKeys);
for (String notPresent : fromKeys) {
out.put(notPresent, JSONObject.NULL);
}
}
if (out.length() == 0) {
return null;
}
return out;
}
private static JSONObject getActiveAddons(Environment e, Environment current) throws JSONException {
// Just return the current add-on set, with a version annotation.
// To do so requires copying.
if (current == null) {
JSONObject out = e.getNonIgnoredAddons();
if (out == null) {
Logger.warn(LOG_TAG, "Null add-ons to return in FHR document. Returning {}.");
out = new JSONObject(); // So that we always return something.
}
out.put("_v", 1);
return out;
}
// Otherwise, return the diff.
JSONObject diff = diff(current.getNonIgnoredAddons(), e.getNonIgnoredAddons(), true);
if (diff == null) {
return null;
}
if (diff == e.addons) {
// Again, needs to copy.
return getActiveAddons(e, null);
}
diff.put("_v", 1);
return diff;
}
}

View File

@ -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.background.healthreport;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.DatabaseEnvironment;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
/**
* This is a {@link ContentProvider} wrapper around a database-backed Health
* Report storage layer.
*
* It stores environments, fields, and measurements, and events which refer to
* each of these by integer ID.
*
* Insert = daily discrete.
* content://org.mozilla.gecko.health/events/env/measurement/v/field
*
* Update = daily last or daily counter
* content://org.mozilla.gecko.health/events/env/measurement/v/field/counter
* content://org.mozilla.gecko.health/events/env/measurement/v/field/last
*
* Delete = drop today's row
* content://org.mozilla.gecko.health/events/env/measurement/v/field/
*
* Query, of course: content://org.mozilla.gecko.health/events/?since
*
* Each operation accepts an optional `time` query parameter, formatted as
* milliseconds since epoch. If omitted, it defaults to the current time.
*
* Each operation also accepts mandatory `profilePath` and `env` arguments.
*
* TODO: document measurements.
*/
public class HealthReportProvider extends ContentProvider {
private HealthReportDatabases databases;
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
public static final String HEALTH_AUTHORITY = HealthReportConstants.HEALTH_AUTHORITY;
// URI matches.
private static final int ENVIRONMENTS_ROOT = 10;
private static final int EVENTS_ROOT = 11;
private static final int EVENTS_RAW_ROOT = 12;
private static final int FIELDS_ROOT = 13;
private static final int MEASUREMENTS_ROOT = 14;
private static final int EVENTS_FIELD_GENERIC = 20;
private static final int EVENTS_FIELD_COUNTER = 21;
private static final int EVENTS_FIELD_LAST = 22;
private static final int ENVIRONMENT_DETAILS = 30;
private static final int FIELDS_MEASUREMENT = 31;
static {
uriMatcher.addURI(HEALTH_AUTHORITY, "environments/", ENVIRONMENTS_ROOT);
uriMatcher.addURI(HEALTH_AUTHORITY, "events/", EVENTS_ROOT);
uriMatcher.addURI(HEALTH_AUTHORITY, "rawevents/", EVENTS_RAW_ROOT);
uriMatcher.addURI(HEALTH_AUTHORITY, "fields/", FIELDS_ROOT);
uriMatcher.addURI(HEALTH_AUTHORITY, "measurements/", MEASUREMENTS_ROOT);
uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*", EVENTS_FIELD_GENERIC);
uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*/counter", EVENTS_FIELD_COUNTER);
uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*/last", EVENTS_FIELD_LAST);
uriMatcher.addURI(HEALTH_AUTHORITY, "environments/#", ENVIRONMENT_DETAILS);
uriMatcher.addURI(HEALTH_AUTHORITY, "fields/*/#", FIELDS_MEASUREMENT);
}
/**
* So we can bypass the ContentProvider layer.
*/
public HealthReportDatabaseStorage getProfileStorage(final String profilePath) {
if (profilePath == null) {
throw new IllegalArgumentException("profilePath must be provided.");
}
return databases.getDatabaseHelperForProfile(new File(profilePath));
}
private HealthReportDatabaseStorage getProfileStorageForUri(Uri uri) {
final String profilePath = uri.getQueryParameter("profilePath");
return getProfileStorage(profilePath);
}
@Override
public void onLowMemory() {
// While we could prune the database here, it wouldn't help - it would restore disk space
// rather then lower our RAM usage. Additionally, pruning the database may use even more
// memory and take too long to run in this method.
super.onLowMemory();
databases.closeDatabaseHelpers();
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public boolean onCreate() {
databases = new HealthReportDatabases(getContext());
return true;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
int match = uriMatcher.match(uri);
HealthReportDatabaseStorage storage = getProfileStorageForUri(uri);
switch (match) {
case FIELDS_MEASUREMENT:
// The keys of this ContentValues are field names.
List<String> pathSegments = uri.getPathSegments();
String measurement = pathSegments.get(1);
int v = Integer.parseInt(pathSegments.get(2));
storage.ensureMeasurementInitialized(measurement, v, getFieldSpecs(values));
return uri;
case ENVIRONMENTS_ROOT:
DatabaseEnvironment environment = storage.getEnvironment();
environment.init(values);
return ContentUris.withAppendedId(uri, environment.register());
case EVENTS_FIELD_GENERIC:
long time = getTimeFromUri(uri);
int day = storage.getDay(time);
int env = getEnvironmentFromUri(uri);
Field field = getFieldFromUri(storage, uri);
if (!values.containsKey("value")) {
throw new IllegalArgumentException("Must provide ContentValues including 'value' key.");
}
Object object = values.get("value");
if (object instanceof Integer ||
object instanceof Long) {
storage.recordDailyDiscrete(env, day, field.getID(), ((Integer) object).intValue());
} else if (object instanceof String) {
storage.recordDailyDiscrete(env, day, field.getID(), (String) object);
} else {
storage.recordDailyDiscrete(env, day, field.getID(), object.toString());
}
// TODO: eventually we might want to return something more useful than
// the input URI.
return uri;
default:
throw new IllegalArgumentException("Unknown insert URI");
}
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int match = uriMatcher.match(uri);
if (match != EVENTS_FIELD_COUNTER &&
match != EVENTS_FIELD_LAST) {
throw new IllegalArgumentException("Must provide operation for update.");
}
HealthReportStorage storage = getProfileStorageForUri(uri);
long time = getTimeFromUri(uri);
int day = storage.getDay(time);
int env = getEnvironmentFromUri(uri);
Field field = getFieldFromUri(storage, uri);
switch (match) {
case EVENTS_FIELD_COUNTER:
int by = values.containsKey("value") ? values.getAsInteger("value") : 1;
storage.incrementDailyCount(env, day, field.getID(), by);
return 1;
case EVENTS_FIELD_LAST:
Object object = values.get("value");
if (object instanceof Integer ||
object instanceof Long) {
storage.recordDailyLast(env, day, field.getID(), (Integer) object);
} else if (object instanceof String) {
storage.recordDailyLast(env, day, field.getID(), (String) object);
} else {
storage.recordDailyLast(env, day, field.getID(), object.toString());
}
return 1;
default:
// javac's flow control analysis sucks.
return 0;
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int match = uriMatcher.match(uri);
HealthReportStorage storage = getProfileStorageForUri(uri);
switch (match) {
case MEASUREMENTS_ROOT:
storage.deleteMeasurements();
return 1;
case ENVIRONMENTS_ROOT:
storage.deleteEnvironments();
return 1;
default:
throw new IllegalArgumentException();
}
// TODO: more
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
int match = uriMatcher.match(uri);
HealthReportStorage storage = getProfileStorageForUri(uri);
switch (match) {
case EVENTS_ROOT:
return storage.getEventsSince(getTimeFromUri(uri));
case EVENTS_RAW_ROOT:
return storage.getRawEventsSince(getTimeFromUri(uri));
case MEASUREMENTS_ROOT:
return storage.getMeasurementVersions();
case FIELDS_ROOT:
return storage.getFieldVersions();
}
List<String> pathSegments = uri.getPathSegments();
switch (match) {
case ENVIRONMENT_DETAILS:
return storage.getEnvironmentRecordForID(Integer.parseInt(pathSegments.get(1), 10));
case FIELDS_MEASUREMENT:
String measurement = pathSegments.get(1);
int v = Integer.parseInt(pathSegments.get(2));
return storage.getFieldVersions(measurement, v);
default:
return null;
}
}
private static long getTimeFromUri(final Uri uri) {
String t = uri.getQueryParameter("time");
if (t == null) {
return System.currentTimeMillis();
} else {
return Long.parseLong(t, 10);
}
}
private static int getEnvironmentFromUri(final Uri uri) {
return Integer.parseInt(uri.getPathSegments().get(1), 10);
}
/**
* Assumes a URI structured like:
*
* <code>content://org.mozilla.gecko.health/events/env/measurement/v/field</code>
*
* @param uri a URI formatted as expected.
* @return a {@link Field} instance.
*/
private static Field getFieldFromUri(HealthReportStorage storage, final Uri uri) {
String measurement;
String field;
int measurementVersion;
List<String> pathSegments = uri.getPathSegments();
measurement = pathSegments.get(2);
measurementVersion = Integer.parseInt(pathSegments.get(3), 10);
field = pathSegments.get(4);
return storage.getField(measurement, measurementVersion, field);
}
private MeasurementFields getFieldSpecs(ContentValues values) {
final ArrayList<FieldSpec> specs = new ArrayList<FieldSpec>(values.size());
for (Entry<String, Object> entry : values.valueSet()) {
specs.add(new FieldSpec(entry.getKey(), (Integer) entry.getValue()));
}
return new MeasurementFields() {
@Override
public Iterable<FieldSpec> getFields() {
return specs;
}
};
}
}

View File

@ -1,238 +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.background.healthreport;
import org.json.JSONObject;
import android.database.Cursor;
import android.util.SparseArray;
/**
* Abstraction over storage for Firefox Health Report on Android.
*/
public interface HealthReportStorage {
// Right now we only care about the name of the field.
public interface MeasurementFields {
public class FieldSpec {
public final String name;
public final int type;
public FieldSpec(String name, int type) {
this.name = name;
this.type = type;
}
}
Iterable<FieldSpec> getFields();
}
public abstract class Field {
protected static final int UNKNOWN_TYPE_OR_FIELD_ID = -1;
protected static final int FLAG_INTEGER = 1 << 0;
protected static final int FLAG_STRING = 1 << 1;
protected static final int FLAG_JSON = 1 << 2;
protected static final int FLAG_DISCRETE = 1 << 8;
protected static final int FLAG_LAST = 1 << 9;
protected static final int FLAG_COUNTER = 1 << 10;
protected static final int FLAG_COUNTED = 1 << 14;
public static final int TYPE_INTEGER_DISCRETE = FLAG_INTEGER | FLAG_DISCRETE;
public static final int TYPE_INTEGER_LAST = FLAG_INTEGER | FLAG_LAST;
public static final int TYPE_INTEGER_COUNTER = FLAG_INTEGER | FLAG_COUNTER;
public static final int TYPE_STRING_DISCRETE = FLAG_STRING | FLAG_DISCRETE;
public static final int TYPE_STRING_LAST = FLAG_STRING | FLAG_LAST;
public static final int TYPE_JSON_DISCRETE = FLAG_JSON | FLAG_DISCRETE;
public static final int TYPE_JSON_LAST = FLAG_JSON | FLAG_LAST;
public static final int TYPE_COUNTED_STRING_DISCRETE = FLAG_COUNTED | TYPE_STRING_DISCRETE;
protected int fieldID = UNKNOWN_TYPE_OR_FIELD_ID;
protected int flags;
protected final String measurementName;
protected final String measurementVersion;
protected final String fieldName;
public Field(String mName, int mVersion, String fieldName, int type) {
this.measurementName = mName;
this.measurementVersion = Integer.toString(mVersion, 10);
this.fieldName = fieldName;
this.flags = type;
}
/**
* @return the ID for this <code>Field</code>
* @throws IllegalStateException if this field is not found in storage
*/
public abstract int getID() throws IllegalStateException;
public boolean isIntegerField() {
return (this.flags & FLAG_INTEGER) > 0;
}
public boolean isStringField() {
return (this.flags & FLAG_STRING) > 0;
}
public boolean isJSONField() {
return (this.flags & FLAG_JSON) > 0;
}
public boolean isStoredAsString() {
return (this.flags & (FLAG_JSON | FLAG_STRING)) > 0;
}
public boolean isDiscreteField() {
return (this.flags & FLAG_DISCRETE) > 0;
}
/**
* True if the accrued values are intended to be bucket-counted. For strings,
* each discrete value will name a bucket, with the number of instances per
* day being the value in the bucket.
*/
public boolean isCountedField() {
return (this.flags & FLAG_COUNTED) > 0;
}
}
/**
* Close open storage handles and otherwise finish up.
*/
public void close();
/**
* Return the day integer corresponding to the provided time.
*
* @param time
* milliseconds since Unix epoch.
* @return an integer day.
*/
public int getDay(long time);
/**
* Return the day integer corresponding to the current time.
*
* @return an integer day.
*/
public int getDay();
/**
* Return a new {@link Environment}, suitable for being populated, hashed, and
* registered.
*
* @return a new {@link Environment} instance.
*/
public Environment getEnvironment();
/**
* @return a mapping from environment IDs to hashes, suitable for use in
* payload generation.
*/
public SparseArray<String> getEnvironmentHashesByID();
/**
* @return a mapping from environment IDs to registered {@link Environment}
* records, suitable for use in payload generation.
*/
public SparseArray<Environment> getEnvironmentRecordsByID();
/**
* @param id
* the environment ID, as returned by {@link Environment#register()}.
* @return a cursor for the record.
*/
public Cursor getEnvironmentRecordForID(int id);
/**
* @param measurement
* the name of a measurement, such as "org.mozilla.appInfo.appInfo".
* @param measurementVersion
* the version of a measurement, such as '3'.
* @param fieldName
* the name of a field, such as "platformVersion".
*
* @return a {@link Field} instance corresponding to the provided values.
*/
public Field getField(String measurement, int measurementVersion,
String fieldName);
/**
* @return a mapping from field IDs to {@link Field} instances, suitable for
* use in payload generation.
*/
public SparseArray<Field> getFieldsByID();
public void recordDailyLast(int env, int day, int field, JSONObject value);
public void recordDailyLast(int env, int day, int field, String value);
public void recordDailyLast(int env, int day, int field, int value);
public void recordDailyDiscrete(int env, int day, int field, JSONObject value);
public void recordDailyDiscrete(int env, int day, int field, String value);
public void recordDailyDiscrete(int env, int day, int field, int value);
public void incrementDailyCount(int env, int day, int field, int by);
public void incrementDailyCount(int env, int day, int field);
/**
* Return true if events exist that were recorded on or after <code>time</code>.
*/
boolean hasEventSince(long time);
/**
* Obtain a cursor over events that were recorded since <code>time</code>.
* This cursor exposes 'raw' events, with integer identifiers for values.
*/
public Cursor getRawEventsSince(long time);
/**
* Obtain a cursor over events that were recorded since <code>time</code>.
*
* This cursor exposes 'friendly' events, with string names and full
* measurement metadata.
*/
public Cursor getEventsSince(long time);
/**
* Ensure that a measurement and all of its fields are registered with the DB.
* No fields will be processed if the measurement exists with the specified
* version.
*
* @param measurement
* a measurement name, such as "org.mozilla.appInfo.appInfo".
* @param version
* a version number, such as '3'.
* @param fields
* a {@link MeasurementFields} instance, consisting of a collection
* of field names.
*/
public void ensureMeasurementInitialized(String measurement,
int version,
MeasurementFields fields);
public Cursor getMeasurementVersions();
public Cursor getFieldVersions();
public Cursor getFieldVersions(String measurement, int measurementVersion);
public void deleteEverything();
public void deleteEnvironments();
public void deleteMeasurements();
/**
* Deletes all environments, addons, and events from the database before the given time.
*
* @param time milliseconds since epoch.
* @param curEnv The ID of the current environment.
* @return The number of environments and addon entries deleted.
*/
public int deleteDataBefore(final long time, final int curEnv);
public int getEventCount();
public int getEnvironmentCount();
public void pruneEvents(final int num);
public void pruneEnvironments(final int num);
public void enqueueOperation(Runnable runnable);
}

View File

@ -1,136 +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.background.healthreport;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.apache.commons.codec.digest.DigestUtils;
import android.content.ContentUris;
import android.net.Uri;
public class HealthReportUtils {
public static final String LOG_TAG = HealthReportUtils.class.getSimpleName();
public static String getEnvironmentHash(final String input) {
return DigestUtils.shaHex(input);
}
/**
* Take an environment URI (one that identifies an environment) and produce an
* event URI.
*
* That this is needed is tragic.
*
* @param environmentURI
* the {@link Uri} returned by an environment operation.
* @return a {@link Uri} to which insertions can be dispatched.
*/
public static Uri getEventURI(Uri environmentURI) {
return environmentURI.buildUpon().path("/events/" + ContentUris.parseId(environmentURI) + "/").build();
}
/**
* Copy the keys from the provided {@link JSONObject} into the provided {@link Set}.
*/
private static <T extends Set<String>> T intoKeySet(T keys, JSONObject o) {
if (o == null || o == JSONObject.NULL) {
return keys;
}
@SuppressWarnings("unchecked")
Iterator<String> it = o.keys();
while (it.hasNext()) {
keys.add(it.next());
}
return keys;
}
/**
* Produce a {@link SortedSet} containing the string keys of the provided
* object.
*
* @param o a {@link JSONObject} with string keys.
* @return a sorted set.
*/
public static SortedSet<String> sortedKeySet(JSONObject o) {
return intoKeySet(new TreeSet<String>(), o);
}
/**
* Produce a {@link Set} containing the string keys of the provided object.
* @param o a {@link JSONObject} with string keys.
* @return an unsorted set.
*/
public static Set<String> keySet(JSONObject o) {
return intoKeySet(new HashSet<String>(), o);
}
/**
* Just like {@link JSONObject#accumulate(String, Object)}, but doesn't do the wrong thing for single values.
* @throws JSONException
*/
public static void append(JSONObject o, String key, Object value) throws JSONException {
if (!o.has(key)) {
JSONArray arr = new JSONArray();
arr.put(value);
o.put(key, arr);
return;
}
Object dest = o.get(key);
if (dest instanceof JSONArray) {
((JSONArray) dest).put(value);
return;
}
JSONArray arr = new JSONArray();
arr.put(dest);
arr.put(value);
o.put(key, arr);
}
/**
* Accumulate counts for how often each provided value occurs.
*
* <code>
* HealthReportUtils.count(o, "foo", "bar");
* </code>
*
* will change
*
* <pre>
* {"foo", {"bar": 1}}
* </pre>
*
* into
*
* <pre>
* {"foo", {"bar": 2}}
* </pre>
*
*/
public static void count(JSONObject o, String key,
String value) throws JSONException {
if (!o.has(key)) {
JSONObject counts = new JSONObject();
counts.put(value, 1);
o.put(key, counts);
return;
}
JSONObject dest = o.getJSONObject(key);
dest.put(value, dest.optInt(value, 0) + 1);
}
public static String generateDocumentId() {
return UUID.randomUUID().toString();
}
}

View File

@ -1,386 +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.background.healthreport;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.Scanner;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ProfileInformationProvider;
/**
* There are some parts of the FHR environment that can't be readily computed
* without a running Gecko -- add-ons, for example. In order to make this
* information available without launching Gecko, we persist it on Fennec
* startup. This class is the notepad in which we write.
*/
public class ProfileInformationCache implements ProfileInformationProvider {
private static final String LOG_TAG = "GeckoProfileInfo";
private static final String CACHE_FILE = "profile_info_cache.json";
/*
* FORMAT_VERSION history:
* -: No version number; implicit v1.
* 1: Add versioning (Bug 878670).
* 2: Bump to regenerate add-on set after landing Bug 900694 (Bug 901622).
* 3: Add distribution, osLocale, appLocale.
* 4: Add experiments as add-ons.
*/
public static final int FORMAT_VERSION = 4;
protected boolean initialized = false;
protected boolean needsWrite = false;
protected final File file;
private volatile boolean blocklistEnabled = true;
private volatile boolean telemetryEnabled = false;
private volatile boolean isAcceptLangUserSet = false;
private volatile long profileCreationTime = 0;
private volatile String distribution = "";
// There are really four kinds of locale in play:
//
// * The OS
// * The Android environment of the app (setDefault)
// * The Gecko locale
// * The requested content locale (Accept-Language).
//
// We track only the first two, assuming that the Gecko locale will typically
// be the same as the app locale.
//
// The app locale is fetched from the PIC because it can be modified at
// runtime -- it won't necessarily be what Locale.getDefaultLocale() returns
// in a fresh non-browser profile.
//
// We also track the OS locale here for the same reason -- we need to store
// the default (OS) value before the locale-switching code takes effect!
private volatile String osLocale = "";
private volatile String appLocale = "";
private volatile JSONObject addons = null;
protected ProfileInformationCache(final File f) {
file = f;
Logger.pii(LOG_TAG, "Using " + file.getAbsolutePath() + " for profile information cache.");
}
public ProfileInformationCache(final String profilePath) {
this(new File(profilePath + File.separator + CACHE_FILE));
}
public synchronized void beginInitialization() {
initialized = false;
needsWrite = true;
}
public JSONObject toJSON() {
JSONObject object = new JSONObject();
try {
object.put("version", FORMAT_VERSION);
object.put("blocklist", blocklistEnabled);
object.put("telemetry", telemetryEnabled);
object.put("isAcceptLangUserSet", isAcceptLangUserSet);
object.put("profileCreated", profileCreationTime);
object.put("osLocale", osLocale);
object.put("appLocale", appLocale);
object.put("distribution", distribution);
object.put("addons", addons);
} catch (JSONException e) {
// There isn't much we can do about this.
// Let's just quietly muffle.
return null;
}
return object;
}
/**
* Attempt to restore this object from a JSON blob. If there is a version mismatch, there has
* likely been an upgrade to the cache format. The cache can be reconstructed without data loss
* so rather than migrating, we invalidate the cache by refusing to store the given JSONObject
* and returning false.
*
* @return false if there's a version mismatch or an error, true on success.
*/
private boolean fromJSON(JSONObject object) throws JSONException {
if (object == null) {
Logger.debug(LOG_TAG, "Can't load restore PIC from null JSON object.");
return false;
}
int version = object.optInt("version", 1);
switch (version) {
case FORMAT_VERSION:
blocklistEnabled = object.getBoolean("blocklist");
telemetryEnabled = object.getBoolean("telemetry");
isAcceptLangUserSet = object.getBoolean("isAcceptLangUserSet");
profileCreationTime = object.getLong("profileCreated");
addons = object.getJSONObject("addons");
distribution = object.getString("distribution");
osLocale = object.getString("osLocale");
appLocale = object.getString("appLocale");
return true;
default:
Logger.warn(LOG_TAG, "Unable to restore from version " + version + " PIC file: expecting " + FORMAT_VERSION);
return false;
}
}
protected JSONObject readFromFile() throws FileNotFoundException, JSONException {
Scanner scanner = null;
try {
scanner = new Scanner(file, "UTF-8").useDelimiter("\\A");
if (!scanner.hasNext()) {
return null;
}
return new JSONObject(scanner.next());
} finally {
if (scanner != null) {
scanner.close();
}
}
}
protected void writeToFile(JSONObject object) throws IOException {
Logger.debug(LOG_TAG, "Writing profile information.");
Logger.pii(LOG_TAG, "Writing to file: " + file.getAbsolutePath());
FileOutputStream stream = new FileOutputStream(file);
OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
try {
writer.append(object.toString());
needsWrite = false;
} finally {
writer.close();
}
}
/**
* Call this <b>on a background thread</b> when you're done adding things.
* @throws IOException if there was a problem serializing or writing the cache to disk.
*/
public synchronized void completeInitialization() throws IOException {
initialized = true;
if (!needsWrite) {
Logger.debug(LOG_TAG, "No write needed.");
return;
}
JSONObject object = toJSON();
if (object == null) {
throw new IOException("Couldn't serialize JSON.");
}
writeToFile(object);
}
/**
* Call this if you're interested in reading.
*
* You should be doing so on a background thread.
*
* @return true if this object was initialized correctly.
*/
public synchronized boolean restoreUnlessInitialized() {
if (initialized) {
return true;
}
if (!file.exists()) {
return false;
}
// One-liner for file reading in Java. So sorry.
Logger.info(LOG_TAG, "Restoring ProfileInformationCache from file.");
Logger.pii(LOG_TAG, "Restoring from file: " + file.getAbsolutePath());
try {
if (!fromJSON(readFromFile())) {
// No need to blow away the file; the caller can eventually overwrite it.
return false;
}
initialized = true;
needsWrite = false;
return true;
} catch (FileNotFoundException e) {
return false;
} catch (JSONException e) {
Logger.warn(LOG_TAG, "Malformed ProfileInformationCache. Not restoring.");
return false;
}
}
private void ensureInitialized() {
if (!initialized) {
throw new IllegalStateException("Not initialized.");
}
}
@Override
public boolean isBlocklistEnabled() {
ensureInitialized();
return blocklistEnabled;
}
public void setBlocklistEnabled(boolean value) {
Logger.debug(LOG_TAG, "Setting blocklist enabled: " + value);
blocklistEnabled = value;
needsWrite = true;
}
@Override
public boolean isTelemetryEnabled() {
ensureInitialized();
return telemetryEnabled;
}
public void setTelemetryEnabled(boolean value) {
Logger.debug(LOG_TAG, "Setting telemetry enabled: " + value);
telemetryEnabled = value;
needsWrite = true;
}
@Override
public boolean isAcceptLangUserSet() {
ensureInitialized();
return isAcceptLangUserSet;
}
public void setAcceptLangUserSet(boolean value) {
Logger.debug(LOG_TAG, "Setting accept-lang as user-set: " + value);
isAcceptLangUserSet = value;
needsWrite = true;
}
@Override
public long getProfileCreationTime() {
ensureInitialized();
return profileCreationTime;
}
public void setProfileCreationTime(long value) {
Logger.debug(LOG_TAG, "Setting profile creation time: " + value);
profileCreationTime = value;
needsWrite = true;
}
@Override
public String getDistributionString() {
ensureInitialized();
return distribution;
}
/**
* Ensure that your arguments are non-null.
*/
public void setDistributionString(String distributionID, String distributionVersion) {
Logger.debug(LOG_TAG, "Setting distribution: " + distributionID + ", " + distributionVersion);
distribution = distributionID + ":" + distributionVersion;
needsWrite = true;
}
@Override
public String getAppLocale() {
ensureInitialized();
return appLocale;
}
public void setAppLocale(String value) {
if (value.equalsIgnoreCase(appLocale)) {
return;
}
Logger.debug(LOG_TAG, "Setting app locale: " + value);
appLocale = value.toLowerCase(Locale.US);
needsWrite = true;
}
@Override
public String getOSLocale() {
ensureInitialized();
return osLocale;
}
public void setOSLocale(String value) {
if (value.equalsIgnoreCase(osLocale)) {
return;
}
Logger.debug(LOG_TAG, "Setting OS locale: " + value);
osLocale = value.toLowerCase(Locale.US);
needsWrite = true;
}
/**
* Update the PIC, if necessary, to match the current locale environment.
*
* @return true if the PIC needed to be updated.
*/
public boolean updateLocales(String osLocale, String appLocale) {
if (this.osLocale.equalsIgnoreCase(osLocale) &&
(appLocale == null || this.appLocale.equalsIgnoreCase(appLocale))) {
return false;
}
this.setOSLocale(osLocale);
if (appLocale != null) {
this.setAppLocale(appLocale);
}
return true;
}
@Override
public JSONObject getAddonsJSON() {
ensureInitialized();
return addons;
}
public void updateJSONForAddon(String id, String json) throws Exception {
addons.put(id, new JSONObject(json));
needsWrite = true;
}
public void removeAddon(String id) {
if (null != addons.remove(id)) {
needsWrite = true;
}
}
/**
* Will throw if you haven't done a full update at least once.
*/
public void updateJSONForAddon(String id, JSONObject json) {
if (addons == null) {
throw new IllegalStateException("Cannot incrementally update add-ons without first initializing.");
}
try {
addons.put(id, json);
needsWrite = true;
} catch (Exception e) {
// Why would this happen?
Logger.warn(LOG_TAG, "Unexpected failure updating JSON for add-on.", e);
}
}
/**
* Update the cached set of add-ons. Throws on invalid input.
*
* @param json a valid add-ons JSON string.
*/
public void setJSONForAddons(String json) throws Exception {
addons = new JSONObject(json);
needsWrite = true;
}
public void setJSONForAddons(JSONObject json) {
addons = json;
needsWrite = true;
}
}

View File

@ -1,90 +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.background.healthreport.prune;
import org.mozilla.gecko.background.BackgroundService;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder;
/**
* A <code>Service</code> to prune unnecessary or excessive health report data.
*
* We extend <code>IntentService</code>, rather than just <code>Service</code>,
* because this gives us a worker thread to avoid excessive main-thread disk access.
*/
public class HealthReportPruneService extends BackgroundService {
public static final String LOG_TAG = HealthReportPruneService.class.getSimpleName();
public static final String WORKER_THREAD_NAME = LOG_TAG + "Worker";
public HealthReportPruneService() {
super(WORKER_THREAD_NAME);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(HealthReportConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
}
@Override
public void onHandleIntent(Intent intent) {
Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
// Intent can be null. Bug 1025937.
if (intent == null) {
Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
return;
}
Logger.debug(LOG_TAG, "Handling prune intent.");
if (!isIntentValid(intent)) {
Logger.warn(LOG_TAG, "Intent not valid - returning.");
return;
}
final String profileName = intent.getStringExtra("profileName");
final String profilePath = intent.getStringExtra("profilePath");
Logger.debug(LOG_TAG, "Ticking for profile " + profileName + " at " + profilePath + ".");
final PrunePolicy policy = getPrunePolicy(profilePath);
policy.tick(System.currentTimeMillis());
}
// Generator function wraps constructor for testing purposes.
protected PrunePolicy getPrunePolicy(final String profilePath) {
final PrunePolicyStorage storage = new PrunePolicyDatabaseStorage(this, profilePath);
return new PrunePolicy(storage, getSharedPreferences());
}
/**
* @param intent must be non-null.
* @return true if the supplied intent contains both profileName and profilePath.
*/
private static boolean isIntentValid(final Intent intent) {
boolean isValid = true;
final String profileName = intent.getStringExtra("profileName");
if (profileName == null) {
Logger.warn(LOG_TAG, "Got intent without profileName.");
isValid = false;
}
final String profilePath = intent.getStringExtra("profilePath");
if (profilePath == null) {
Logger.warn(LOG_TAG, "Got intent without profilePath.");
isValid = false;
}
return isValid;
}
}

View File

@ -1,233 +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.background.healthreport.prune;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import android.content.SharedPreferences;
/**
* Manages scheduling of the pruning of old Firefox Health Report data.
*
* There are three main actions that take place:
* 1) Excessive storage pruning: The recorded data is taking up an unreasonable amount of space.
* 2) Expired data pruning: Data that is kept around longer than is useful.
* 3) Cleanup: To deal with storage maintenance (e.g. bloat and fragmentation)
*
* (1) and (2) are performed periodically on their own schedules. (3) will activate after a
* certain duration but only after (1) or (2) is performed.
*/
public class PrunePolicy {
public static final String LOG_TAG = PrunePolicy.class.getSimpleName();
protected final PrunePolicyStorage storage;
protected final SharedPreferences sharedPreferences;
protected final Editor editor;
public PrunePolicy(final PrunePolicyStorage storage, final SharedPreferences sharedPrefs) {
this.storage = storage;
this.sharedPreferences = sharedPrefs;
this.editor = new Editor(this.sharedPreferences.edit());
}
protected SharedPreferences getSharedPreferences() {
return this.sharedPreferences;
}
public void tick(final long time) {
try {
try {
boolean pruned = attemptPruneBySize(time);
pruned = attemptExpiration(time) || pruned;
// We only need to cleanup after a large pruning.
if (pruned) {
attemptStorageCleanup(time);
}
} catch (Exception e) {
// While catching Exception is ordinarily bad form, this Service runs in the same process
// as Fennec so if we crash, it crashes. Additionally, this Service runs regularly so
// these crashes could be regular. Thus, we choose to quietly fail instead.
Logger.error(LOG_TAG, "Got exception pruning document.", e);
} finally {
editor.commit();
}
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception committing to SharedPreferences.", e);
} finally {
storage.close();
}
}
protected boolean attemptPruneBySize(final long time) {
final long nextPrune = getNextPruneBySizeTime();
if (nextPrune < 0) {
Logger.debug(LOG_TAG, "Initializing prune-by-size time.");
editor.setNextPruneBySizeTime(time + getMinimumTimeBetweenPruneBySizeChecks());
return false;
}
// If the system clock is skewed into the past, making the time between prunes too long, reset
// the clock.
if (nextPrune > getMinimumTimeBetweenPruneBySizeChecks() + time) {
Logger.debug(LOG_TAG, "Clock skew detected - resetting prune-by-size time.");
editor.setNextPruneBySizeTime(time + getMinimumTimeBetweenPruneBySizeChecks());
return false;
}
if (nextPrune > time) {
Logger.debug(LOG_TAG, "Skipping prune-by-size - wait period has not yet elapsed.");
return false;
}
Logger.debug(LOG_TAG, "Attempting prune-by-size.");
// Prune environments first because their cascading deletions may delete some events. These
// environments are pruned in order of least-recently used first. Note that orphaned
// environments are ignored here and should be removed elsewhere.
final int environmentCount = storage.getEnvironmentCount();
if (environmentCount > getMaxEnvironmentCount()) {
final int environmentPruneCount = environmentCount - getEnvironmentCountAfterPrune();
Logger.debug(LOG_TAG, "Pruning " + environmentPruneCount + " environments.");
storage.pruneEnvironments(environmentPruneCount);
}
final int eventCount = storage.getEventCount();
if (eventCount > getMaxEventCount()) {
final int eventPruneCount = eventCount - getEventCountAfterPrune();
Logger.debug(LOG_TAG, "Pruning up to " + eventPruneCount + " events.");
storage.pruneEvents(eventPruneCount);
}
editor.setNextPruneBySizeTime(time + getMinimumTimeBetweenPruneBySizeChecks());
return true;
}
protected boolean attemptExpiration(final long time) {
final long nextPrune = getNextExpirationTime();
if (nextPrune < 0) {
Logger.debug(LOG_TAG, "Initializing expiration time.");
editor.setNextExpirationTime(time + getMinimumTimeBetweenExpirationChecks());
return false;
}
// If the system clock is skewed into the past, making the time between prunes too long, reset
// the clock.
if (nextPrune > getMinimumTimeBetweenExpirationChecks() + time) {
Logger.debug(LOG_TAG, "Clock skew detected - resetting expiration time.");
editor.setNextExpirationTime(time + getMinimumTimeBetweenExpirationChecks());
return false;
}
if (nextPrune > time) {
Logger.debug(LOG_TAG, "Skipping expiration - wait period has not yet elapsed.");
return false;
}
final long oldEventTime = time - getEventExistenceDuration();
Logger.debug(LOG_TAG, "Pruning data older than " + oldEventTime + ".");
storage.deleteDataBefore(oldEventTime);
editor.setNextExpirationTime(time + getMinimumTimeBetweenExpirationChecks());
return true;
}
protected boolean attemptStorageCleanup(final long time) {
// Cleanup if max duration since last cleanup is exceeded.
final long nextCleanup = getNextCleanupTime();
if (nextCleanup < 0) {
Logger.debug(LOG_TAG, "Initializing cleanup time.");
editor.setNextCleanupTime(time + getMinimumTimeBetweenCleanupChecks());
return false;
}
// If the system clock is skewed into the past, making the time between cleanups too long,
// reset the clock.
if (nextCleanup > getMinimumTimeBetweenCleanupChecks() + time) {
Logger.debug(LOG_TAG, "Clock skew detected - resetting cleanup time.");
editor.setNextCleanupTime(time + getMinimumTimeBetweenCleanupChecks());
return false;
}
if (nextCleanup > time) {
Logger.debug(LOG_TAG, "Skipping cleanup - wait period has not yet elapsed.");
return false;
}
editor.setNextCleanupTime(time + getMinimumTimeBetweenCleanupChecks());
Logger.debug(LOG_TAG, "Cleaning up storage.");
storage.cleanup();
return true;
}
protected static class Editor {
protected final SharedPreferences.Editor editor;
public Editor(final SharedPreferences.Editor editor) {
this.editor = editor;
}
public void commit() {
editor.commit();
}
public Editor setNextExpirationTime(final long time) {
editor.putLong(HealthReportConstants.PREF_EXPIRATION_TIME, time);
return this;
}
public Editor setNextPruneBySizeTime(final long time) {
editor.putLong(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME, time);
return this;
}
public Editor setNextCleanupTime(final long time) {
editor.putLong(HealthReportConstants.PREF_CLEANUP_TIME, time);
return this;
}
}
private long getNextExpirationTime() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_EXPIRATION_TIME, -1L);
}
private long getEventExistenceDuration() {
return HealthReportConstants.EVENT_EXISTENCE_DURATION;
}
private long getMinimumTimeBetweenExpirationChecks() {
return HealthReportConstants.MINIMUM_TIME_BETWEEN_EXPIRATION_CHECKS_MILLIS;
}
private long getNextPruneBySizeTime() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME, -1L);
}
private long getMinimumTimeBetweenPruneBySizeChecks() {
return HealthReportConstants.MINIMUM_TIME_BETWEEN_PRUNE_BY_SIZE_CHECKS_MILLIS;
}
private int getMaxEnvironmentCount() {
return HealthReportConstants.MAX_ENVIRONMENT_COUNT;
}
private int getEnvironmentCountAfterPrune() {
return HealthReportConstants.ENVIRONMENT_COUNT_AFTER_PRUNE;
}
private int getMaxEventCount() {
return HealthReportConstants.MAX_EVENT_COUNT;
}
private int getEventCountAfterPrune() {
return HealthReportConstants.EVENT_COUNT_AFTER_PRUNE;
}
private long getNextCleanupTime() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_CLEANUP_TIME, -1L);
}
private long getMinimumTimeBetweenCleanupChecks() {
return HealthReportConstants.MINIMUM_TIME_BETWEEN_CLEANUP_CHECKS_MILLIS;
}
}

View File

@ -1,147 +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.background.healthreport.prune;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.AndroidConfigurationProvider;
import org.mozilla.gecko.background.healthreport.Environment;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
import android.content.ContentProviderClient;
import android.content.Context;
/**
* Abstracts over the Storage instance behind the PrunePolicy. The underlying storage instance is
* a {@link HealthReportDatabaseStorage} instance. Since our cleanup routine vacuums, auto_vacuum
* can be disabled. It is enabled by default, however, turning it off requires an expensive vacuum
* so we wait until our first {@link cleanup} call since we are vacuuming anyway.
*/
public class PrunePolicyDatabaseStorage implements PrunePolicyStorage {
public static final String LOG_TAG = PrunePolicyDatabaseStorage.class.getSimpleName();
private final Context context;
private final String profilePath;
private final ConfigurationProvider config;
private ContentProviderClient client;
private HealthReportDatabaseStorage storage;
private int currentEnvironmentID; // So we don't prune the current environment.
public PrunePolicyDatabaseStorage(final Context context, final String profilePath) {
this.context = context;
this.profilePath = profilePath;
this.config = new AndroidConfigurationProvider(context);
this.currentEnvironmentID = -1;
}
@Override
public void pruneEvents(final int count) {
getStorage().pruneEvents(count);
}
@Override
public void pruneEnvironments(final int count) {
getStorage().pruneEnvironments(count);
// Re-populate the DB and environment cache with the current environment in the unlikely event
// that it was deleted.
this.currentEnvironmentID = -1;
getCurrentEnvironmentID();
}
/**
* Deletes data recorded before the given time. Note that if this method fails to retrieve the
* current environment from the profile cache, it will not delete data so be sure to prune by
* other methods (e.g. {@link pruneEvents}) as well.
*/
@Override
public int deleteDataBefore(final long time) {
return getStorage().deleteDataBefore(time, getCurrentEnvironmentID());
}
@Override
public void cleanup() {
final HealthReportDatabaseStorage storage = getStorage();
// The change to auto_vacuum will only take affect after a vacuum.
storage.disableAutoVacuuming();
storage.vacuum();
}
@Override
public int getEventCount() {
return getStorage().getEventCount();
}
@Override
public int getEnvironmentCount() {
return getStorage().getEnvironmentCount();
}
@Override
public void close() {
if (client != null) {
client.release();
client = null;
}
}
/**
* Retrieves the {@link HealthReportDatabaseStorage} associated with the profile of the policy.
* For efficiency, the underlying {@link ContentProviderClient} and
* {@link HealthReportDatabaseStorage} are cached for later invocations. However, this means a
* call to this method MUST be accompanied by a call to {@link close}. Throws
* {@link IllegalStateException} if the storage instance could not be retrieved - note that the
* {@link ContentProviderClient} instance will not be closed in this case and
* {@link releaseClient} should still be called.
*/
protected HealthReportDatabaseStorage getStorage() {
if (storage != null) {
return storage;
}
client = EnvironmentBuilder.getContentProviderClient(context);
if (client == null) {
// TODO: Record prune failures and submit as part of FHR upload.
Logger.warn(LOG_TAG, "Unable to get ContentProviderClient - throwing.");
throw new IllegalStateException("Unable to get ContentProviderClient.");
}
try {
storage = EnvironmentBuilder.getStorage(client, profilePath);
if (storage == null) {
// TODO: Record prune failures and submit as part of FHR upload.
Logger.warn(LOG_TAG,"Unable to get HealthReportDatabaseStorage for " + profilePath +
" - throwing.");
throw new IllegalStateException("Unable to get HealthReportDatabaseStorage for " +
profilePath + " (== null).");
}
} catch (ClassCastException ex) {
// TODO: Record prune failures and submit as part of FHR upload.
Logger.warn(LOG_TAG,"Unable to get HealthReportDatabaseStorage for " + profilePath +
profilePath + " (ClassCastException).");
throw new IllegalStateException("Unable to get HealthReportDatabaseStorage for " +
profilePath + ".", ex);
}
return storage;
}
protected int getCurrentEnvironmentID() {
if (currentEnvironmentID < 0) {
final ProfileInformationCache cache = new ProfileInformationCache(profilePath);
if (!cache.restoreUnlessInitialized()) {
throw new IllegalStateException("Current environment unknown.");
}
final Environment env = EnvironmentBuilder.getCurrentEnvironment(cache, config);
currentEnvironmentID = env.register();
}
return currentEnvironmentID;
}
}

View File

@ -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.background.healthreport.prune;
/**
* Abstracts over the Storage instance behind the PrunePolicy.
*/
public interface PrunePolicyStorage {
public void pruneEvents(final int count);
public void pruneEnvironments(final int count);
public int deleteDataBefore(final long time);
public void cleanup();
public int getEventCount();
public int getEnvironmentCount();
/**
* Release the resources owned by this helper. MUST be called before this helper is garbage
* collected.
*/
public void close();
}

View File

@ -1,470 +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.background.healthreport.upload;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.SharedPreferences;
import ch.boye.httpclientandroidlib.HttpResponse;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.background.bagheera.BagheeraClient;
import org.mozilla.gecko.background.bagheera.BagheeraRequestDelegate;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.AndroidConfigurationProvider;
import org.mozilla.gecko.background.healthreport.Environment;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.healthreport.HealthReportGenerator;
import org.mozilla.gecko.background.healthreport.HealthReportStorage;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
import org.mozilla.gecko.sync.net.BaseResource;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
public class AndroidSubmissionClient implements SubmissionClient {
protected static final String LOG_TAG = AndroidSubmissionClient.class.getSimpleName();
private static final String MEASUREMENT_NAME_SUBMISSIONS = "org.mozilla.healthreport.submissions";
private static final int MEASUREMENT_VERSION_SUBMISSIONS = 1;
protected final Context context;
protected final SharedPreferences sharedPreferences;
protected final String profilePath;
protected final ConfigurationProvider config;
public AndroidSubmissionClient(Context context, SharedPreferences sharedPreferences, String profilePath) {
this(context, sharedPreferences, profilePath, new AndroidConfigurationProvider(context));
}
public AndroidSubmissionClient(Context context, SharedPreferences sharedPreferences, String profilePath, ConfigurationProvider config) {
this.context = context;
this.sharedPreferences = sharedPreferences;
this.profilePath = profilePath;
this.config = config;
}
public SharedPreferences getSharedPreferences() {
return sharedPreferences;
}
public String getDocumentServerURI() {
return getSharedPreferences().getString(HealthReportConstants.PREF_DOCUMENT_SERVER_URI, HealthReportConstants.DEFAULT_DOCUMENT_SERVER_URI);
}
public String getDocumentServerNamespace() {
return getSharedPreferences().getString(HealthReportConstants.PREF_DOCUMENT_SERVER_NAMESPACE, HealthReportConstants.DEFAULT_DOCUMENT_SERVER_NAMESPACE);
}
public long getLastUploadLocalTime() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, 0L);
}
public String getLastUploadDocumentId() {
return getSharedPreferences().getString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, null);
}
public boolean hasUploadBeenRequested() {
return getSharedPreferences().contains(HealthReportConstants.PREF_LAST_UPLOAD_REQUESTED);
}
public void setLastUploadLocalTimeAndDocumentId(long localTime, String id) {
getSharedPreferences().edit()
.putLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, localTime)
.putString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, id)
.commit();
}
protected HealthReportDatabaseStorage getStorage(final ContentProviderClient client) {
return EnvironmentBuilder.getStorage(client, profilePath);
}
protected JSONObject generateDocument(final long localTime, final long last,
final SubmissionsTracker tracker) throws JSONException {
final long since = localTime - GlobalConstants.MILLISECONDS_PER_SIX_MONTHS;
final HealthReportGenerator generator = tracker.getGenerator();
return generator.generateDocument(since, last, profilePath, config);
}
protected void uploadPayload(String id, String payload, Collection<String> oldIds, BagheeraRequestDelegate uploadDelegate) {
final BagheeraClient client = new BagheeraClient(getDocumentServerURI());
Logger.pii(LOG_TAG, "New health report has id " + id +
"and obsoletes " + (oldIds != null ? Integer.toString(oldIds.size()) : "no") + " old ids.");
try {
client.uploadJSONDocument(getDocumentServerNamespace(),
id,
payload,
oldIds,
uploadDelegate);
} catch (Exception e) {
uploadDelegate.handleError(e);
}
}
@Override
public void upload(long localTime, String id, Collection<String> oldIds, Delegate delegate) {
// We abuse the life-cycle of an Android ContentProvider slightly by holding
// onto a ContentProviderClient while we generate a payload. This keeps our
// database storage alive, and may also allow us to share a database
// connection with a BrowserHealthRecorder from Fennec. The ContentProvider
// owns all underlying Storage instances, so we don't need to explicitly
// close them.
ContentProviderClient client = EnvironmentBuilder.getContentProviderClient(context);
if (client == null) {
// TODO: Bug 910898 - Store client failure in SharedPrefs so we can increment next time with storage.
delegate.onHardFailure(localTime, null, "Could not fetch content provider client.", null);
return;
}
try {
// Storage instance is owned by HealthReportProvider, so we don't need to
// close it. It's worth noting that this call will fail if called
// out-of-process.
final HealthReportDatabaseStorage storage = getStorage(client);
if (storage == null) {
// TODO: Bug 910898 - Store error in SharedPrefs so we can increment next time with storage.
delegate.onHardFailure(localTime, null, "No storage when generating report.", null);
return;
}
long last = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING);
if (!storage.hasEventSince(last)) {
delegate.onHardFailure(localTime, null, "No new events in storage.", null);
return;
}
initializeStorageForUploadProviders(storage);
final SubmissionsTracker tracker =
getSubmissionsTracker(storage, localTime, hasUploadBeenRequested());
try {
// TODO: Bug 910898 - Add errors from sharedPrefs to tracker.
final JSONObject document = generateDocument(localTime, last, tracker);
if (document == null) {
delegate.onHardFailure(localTime, null, "Generator returned null document.", null);
return;
}
final BagheeraRequestDelegate uploadDelegate = tracker.getDelegate(delegate, localTime,
true, id);
this.uploadPayload(id, document.toString(), oldIds, uploadDelegate);
} catch (Exception e) {
// Incrementing the failure count here could potentially cause the failure count to be
// incremented twice, but this helper class checks and prevents this.
tracker.incrementUploadClientFailureCount();
throw e;
}
} catch (Exception e) {
// TODO: Bug 910898 - Store client failure in SharedPrefs so we can increment next time with storage.
Logger.warn(LOG_TAG, "Got exception generating document.", e);
delegate.onHardFailure(localTime, null, "Got exception uploading.", e);
return;
} finally {
client.release();
}
}
protected SubmissionsTracker getSubmissionsTracker(final HealthReportStorage storage,
final long localTime, final boolean hasUploadBeenRequested) {
return new SubmissionsTracker(storage, localTime, hasUploadBeenRequested);
}
@Override
public void delete(final long localTime, final String id, Delegate delegate) {
final BagheeraClient client = new BagheeraClient(getDocumentServerURI());
Logger.pii(LOG_TAG, "Deleting health report with id " + id + ".");
BagheeraRequestDelegate deleteDelegate = new RequestDelegate(delegate, localTime, false, id);
try {
client.deleteDocument(getDocumentServerNamespace(), id, deleteDelegate);
} catch (Exception e) {
deleteDelegate.handleError(e);
}
}
protected class RequestDelegate implements BagheeraRequestDelegate {
protected final Delegate delegate;
protected final boolean isUpload;
protected final String methodString;
protected final long localTime;
protected final String id;
public RequestDelegate(Delegate delegate, long localTime, boolean isUpload, String id) {
this.delegate = delegate;
this.localTime = localTime;
this.isUpload = isUpload;
this.methodString = this.isUpload ? "upload" : "delete";
this.id = id;
}
@Override
public String getUserAgent() {
return HealthReportConstants.USER_AGENT;
}
@Override
public void handleSuccess(int status, String namespace, String id, HttpResponse response) {
BaseResource.consumeEntity(response);
if (isUpload) {
setLastUploadLocalTimeAndDocumentId(localTime, id);
}
Logger.debug(LOG_TAG, "Successful " + methodString + " at " + localTime + ".");
delegate.onSuccess(localTime, id);
}
/**
* Bagheera status codes:
*
* 403 Forbidden - Violated access restrictions. Most likely because of the method used.
* 413 Request Too Large - Request payload was larger than the configured maximum.
* 400 Bad Request - Returned if the POST/PUT failed validation in some manner.
* 404 Not Found - Returned if the URI path doesn't exist or if the URI was not in the proper format.
* 500 Server Error - General server error. Someone with access should look at the logs for more details.
*/
@Override
public void handleFailure(int status, String namespace, HttpResponse response) {
BaseResource.consumeEntity(response);
Logger.debug(LOG_TAG, "Failed " + methodString + " at " + localTime + ".");
if (status >= 500) {
delegate.onSoftFailure(localTime, id, "Got status " + status + " from server.", null);
return;
}
// Things are either bad locally (bad payload format, too much data) or
// bad remotely (badly configured server, temporarily unavailable). Try
// again tomorrow.
delegate.onHardFailure(localTime, id, "Got status " + status + " from server.", null);
}
@Override
public void handleError(Exception e) {
Logger.debug(LOG_TAG, "Exception during " + methodString + " at " + localTime + ".", e);
if (e instanceof IOException) {
// Let's assume IO exceptions are Android dropping the network.
delegate.onSoftFailure(localTime, id, "Got exception during " + methodString + ".", e);
return;
}
delegate.onHardFailure(localTime, id, "Got exception during " + methodString + ".", e);
}
};
private void initializeStorageForUploadProviders(HealthReportDatabaseStorage storage) {
storage.beginInitialization();
try {
initializeSubmissionsProvider(storage);
storage.finishInitialization();
} catch (Exception e) {
// TODO: Bug 910898 - Store error in SharedPrefs so we can increment next time with storage.
storage.abortInitialization();
throw new IllegalStateException("Could not initialize storage for upload provider.", e);
}
}
private void initializeSubmissionsProvider(HealthReportDatabaseStorage storage) {
storage.ensureMeasurementInitialized(
MEASUREMENT_NAME_SUBMISSIONS,
MEASUREMENT_VERSION_SUBMISSIONS,
new MeasurementFields() {
@Override
public Iterable<FieldSpec> getFields() {
final ArrayList<FieldSpec> out = new ArrayList<FieldSpec>();
for (SubmissionsFieldName fieldName : SubmissionsFieldName.values()) {
FieldSpec spec = new FieldSpec(fieldName.getName(), Field.TYPE_INTEGER_COUNTER);
out.add(spec);
}
return out;
}
});
}
public static enum SubmissionsFieldName {
FIRST_ATTEMPT("firstDocumentUploadAttempt"),
CONTINUATION_ATTEMPT("continuationDocumentUploadAttempt"),
SUCCESS("uploadSuccess"),
TRANSPORT_FAILURE("uploadTransportFailure"),
SERVER_FAILURE("uploadServerFailure"),
CLIENT_FAILURE("uploadClientFailure");
private final String name;
SubmissionsFieldName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public int getID(HealthReportStorage storage) {
final Field field = storage.getField(MEASUREMENT_NAME_SUBMISSIONS,
MEASUREMENT_VERSION_SUBMISSIONS,
name);
return field.getID();
}
}
/**
* Encapsulates the counting mechanisms for submissions status counts. Ensures multiple failures
* and successes are not recorded for a single instance.
*/
public class SubmissionsTracker {
private final HealthReportStorage storage;
private final ProfileInformationCache profileCache;
private final int day;
private final int envID;
private boolean isUploadStatusCountIncremented;
public SubmissionsTracker(final HealthReportStorage storage, final long localTime,
final boolean hasUploadBeenRequested) throws IllegalStateException {
this.storage = storage;
this.profileCache = getProfileInformationCache();
this.day = storage.getDay(localTime);
this.envID = registerCurrentEnvironment();
this.isUploadStatusCountIncremented = false;
if (!hasUploadBeenRequested) {
incrementFirstUploadAttemptCount();
} else {
incrementContinuationAttemptCount();
}
}
protected ProfileInformationCache getProfileInformationCache() {
final ProfileInformationCache profileCache = new ProfileInformationCache(profilePath);
if (!profileCache.restoreUnlessInitialized()) {
Logger.warn(LOG_TAG, "Not enough profile information to compute current environment.");
throw new IllegalStateException("Could not retrieve current environment.");
}
return profileCache;
}
protected int registerCurrentEnvironment() {
return EnvironmentBuilder.registerCurrentEnvironment(storage, profileCache, config);
}
protected void incrementFirstUploadAttemptCount() {
Logger.debug(LOG_TAG, "Incrementing first upload attempt field.");
storage.incrementDailyCount(envID, day, SubmissionsFieldName.FIRST_ATTEMPT.getID(storage));
}
protected void incrementContinuationAttemptCount() {
Logger.debug(LOG_TAG, "Incrementing continuation upload attempt field.");
storage.incrementDailyCount(envID, day, SubmissionsFieldName.CONTINUATION_ATTEMPT.getID(storage));
}
public void incrementUploadSuccessCount() {
incrementStatusCount(SubmissionsFieldName.SUCCESS.getID(storage), "success");
}
public void incrementUploadClientFailureCount() {
incrementStatusCount(SubmissionsFieldName.CLIENT_FAILURE.getID(storage), "client failure");
}
public void incrementUploadTransportFailureCount() {
incrementStatusCount(SubmissionsFieldName.TRANSPORT_FAILURE.getID(storage), "transport failure");
}
public void incrementUploadServerFailureCount() {
incrementStatusCount(SubmissionsFieldName.SERVER_FAILURE.getID(storage), "server failure");
}
private void incrementStatusCount(final int fieldID, final String countType) {
if (!isUploadStatusCountIncremented) {
Logger.debug(LOG_TAG, "Incrementing upload attempt " + countType + " count.");
storage.incrementDailyCount(envID, day, fieldID);
isUploadStatusCountIncremented = true;
} else {
Logger.warn(LOG_TAG, "Upload status count already incremented - not incrementing " +
countType + " count.");
}
}
public TrackingGenerator getGenerator() {
return new TrackingGenerator();
}
public class TrackingGenerator extends HealthReportGenerator {
public TrackingGenerator() {
super(storage);
}
@Override
public JSONObject generateDocument(long since, long lastPingTime,
String generationProfilePath, ConfigurationProvider providedConfig) throws JSONException {
// Let's make sure we have an accurate locale.
Locales.getLocaleManager().getAndApplyPersistedLocale(context);
final JSONObject document;
// If the given profilePath matches the one we cached for the tracker, use the cached env.
if (profilePath != null && profilePath.equals(generationProfilePath)) {
final Environment environment = getCurrentEnvironment();
document = super.generateDocument(since, lastPingTime, environment);
} else {
document = super.generateDocument(since, lastPingTime, generationProfilePath, providedConfig);
}
if (document == null) {
incrementUploadClientFailureCount();
}
return document;
}
protected Environment getCurrentEnvironment() {
return EnvironmentBuilder.getCurrentEnvironment(profileCache, config);
}
}
public TrackingRequestDelegate getDelegate(final Delegate delegate, final long localTime,
final boolean isUpload, final String id) {
return new TrackingRequestDelegate(delegate, localTime, isUpload, id);
}
public class TrackingRequestDelegate extends RequestDelegate {
public TrackingRequestDelegate(final Delegate delegate, final long localTime,
final boolean isUpload, final String id) {
super(delegate, localTime, isUpload, id);
}
@Override
public void handleSuccess(int status, String namespace, String id, HttpResponse response) {
super.handleSuccess(status, namespace, id, response);
incrementUploadSuccessCount();
}
@Override
public void handleFailure(int status, String namespace, HttpResponse response) {
super.handleFailure(status, namespace, response);
incrementUploadServerFailureCount();
}
@Override
public void handleError(Exception e) {
super.handleError(e);
if (e instanceof IllegalArgumentException ||
e instanceof UnsupportedEncodingException ||
e instanceof URISyntaxException) {
incrementUploadClientFailureCount();
} else {
incrementUploadTransportFailureCount();
}
}
}
}
}

View File

@ -1,94 +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.background.healthreport.upload;
import org.mozilla.gecko.background.BackgroundService;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.IBinder;
/**
* A <code>Service</code> to manage and upload health report data.
*
* We extend <code>IntentService</code>, rather than just <code>Service</code>,
* because this gives us a worker thread to avoid main-thread networking.
*
* Yes, even though we're in an alarm-triggered service, it still counts as
* main-thread.
*/
public class HealthReportUploadService extends BackgroundService {
public static final String LOG_TAG = HealthReportUploadService.class.getSimpleName();
public static final String WORKER_THREAD_NAME = LOG_TAG + "Worker";
public HealthReportUploadService() {
super(WORKER_THREAD_NAME);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(HealthReportConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
}
@Override
public void onHandleIntent(Intent intent) {
Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
// Intent can be null. Bug 1025937.
if (intent == null) {
Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
return;
}
if (HealthReportConstants.UPLOAD_FEATURE_DISABLED) {
Logger.debug(LOG_TAG, "Health report upload feature is compile-time disabled; not handling upload intent.");
return;
}
Logger.debug(LOG_TAG, "Health report upload feature is compile-time enabled; handling upload intent.");
String profileName = intent.getStringExtra("profileName");
String profilePath = intent.getStringExtra("profilePath");
if (profileName == null || profilePath == null) {
Logger.warn(LOG_TAG, "Got intent without profilePath or profileName. Ignoring.");
return;
}
if (!intent.hasExtra("uploadEnabled")) {
Logger.warn(LOG_TAG, "Got intent without uploadEnabled. Ignoring.");
return;
}
// We disabled Health Report uploads in Bug 1230206, because the service is being decommissioned.
// We chose this specific place to turn uploads off because we wish to preserve deletions in the
// interim, and this is the tested code path for when a user turns off upload, but still expects
// deletions to work.
boolean uploadEnabled = false;
// Don't do anything if the device can't talk to the server.
if (!backgroundDataIsEnabled()) {
Logger.debug(LOG_TAG, "Background data is not enabled; skipping.");
return;
}
Logger.pii(LOG_TAG, "Ticking policy for profile " + profileName + " at " + profilePath + ".");
final SharedPreferences sharedPrefs = getSharedPreferences();
final ObsoleteDocumentTracker tracker = new ObsoleteDocumentTracker(sharedPrefs);
SubmissionClient client = new AndroidSubmissionClient(this, sharedPrefs, profilePath);
SubmissionPolicy policy = new SubmissionPolicy(sharedPrefs, client, tracker, uploadEnabled);
final long now = System.currentTimeMillis();
policy.tick(now);
}
}

View File

@ -1,245 +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.background.healthreport.upload;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import android.content.SharedPreferences;
public class ObsoleteDocumentTracker {
public static final String LOG_TAG = ObsoleteDocumentTracker.class.getSimpleName();
protected final SharedPreferences sharedPrefs;
public ObsoleteDocumentTracker(SharedPreferences sharedPrefs) {
this.sharedPrefs = sharedPrefs;
}
protected ExtendedJSONObject getObsoleteIds() {
String s = sharedPrefs.getString(HealthReportConstants.PREF_OBSOLETE_DOCUMENT_IDS_TO_DELETION_ATTEMPTS_REMAINING, null);
if (s == null) {
// It's possible we're migrating an old profile forward.
String lastId = sharedPrefs.getString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, null);
if (lastId == null) {
return new ExtendedJSONObject();
}
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put(lastId, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
setObsoleteIds(ids);
return ids;
}
try {
return ExtendedJSONObject.parseJSONObject(s);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception getting obsolete ids.", e);
return new ExtendedJSONObject();
}
}
/**
* Write obsolete ids to disk.
*
* @param ids to write.
*/
protected void setObsoleteIds(ExtendedJSONObject ids) {
sharedPrefs
.edit()
.putString(HealthReportConstants.PREF_OBSOLETE_DOCUMENT_IDS_TO_DELETION_ATTEMPTS_REMAINING, ids.toString())
.commit();
}
/**
* Remove id from set of obsolete document ids tracked for deletion.
*
* Public for testing.
*
* @param id to stop tracking.
*/
public void removeObsoleteId(String id) {
ExtendedJSONObject ids = getObsoleteIds();
ids.remove(id);
setObsoleteIds(ids);
}
protected void decrementObsoleteId(ExtendedJSONObject ids, String id) {
if (!ids.containsKey(id)) {
return;
}
try {
Long attempts = ids.getLong(id);
if (attempts == null || --attempts < 1) {
ids.remove(id);
} else {
ids.put(id, attempts);
}
} catch (ClassCastException e) {
ids.remove(id);
Logger.info(LOG_TAG, "Got exception decrementing obsolete ids counter.", e);
}
}
/**
* Decrement attempts remaining for id in set of obsolete document ids tracked
* for deletion.
*
* Public for testing.
*
* @param id to decrement attempts.
*/
public void decrementObsoleteIdAttempts(String id) {
ExtendedJSONObject ids = getObsoleteIds();
decrementObsoleteId(ids, id);
setObsoleteIds(ids);
}
public void purgeObsoleteIds(Collection<String> oldIds) {
ExtendedJSONObject ids = getObsoleteIds();
for (String oldId : oldIds) {
ids.remove(oldId);
}
setObsoleteIds(ids);
}
public void decrementObsoleteIdAttempts(Collection<String> oldIds) {
ExtendedJSONObject ids = getObsoleteIds();
for (String oldId : oldIds) {
decrementObsoleteId(ids, oldId);
}
setObsoleteIds(ids);
}
/**
* Sort Longs in decreasing order, moving null and non-Longs to the front.
*
* Public for testing only.
*/
public static class PairComparator implements Comparator<Entry<String, Object>> {
@Override
public int compare(Entry<String, Object> lhs, Entry<String, Object> rhs) {
Object l = lhs.getValue();
Object r = rhs.getValue();
if (!(l instanceof Long)) {
if (!(r instanceof Long)) {
return 0;
}
return -1;
}
if (!(r instanceof Long)) {
return 1;
}
return ((Long) r).compareTo((Long) l);
}
}
/**
* Return a batch of obsolete document IDs that should be deleted next.
*
* Document IDs are long and sending too many in a single request might
* increase the likelihood of POST failures, so we delete a (deterministic)
* subset here.
*
* @return a non-null collection.
*/
public Collection<String> getBatchOfObsoleteIds() {
ExtendedJSONObject ids = getObsoleteIds();
// Sort by increasing order of key values.
List<Entry<String, Object>> pairs = new ArrayList<Entry<String,Object>>(ids.entrySet());
Collections.sort(pairs, new PairComparator());
List<String> batch = new ArrayList<String>(HealthReportConstants.MAXIMUM_DELETIONS_PER_POST);
int i = 0;
while (batch.size() < HealthReportConstants.MAXIMUM_DELETIONS_PER_POST && i < pairs.size()) {
batch.add(pairs.get(i++).getKey());
}
return batch;
}
/**
* Track the given document ID for eventual obsolescence and deletion.
* Obsolete IDs are not known to have been uploaded to the server, so we just
* give a best effort attempt at deleting them
*
* @param id to eventually delete.
*/
public void addObsoleteId(String id) {
ExtendedJSONObject ids = getObsoleteIds();
if (ids.size() >= HealthReportConstants.MAXIMUM_STORED_OBSOLETE_DOCUMENT_IDS) {
// Remove the one that's been tried the most and is least likely to be
// known to be on the server. Since the comparator orders in decreasing
// order, we take the max.
ids.remove(Collections.max(ids.entrySet(), new PairComparator()).getKey());
}
ids.put(id, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
setObsoleteIds(ids);
}
/**
* Track the given document ID for eventual obsolescence and deletion, and
* give it priority since we know this ID has made it to the server, and we
* definitely don't want to orphan it.
*
* @param id to eventually delete.
*/
public void markIdAsUploaded(String id) {
ExtendedJSONObject ids = getObsoleteIds();
ids.put(id, HealthReportConstants.DELETION_ATTEMPTS_PER_KNOWN_TO_BE_ON_SERVER_DOCUMENT_ID);
setObsoleteIds(ids);
}
public boolean hasObsoleteIds() {
return getObsoleteIds().size() > 0;
}
public int numberOfObsoleteIds() {
return getObsoleteIds().size();
}
public String getNextObsoleteId() {
ExtendedJSONObject ids = getObsoleteIds();
if (ids.size() < 1) {
return null;
}
try {
// Delete the one that's most likely to be known to be on the server, and
// that's not been tried as much. Since the comparator orders in
// decreasing order, we take the min.
return Collections.min(ids.entrySet(), new PairComparator()).getKey();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception picking obsolete id to delete.", e);
return null;
}
}
/**
* We want cleaning up documents on the server to be best effort. Purge badly
* formed IDs and cap the number of times we try to delete so that the queue
* doesn't take too long.
*/
public void limitObsoleteIds() {
ExtendedJSONObject ids = getObsoleteIds();
Set<String> keys = new HashSet<String>(ids.keySet()); // Avoid invalidating an iterator.
for (String key : keys) {
Object o = ids.get(key);
if (!(o instanceof Long)) {
continue;
}
if ((Long) o > HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID) {
ids.put(key, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
}
}
setObsoleteIds(ids);
}
}

View File

@ -1,42 +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.background.healthreport.upload;
import java.util.Collection;
public interface SubmissionClient {
public interface Delegate {
/**
* Called in the event of a temporary failure; we should try again soon.
*
* @param localTime milliseconds since the epoch.
* @param id if known; may be null.
* @param reason for failure.
* @param e if there was an exception; may be null.
*/
public void onSoftFailure(long localTime, String id, String reason, Exception e);
/**
* Called in the event of a failure; we should try again, but not today.
*
* @param localTime milliseconds since the epoch.
* @param id if known; may be null.
* @param reason for failure.
* @param e if there was an exception; may be null.
*/
public void onHardFailure(long localTime, String id, String reason, Exception e);
/**
* Success!
*
* @param localTime milliseconds since the epoch.
* @param id is always known; not null.
*/
public void onSuccess(long localTime, String id);
}
public void upload(long localTime, String id, Collection<String> oldIds, Delegate delegate);
public void delete(long localTime, String id, Delegate delegate);
}

View File

@ -1,462 +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.background.healthreport.upload;
import java.net.MalformedURLException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Collection;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import org.mozilla.gecko.background.healthreport.HealthReportUtils;
import org.mozilla.gecko.background.healthreport.upload.SubmissionClient.Delegate;
import android.content.SharedPreferences;
/**
* Manages scheduling of Firefox Health Report data submission.
*
* The rules of data submission are as follows:
*
* 1. Do not submit data more than once every 24 hours.
*
* 2. Try to submit as close to 24 hours apart as possible.
*
* 3. Do not submit too soon after application startup so as to not negatively
* impact performance at startup.
*
* 4. Before first ever data submission, the user should be notified about data
* collection practices.
*
* 5. User should have opportunity to react to this notification before data
* submission.
*
* 6. Display of notification without any explicit user action constitutes
* implicit consent after a certain duration of time.
*
* 7. If data submission fails, try at most 2 additional times before giving up
* on that day's submission.
*
* On Android, items 4, 5, and 6 are addressed by displaying an Android
* notification on first run.
*/
public class SubmissionPolicy {
public static final String LOG_TAG = SubmissionPolicy.class.getSimpleName();
protected final SharedPreferences sharedPreferences;
protected final SubmissionClient client;
protected final boolean uploadEnabled;
protected final ObsoleteDocumentTracker tracker;
public SubmissionPolicy(final SharedPreferences sharedPreferences,
final SubmissionClient client,
final ObsoleteDocumentTracker tracker,
boolean uploadEnabled) {
if (sharedPreferences == null) {
throw new IllegalArgumentException("sharedPreferences must not be null");
}
this.sharedPreferences = sharedPreferences;
this.client = client;
this.tracker = tracker;
this.uploadEnabled = uploadEnabled;
}
/**
* Check what action must happen, advance counters and timestamps, and
* possibly spawn a request to the server.
*
* @param localTime now.
* @return true if a request was spawned; false otherwise.
*/
public boolean tick(final long localTime) {
final long nextUpload = getNextSubmission();
// If the system clock were ever set to a time in the distant future,
// it's possible our next schedule date is far out as well. We know
// we shouldn't schedule for more than a day out, so we reset the next
// scheduled date appropriately. 3 days was chosen to match desktop's
// arbitrary choice.
if (nextUpload >= localTime + 3 * getMinimumTimeBetweenUploads()) {
Logger.warn(LOG_TAG, "Next upload scheduled far in the future; system clock reset? " + nextUpload + " > " + localTime);
// Things are strange, we want to start again but we don't want to stampede.
editor()
.setNextSubmission(localTime + getMinimumTimeBetweenUploads())
.commit();
return false;
}
// Don't upload unless an interval has elapsed.
if (localTime < nextUpload) {
Logger.debug(LOG_TAG, "We uploaded less than an interval ago; skipping. " + nextUpload + " > " + localTime);
return false;
}
if (!uploadEnabled) {
// We only delete (rather than mark as obsolete during upload) when
// uploading is disabled. We try to delete aggressively, since the volume
// of deletes should be very low. But we don't want to send too many
// delete requests at the same time, so we process these one at a time. In
// the future (Bug 872756), we will be able to delete multiple documents
// with one request.
final String obsoleteId = tracker.getNextObsoleteId();
if (obsoleteId == null) {
Logger.debug(LOG_TAG, "Upload disabled and nothing to delete.");
return false;
}
Logger.info(LOG_TAG, "Upload disabled. Deleting obsolete document.");
Editor editor = editor();
editor.setLastDeleteRequested(localTime); // Write committed by delegate.
client.delete(localTime, obsoleteId, new DeleteDelegate(editor));
return true;
}
long firstRun = getFirstRunLocalTime();
if (firstRun < 0) {
firstRun = localTime;
// Make sure we start clean and as soon as possible.
editor()
.setFirstRunLocalTime(firstRun)
.setNextSubmission(localTime + getMinimumTimeBeforeFirstSubmission())
.setCurrentDayFailureCount(0)
.commit();
}
// This case will occur if the nextSubmission time is not set (== -1) but firstRun is.
if (localTime < firstRun + getMinimumTimeBeforeFirstSubmission()) {
Logger.info(LOG_TAG, "Need to wait " + getMinimumTimeBeforeFirstSubmission() + " before first upload.");
return false;
}
// The first upload attempt for a given document submission begins a 24-hour period in which
// the upload will retry upon a soft failure. At the end of this period, the submission
// failure count is reset, ensuring each day's first submission attempt has a zeroed failure
// count. A period may also end on upload success or hard failure.
if (localTime >= getCurrentDayResetTime()) {
editor()
.setCurrentDayResetTime(localTime + getMinimumTimeBetweenUploads())
.setCurrentDayFailureCount(0)
.commit();
}
String id = HealthReportUtils.generateDocumentId();
Collection<String> oldIds = tracker.getBatchOfObsoleteIds();
tracker.addObsoleteId(id);
Editor editor = editor();
editor.setLastUploadRequested(localTime); // Write committed by delegate.
client.upload(localTime, id, oldIds, new UploadDelegate(editor, oldIds));
return true;
}
/**
* Return true if the upload that produced <code>e</code> definitely did not
* produce a new record on the remote server.
*
* @param e
* <code>Exception</code> that upload produced.
* @return true if the server could not have a new record.
*/
protected boolean isLocalException(Exception e) {
return (e instanceof MalformedURLException) ||
(e instanceof SocketException) ||
(e instanceof UnknownHostException);
}
protected class UploadDelegate implements Delegate {
protected final Editor editor;
protected final Collection<String> oldIds;
public UploadDelegate(Editor editor, Collection<String> oldIds) {
this.editor = editor;
this.oldIds = oldIds;
}
@Override
public void onSuccess(long localTime, String id) {
long next = localTime + getMinimumTimeBetweenUploads();
tracker.markIdAsUploaded(id);
tracker.purgeObsoleteIds(oldIds);
editor
.setNextSubmission(next)
.setLastUploadSucceeded(localTime)
.setCurrentDayFailureCount(0)
.clearCurrentDayResetTime() // Set again on the next submission's first upload attempt.
.commit();
if (Logger.LOG_PERSONAL_INFORMATION) {
Logger.pii(LOG_TAG, "Successful upload with id " + id + " obsoleting "
+ oldIds.size() + " old records reported at " + localTime + "; next upload at " + next + ".");
} else {
Logger.info(LOG_TAG, "Successful upload obsoleting " + oldIds.size()
+ " old records reported at " + localTime + "; next upload at " + next + ".");
}
}
@Override
public void onHardFailure(long localTime, String id, String reason, Exception e) {
long next = localTime + getMinimumTimeBetweenUploads();
if (isLocalException(e)) {
Logger.info(LOG_TAG, "Hard failure caused by local exception; not tracking id and not decrementing attempts.");
tracker.removeObsoleteId(id);
} else {
tracker.decrementObsoleteIdAttempts(oldIds);
}
editor
.setNextSubmission(next)
.setLastUploadFailed(localTime)
.setCurrentDayFailureCount(0)
.clearCurrentDayResetTime() // Set again on the next submission's first upload attempt.
.commit();
Logger.warn(LOG_TAG, "Hard failure reported at " + localTime + ": " + reason + " Next upload at " + next + ".", e);
}
@Override
public void onSoftFailure(long localTime, String id, String reason, Exception e) {
int failuresToday = getCurrentDayFailureCount();
Logger.warn(LOG_TAG, "Soft failure reported at " + localTime + ": " + reason + " Previously failed " + failuresToday + " time(s) today.");
if (failuresToday >= getMaximumFailuresPerDay()) {
onHardFailure(localTime, id, "Reached the limit of daily upload attempts: " + failuresToday, e);
return;
}
long next = localTime + getMinimumTimeAfterFailure();
if (isLocalException(e)) {
Logger.info(LOG_TAG, "Soft failure caused by local exception; not tracking id and not decrementing attempts.");
tracker.removeObsoleteId(id);
} else {
tracker.decrementObsoleteIdAttempts(oldIds);
}
editor
.setNextSubmission(next)
.setLastUploadFailed(localTime)
.setCurrentDayFailureCount(failuresToday + 1)
.commit();
Logger.info(LOG_TAG, "Retrying upload at " + next + ".");
}
}
protected class DeleteDelegate implements Delegate {
protected final Editor editor;
public DeleteDelegate(Editor editor) {
this.editor = editor;
}
@Override
public void onSoftFailure(final long localTime, String id, String reason, Exception e) {
long next = localTime + getMinimumTimeBetweenDeletes();
if (isLocalException(e)) {
Logger.info(LOG_TAG, "Soft failure caused by local exception; not decrementing attempts.");
} else {
tracker.decrementObsoleteIdAttempts(id);
}
editor
.setNextSubmission(next)
.setLastDeleteFailed(localTime)
.commit();
if (Logger.LOG_PERSONAL_INFORMATION) {
Logger.info(LOG_TAG, "Got soft failure at " + localTime + " deleting obsolete document with id " + id + ": " + reason + " Trying again later.");
} else {
Logger.info(LOG_TAG, "Got soft failure at " + localTime + " deleting obsolete document: " + reason + " Trying again later.");
}
}
@Override
public void onHardFailure(final long localTime, String id, String reason, Exception e) {
// We're never going to be able to delete this id, so don't keep trying.
long next = localTime + getMinimumTimeBetweenDeletes();
tracker.removeObsoleteId(id);
editor
.setNextSubmission(next)
.setLastDeleteFailed(localTime)
.commit();
if (Logger.LOG_PERSONAL_INFORMATION) {
Logger.warn(LOG_TAG, "Got hard failure at " + localTime + " deleting obsolete document with id " + id + ": " + reason + " Abandoning delete request.", e);
} else {
Logger.warn(LOG_TAG, "Got hard failure at " + localTime + " deleting obsolete document: " + reason + " Abandoning delete request.", e);
}
}
@Override
public void onSuccess(final long localTime, String id) {
long next = localTime + getMinimumTimeBetweenDeletes();
tracker.removeObsoleteId(id);
editor
.setNextSubmission(next)
.setLastDeleteSucceeded(localTime)
.commit();
if (Logger.LOG_PERSONAL_INFORMATION) {
Logger.pii(LOG_TAG, "Deleted an obsolete document with id " + id + " at " + localTime + ".");
} else {
Logger.info(LOG_TAG, "Deleted an obsolete document at " + localTime + ".");
}
}
}
public SharedPreferences getSharedPreferences() {
return this.sharedPreferences;
}
public long getMinimumTimeBetweenUploads() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_BETWEEN_UPLOADS, HealthReportConstants.DEFAULT_MINIMUM_TIME_BETWEEN_UPLOADS);
}
public long getMinimumTimeBeforeFirstSubmission() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION, HealthReportConstants.DEFAULT_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION);
}
public long getMinimumTimeAfterFailure() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_AFTER_FAILURE, HealthReportConstants.DEFAULT_MINIMUM_TIME_AFTER_FAILURE);
}
public long getMaximumFailuresPerDay() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_MAXIMUM_FAILURES_PER_DAY, HealthReportConstants.DEFAULT_MAXIMUM_FAILURES_PER_DAY);
}
// Authoritative.
public long getFirstRunLocalTime() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_FIRST_RUN, -1);
}
// Authoritative.
public long getNextSubmission() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_NEXT_SUBMISSION, -1);
}
// Authoritative.
public int getCurrentDayFailureCount() {
return getSharedPreferences().getInt(HealthReportConstants.PREF_CURRENT_DAY_FAILURE_COUNT, 0);
}
// Authoritative.
public long getCurrentDayResetTime() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_CURRENT_DAY_RESET_TIME, -1);
}
/**
* To avoid writing to disk multiple times, we encapsulate writes in a
* helper class. Be sure to call <code>commit</code> to flush to disk!
*/
protected Editor editor() {
return new Editor(getSharedPreferences().edit());
}
protected static class Editor {
protected final SharedPreferences.Editor editor;
public Editor(SharedPreferences.Editor editor) {
this.editor = editor;
}
public void commit() {
editor.commit();
}
// Authoritative.
public Editor setFirstRunLocalTime(long localTime) {
editor.putLong(HealthReportConstants.PREF_FIRST_RUN, localTime);
return this;
}
// Authoritative.
public Editor setNextSubmission(long localTime) {
editor.putLong(HealthReportConstants.PREF_NEXT_SUBMISSION, localTime);
return this;
}
// Authoritative.
public Editor setCurrentDayFailureCount(int failureCount) {
editor.putInt(HealthReportConstants.PREF_CURRENT_DAY_FAILURE_COUNT, failureCount);
return this;
}
// Authoritative.
public Editor setCurrentDayResetTime(long resetTime) {
editor.putLong(HealthReportConstants.PREF_CURRENT_DAY_RESET_TIME, resetTime);
return this;
}
// Authoritative.
public Editor clearCurrentDayResetTime() {
editor.putLong(HealthReportConstants.PREF_CURRENT_DAY_RESET_TIME, -1);
return this;
}
// Authoritative.
public Editor setLastUploadRequested(long localTime) {
editor.putLong(HealthReportConstants.PREF_LAST_UPLOAD_REQUESTED, localTime);
return this;
}
// Forensics only.
public Editor setLastUploadSucceeded(long localTime) {
editor.putLong(HealthReportConstants.PREF_LAST_UPLOAD_SUCCEEDED, localTime);
return this;
}
// Forensics only.
public Editor setLastUploadFailed(long localTime) {
editor.putLong(HealthReportConstants.PREF_LAST_UPLOAD_FAILED, localTime);
return this;
}
// Forensics only.
public Editor setLastDeleteRequested(long localTime) {
editor.putLong(HealthReportConstants.PREF_LAST_DELETE_REQUESTED, localTime);
return this;
}
// Forensics only.
public Editor setLastDeleteSucceeded(long localTime) {
editor.putLong(HealthReportConstants.PREF_LAST_DELETE_SUCCEEDED, localTime);
return this;
}
// Forensics only.
public Editor setLastDeleteFailed(long localTime) {
editor.putLong(HealthReportConstants.PREF_LAST_DELETE_FAILED, localTime);
return this;
}
}
// Authoritative.
public long getLastUploadRequested() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_REQUESTED, -1);
}
// Forensics only.
public long getLastUploadSucceeded() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_SUCCEEDED, -1);
}
// Forensics only.
public long getLastUploadFailed() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_FAILED, -1);
}
// Forensics only.
public long getLastDeleteRequested() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_DELETE_REQUESTED, -1);
}
// Forensics only.
public long getLastDeleteSucceeded() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_DELETE_SUCCEEDED, -1);
}
// Forensics only.
public long getLastDeleteFailed() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_DELETE_FAILED, -1);
}
public long getMinimumTimeBetweenDeletes() {
return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_BETWEEN_DELETES, HealthReportConstants.DEFAULT_MINIMUM_TIME_BETWEEN_DELETES);
}
}

View File

@ -26,22 +26,6 @@ background_junit3_sources = [
'src/org/mozilla/gecko/background/fxa/TestAccountLoader.java',
'src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java',
'src/org/mozilla/gecko/background/fxa/TestFirefoxAccounts.java',
'src/org/mozilla/gecko/background/healthreport/MockDatabaseEnvironment.java',
'src/org/mozilla/gecko/background/healthreport/MockHealthReportDatabaseStorage.java',
'src/org/mozilla/gecko/background/healthreport/MockHealthReportSQLiteOpenHelper.java',
'src/org/mozilla/gecko/background/healthreport/MockProfileInformationCache.java',
'src/org/mozilla/gecko/background/healthreport/prune/TestHealthReportPruneService.java',
'src/org/mozilla/gecko/background/healthreport/prune/TestPrunePolicyDatabaseStorage.java',
'src/org/mozilla/gecko/background/healthreport/TestEnvironmentBuilder.java',
'src/org/mozilla/gecko/background/healthreport/TestEnvironmentV1HashAppender.java',
'src/org/mozilla/gecko/background/healthreport/TestHealthReportBroadcastService.java',
'src/org/mozilla/gecko/background/healthreport/TestHealthReportDatabaseStorage.java',
'src/org/mozilla/gecko/background/healthreport/TestHealthReportGenerator.java',
'src/org/mozilla/gecko/background/healthreport/TestHealthReportProvider.java',
'src/org/mozilla/gecko/background/healthreport/TestHealthReportSQLiteOpenHelper.java',
'src/org/mozilla/gecko/background/healthreport/TestProfileInformationCache.java',
'src/org/mozilla/gecko/background/healthreport/upload/TestAndroidSubmissionClient.java',
'src/org/mozilla/gecko/background/healthreport/upload/TestHealthReportUploadService.java',
'src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java',
'src/org/mozilla/gecko/background/helpers/BackgroundServiceTestCase.java',
'src/org/mozilla/gecko/background/helpers/DBHelpers.java',
@ -105,7 +89,6 @@ background_junit3_sources = [
'src/org/mozilla/gecko/background/testhelpers/MockRecord.java',
'src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java',
'src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java',
'src/org/mozilla/gecko/background/testhelpers/StubDelegate.java',
'src/org/mozilla/gecko/background/testhelpers/WaitHelper.java',
'src/org/mozilla/gecko/background/testhelpers/WBORepository.java',
]

View File

@ -19,18 +19,6 @@ subsuite = background
[src/org/mozilla/gecko/background/db/TestPasswordsRepository.java]
[src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java]
[src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java]
[src/org/mozilla/gecko/background/healthreport/TestEnvironmentBuilder.java]
[src/org/mozilla/gecko/background/healthreport/TestEnvironmentV1HashAppender.java]
[src/org/mozilla/gecko/background/healthreport/TestHealthReportBroadcastService.java]
[src/org/mozilla/gecko/background/healthreport/TestHealthReportDatabaseStorage.java]
[src/org/mozilla/gecko/background/healthreport/TestHealthReportGenerator.java]
[src/org/mozilla/gecko/background/healthreport/TestHealthReportProvider.java]
[src/org/mozilla/gecko/background/healthreport/TestHealthReportSQLiteOpenHelper.java]
[src/org/mozilla/gecko/background/healthreport/TestProfileInformationCache.java]
[src/org/mozilla/gecko/background/healthreport/prune/TestHealthReportPruneService.java]
[src/org/mozilla/gecko/background/healthreport/prune/TestPrunePolicyDatabaseStorage.java]
[src/org/mozilla/gecko/background/healthreport/upload/TestAndroidSubmissionClient.java]
[src/org/mozilla/gecko/background/healthreport/upload/TestHealthReportUploadService.java]
[src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java]
[src/org/mozilla/gecko/background/sync/TestAccountPickler.java]
[src/org/mozilla/gecko/background/sync/TestClientsStage.java]

View File

@ -1,76 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.DatabaseEnvironment;
public class MockDatabaseEnvironment extends DatabaseEnvironment {
public MockDatabaseEnvironment(HealthReportDatabaseStorage storage, Class<? extends EnvironmentAppender> appender) {
super(storage, appender);
}
public MockDatabaseEnvironment(HealthReportDatabaseStorage storage) {
super(storage);
}
public static class MockEnvironmentAppender extends EnvironmentAppender {
public StringBuilder appended = new StringBuilder();
public MockEnvironmentAppender() {
super();
}
@Override
public void append(String s) {
appended.append(s);
}
@Override
public void append(int v) {
appended.append(v);
}
@Override
public String toString() {
return appended.toString();
}
}
public MockDatabaseEnvironment mockInit(String appVersion) {
profileCreation = 1234;
cpuCount = 2;
memoryMB = 512;
isBlocklistEnabled = 1;
isTelemetryEnabled = 1;
extensionCount = 0;
pluginCount = 0;
themeCount = 0;
architecture = "";
sysName = "";
sysVersion = "";
vendor = "";
appName = "";
appID = "";
this.appVersion = appVersion;
appBuildID = "";
platformVersion = "";
platformBuildID = "";
os = "";
xpcomabi = "";
updateChannel = "";
// v2 fields.
distribution = "";
appLocale = "";
osLocale = "";
acceptLangSet = 0;
version = Environment.CURRENT_VERSION;
return this;
}
}

View File

@ -1,280 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import org.json.JSONObject;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec;
import android.content.ContentValues;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
public class MockHealthReportDatabaseStorage extends HealthReportDatabaseStorage {
public long now = System.currentTimeMillis();
public long getOneDayAgo() {
return now - GlobalConstants.MILLISECONDS_PER_DAY;
}
public int getYesterday() {
return super.getDay(this.getOneDayAgo());
}
public int getToday() {
return super.getDay(now);
}
public int getTomorrow() {
return super.getDay(now + GlobalConstants.MILLISECONDS_PER_DAY);
}
public int getGivenDaysAgo(int numDays) {
return super.getDay(this.getGivenDaysAgoMillis(numDays));
}
public long getGivenDaysAgoMillis(int numDays) {
return now - numDays * GlobalConstants.MILLISECONDS_PER_DAY;
}
public ConcurrentHashMap<String, Integer> getEnvironmentCache() {
return this.envs;
}
public MockHealthReportDatabaseStorage(Context context, File fakeProfileDirectory) {
super(context, fakeProfileDirectory);
}
public SQLiteDatabase getDB() {
return this.helper.getWritableDatabase();
}
@Override
public MockDatabaseEnvironment getEnvironment() {
return new MockDatabaseEnvironment(this);
}
@Override
public int deleteEnvAndEventsBefore(long time, int curEnv) {
return super.deleteEnvAndEventsBefore(time, curEnv);
}
@Override
public int deleteOrphanedEnv(int curEnv) {
return super.deleteOrphanedEnv(curEnv);
}
@Override
public int deleteEventsBefore(String dayString) {
return super.deleteEventsBefore(dayString);
}
@Override
public int deleteOrphanedAddons() {
return super.deleteOrphanedAddons();
}
@Override
public int getIntFromQuery(final String sql, final String[] selectionArgs) {
return super.getIntFromQuery(sql, selectionArgs);
}
/**
* A storage instance prepopulated with dummy data to be used for testing.
*
* Modifying this data directly will cause tests relying on it to fail so use the versioned
* constructor to change the data if it's the desired version. Example:
* <pre>
* if (version >= 3) {
* addVersion3Stuff();
* }
* if (version >= 2) {
* addVersion2Stuff();
* }
* addVersion1Stuff();
* </pre>
*
* Don't forget to increment the {@link MAX_VERSION_USED} constant.
*
* Note that all instances of this class use the same underlying database and so each newly
* created instance will share the same data.
*/
public static class PrepopulatedMockHealthReportDatabaseStorage extends MockHealthReportDatabaseStorage {
// A constant to enforce which version constructor is the maximum used so far.
private int MAX_VERSION_USED = 2;
public String[] measurementNames;
public int[] measurementVers;
public FieldSpecContainer[] fieldSpecContainers;
public int env;
private final JSONObject addonJSON = new JSONObject(
"{ " +
"\"amznUWL2@amazon.com\": { " +
" \"userDisabled\": false, " +
" \"appDisabled\": false, " +
" \"version\": \"1.10\", " +
" \"type\": \"extension\", " +
" \"scope\": 1, " +
" \"foreignInstall\": false, " +
" \"hasBinaryComponents\": false, " +
" \"installDay\": 15269, " +
" \"updateDay\": 15602 " +
"}, " +
"\"jid0-qBnIpLfDFa4LpdrjhAC6vBqN20Q@jetpack\": { " +
" \"userDisabled\": false, " +
" \"appDisabled\": false, " +
" \"version\": \"1.12.1\", " +
" \"type\": \"extension\", " +
" \"scope\": 1, " +
" \"foreignInstall\": false, " +
" \"hasBinaryComponents\": false, " +
" \"installDay\": 15062, " +
" \"updateDay\": 15580 " +
"} " +
"} ");
public static class FieldSpecContainer {
public final FieldSpec counter;
public final FieldSpec discrete;
public final FieldSpec last;
public FieldSpecContainer(FieldSpec counter, FieldSpec discrete, FieldSpec last) {
this.counter = counter;
this.discrete = discrete;
this.last = last;
}
public ArrayList<FieldSpec> asList() {
final ArrayList<FieldSpec> out = new ArrayList<FieldSpec>(3);
out.add(counter);
out.add(discrete);
out.add(last);
return out;
}
}
public PrepopulatedMockHealthReportDatabaseStorage(Context context, File fakeProfileDirectory) throws Exception {
this(context, fakeProfileDirectory, 1);
}
public PrepopulatedMockHealthReportDatabaseStorage(Context context, File fakeProfileDirectory, int version) throws Exception {
super(context, fakeProfileDirectory);
if (version > MAX_VERSION_USED || version < 1) {
throw new IllegalStateException("Invalid version number! Check " +
"PrepopulatedMockHealthReportDatabaseStorage.MAX_VERSION_USED!");
}
measurementNames = new String[2];
measurementNames[0] = "a_string_measurement";
measurementNames[1] = "b_integer_measurement";
measurementVers = new int[2];
measurementVers[0] = 1;
measurementVers[1] = 2;
fieldSpecContainers = new FieldSpecContainer[2];
fieldSpecContainers[0] = new FieldSpecContainer(
new FieldSpec("a_counter_integer_field", Field.TYPE_INTEGER_COUNTER),
new FieldSpec("a_discrete_string_field", Field.TYPE_STRING_DISCRETE),
new FieldSpec("a_last_string_field", Field.TYPE_STRING_LAST));
fieldSpecContainers[1] = new FieldSpecContainer(
new FieldSpec("b_counter_integer_field", Field.TYPE_INTEGER_COUNTER),
new FieldSpec("b_discrete_integer_field", Field.TYPE_INTEGER_DISCRETE),
new FieldSpec("b_last_integer_field", Field.TYPE_INTEGER_LAST));
final MeasurementFields[] measurementFields =
new MeasurementFields[fieldSpecContainers.length];
for (int i = 0; i < fieldSpecContainers.length; i++) {
final FieldSpecContainer fieldSpecContainer = fieldSpecContainers[i];
measurementFields[i] = new MeasurementFields() {
@Override
public Iterable<FieldSpec> getFields() {
return fieldSpecContainer.asList();
}
};
}
this.beginInitialization();
for (int i = 0; i < measurementNames.length; i++) {
this.ensureMeasurementInitialized(measurementNames[i], measurementVers[i],
measurementFields[i]);
}
this.finishInitialization();
MockDatabaseEnvironment environment = this.getEnvironment();
environment.mockInit("v123");
environment.setJSONForAddons(addonJSON);
env = environment.register();
String mName = measurementNames[0];
int mVer = measurementVers[0];
FieldSpecContainer fieldSpecCont = fieldSpecContainers[0];
int fieldID = this.getField(mName, mVer, fieldSpecCont.counter.name).getID();
this.incrementDailyCount(env, this.getGivenDaysAgo(7), fieldID, 1);
this.incrementDailyCount(env, this.getGivenDaysAgo(4), fieldID, 2);
this.incrementDailyCount(env, this.getToday(), fieldID, 3);
fieldID = this.getField(mName, mVer, fieldSpecCont.discrete.name).getID();
this.recordDailyDiscrete(env, this.getGivenDaysAgo(5), fieldID, "five");
this.recordDailyDiscrete(env, this.getGivenDaysAgo(5), fieldID, "five-two");
this.recordDailyDiscrete(env, this.getGivenDaysAgo(2), fieldID, "two");
this.recordDailyDiscrete(env, this.getToday(), fieldID, "zero");
fieldID = this.getField(mName, mVer, fieldSpecCont.last.name).getID();
this.recordDailyLast(env, this.getGivenDaysAgo(6), fieldID, "six");
this.recordDailyLast(env, this.getGivenDaysAgo(3), fieldID, "three");
this.recordDailyLast(env, this.getToday(), fieldID, "zero");
mName = measurementNames[1];
mVer = measurementVers[1];
fieldSpecCont = fieldSpecContainers[1];
fieldID = this.getField(mName, mVer, fieldSpecCont.counter.name).getID();
this.incrementDailyCount(env, this.getGivenDaysAgo(2), fieldID, 2);
fieldID = this.getField(mName, mVer, fieldSpecCont.discrete.name).getID();
this.recordDailyDiscrete(env, this.getToday(), fieldID, 0);
this.recordDailyDiscrete(env, this.getToday(), fieldID, 1);
fieldID = this.getField(mName, mVer, fieldSpecCont.last.name).getID();
this.recordDailyLast(env, this.getYesterday(), fieldID, 1);
if (version >= 2) {
// Insert more diverse environments.
for (int i = 1; i <= 3; i++) {
environment = this.getEnvironment();
environment.mockInit("v" + i);
env = environment.register();
this.recordDailyLast(env, this.getGivenDaysAgo(7 * i + 1), fieldID, 13);
}
environment = this.getEnvironment();
environment.mockInit("v4");
env = environment.register();
this.recordDailyLast(env, this.getGivenDaysAgo(1000), fieldID, 14);
this.recordDailyLast(env, this.getToday(), fieldID, 15);
}
}
public void insertTextualEvents(final int count) {
final ContentValues v = new ContentValues();
v.put("env", env);
final int fieldID = this.getField(measurementNames[0], measurementVers[0],
fieldSpecContainers[0].discrete.name).getID();
v.put("field", fieldID);
v.put("value", "data");
final SQLiteDatabase db = this.helper.getWritableDatabase();
db.beginTransaction();
try {
for (int i = 1; i <= count; i++) {
v.put("date", i);
db.insertOrThrow("events_textual", null, v);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
}
}

View File

@ -1,172 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.HealthReportSQLiteOpenHelper;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
public class MockHealthReportSQLiteOpenHelper extends HealthReportSQLiteOpenHelper {
private int version;
public MockHealthReportSQLiteOpenHelper(Context context, File fakeProfileDirectory, String name) {
super(context, fakeProfileDirectory, name);
version = HealthReportSQLiteOpenHelper.CURRENT_VERSION;
}
public MockHealthReportSQLiteOpenHelper(Context context, File fakeProfileDirectory, String name, int version) {
super(context, fakeProfileDirectory, name, version);
this.version = version;
}
@Override
public void onCreate(SQLiteDatabase db) {
if (version == HealthReportSQLiteOpenHelper.CURRENT_VERSION) {
super.onCreate(db);
} else if (version == 4) {
onCreateSchemaVersion4(db);
} else {
throw new IllegalStateException("Unknown version number, " + version + ".");
}
}
// Copy-pasta from HealthReportDatabaseStorage.onCreate from v4.
public void onCreateSchemaVersion4(SQLiteDatabase db) {
db.beginTransaction();
try {
db.execSQL("CREATE TABLE addons (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" body TEXT, " +
" UNIQUE (body) " +
")");
db.execSQL("CREATE TABLE environments (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" hash TEXT, " +
" profileCreation INTEGER, " +
" cpuCount INTEGER, " +
" memoryMB INTEGER, " +
" isBlocklistEnabled INTEGER, " +
" isTelemetryEnabled INTEGER, " +
" extensionCount INTEGER, " +
" pluginCount INTEGER, " +
" themeCount INTEGER, " +
" architecture TEXT, " +
" sysName TEXT, " +
" sysVersion TEXT, " +
" vendor TEXT, " +
" appName TEXT, " +
" appID TEXT, " +
" appVersion TEXT, " +
" appBuildID TEXT, " +
" platformVersion TEXT, " +
" platformBuildID TEXT, " +
" os TEXT, " +
" xpcomabi TEXT, " +
" updateChannel TEXT, " +
" addonsID INTEGER, " +
" FOREIGN KEY (addonsID) REFERENCES addons(id) ON DELETE RESTRICT, " +
" UNIQUE (hash) " +
")");
db.execSQL("CREATE TABLE measurements (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" name TEXT, " +
" version INTEGER, " +
" UNIQUE (name, version) " +
")");
db.execSQL("CREATE TABLE fields (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" measurement INTEGER, " +
" name TEXT, " +
" flags INTEGER, " +
" FOREIGN KEY (measurement) REFERENCES measurements(id) ON DELETE CASCADE, " +
" UNIQUE (measurement, name)" +
")");
db.execSQL("CREATE TABLE events_integer (" +
" date INTEGER, " +
" env INTEGER, " +
" field INTEGER, " +
" value INTEGER, " +
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
")");
db.execSQL("CREATE TABLE events_textual (" +
" date INTEGER, " +
" env INTEGER, " +
" field INTEGER, " +
" value TEXT, " +
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
")");
db.execSQL("CREATE INDEX idx_events_integer_date_env_field ON events_integer (date, env, field)");
db.execSQL("CREATE INDEX idx_events_textual_date_env_field ON events_textual (date, env, field)");
db.execSQL("CREATE VIEW events AS " +
"SELECT date, env, field, value FROM events_integer " +
"UNION ALL " +
"SELECT date, env, field, value FROM events_textual");
db.execSQL("CREATE VIEW named_events AS " +
"SELECT date, " +
" environments.hash AS environment, " +
" measurements.name AS measurement_name, " +
" measurements.version AS measurement_version, " +
" fields.name AS field_name, " +
" fields.flags AS field_flags, " +
" value FROM " +
"events JOIN environments ON events.env = environments.id " +
" JOIN fields ON events.field = fields.id " +
" JOIN measurements ON fields.measurement = measurements.id");
db.execSQL("CREATE VIEW named_fields AS " +
"SELECT measurements.name AS measurement_name, " +
" measurements.id AS measurement_id, " +
" measurements.version AS measurement_version, " +
" fields.name AS field_name, " +
" fields.id AS field_id, " +
" fields.flags AS field_flags " +
"FROM fields JOIN measurements ON fields.measurement = measurements.id");
db.execSQL("CREATE VIEW current_measurements AS " +
"SELECT name, MAX(version) AS version FROM measurements GROUP BY name");
// createAddonsEnvironmentsView(db):
db.execSQL("CREATE VIEW environments_with_addons AS " +
"SELECT e.id AS id, " +
" e.hash AS hash, " +
" e.profileCreation AS profileCreation, " +
" e.cpuCount AS cpuCount, " +
" e.memoryMB AS memoryMB, " +
" e.isBlocklistEnabled AS isBlocklistEnabled, " +
" e.isTelemetryEnabled AS isTelemetryEnabled, " +
" e.extensionCount AS extensionCount, " +
" e.pluginCount AS pluginCount, " +
" e.themeCount AS themeCount, " +
" e.architecture AS architecture, " +
" e.sysName AS sysName, " +
" e.sysVersion AS sysVersion, " +
" e.vendor AS vendor, " +
" e.appName AS appName, " +
" e.appID AS appID, " +
" e.appVersion AS appVersion, " +
" e.appBuildID AS appBuildID, " +
" e.platformVersion AS platformVersion, " +
" e.platformBuildID AS platformBuildID, " +
" e.os AS os, " +
" e.xpcomabi AS xpcomabi, " +
" e.updateChannel AS updateChannel, " +
" addons.body AS addonsBody " +
"FROM environments AS e, addons " +
"WHERE e.addonsID = addons.id");
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
}

View File

@ -1,44 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
public class MockProfileInformationCache extends ProfileInformationCache {
public MockProfileInformationCache(String profilePath) {
super(profilePath);
}
public MockProfileInformationCache(File mockFile) {
super(mockFile);
}
public boolean isInitialized() {
return this.initialized;
}
public boolean needsWrite() {
return this.needsWrite;
}
public File getFile() {
return this.file;
}
public void writeJSON(JSONObject toWrite) throws IOException {
writeToFile(toWrite);
}
public JSONObject readJSON() throws FileNotFoundException, JSONException {
return readFromFile();
}
public void setInitialized(final boolean initialized) {
this.initialized = initialized;
}
}

View File

@ -1,83 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
public class TestEnvironmentBuilder extends FakeProfileTestCase {
public static void testIgnoringAddons() throws JSONException {
Environment env = new Environment() {
@Override
public int register() {
return 0;
}
};
JSONObject addons = new JSONObject();
JSONObject foo = new JSONObject();
foo.put("a", 1);
foo.put("b", "c");
addons.put("foo", foo);
JSONObject ignore = new JSONObject();
ignore.put("ignore", true);
addons.put("ig", ignore);
env.setJSONForAddons(addons);
JSONObject kept = env.getNonIgnoredAddons();
assertTrue(kept.has("foo"));
assertFalse(kept.has("ig"));
JSONObject fooCopy = kept.getJSONObject("foo");
assertSame(foo, fooCopy);
}
public void testSanity() throws IOException {
File subdir = new File(this.fakeProfileDirectory.getAbsolutePath() +
File.separator + "testPersisting");
subdir.mkdir();
long now = System.currentTimeMillis();
int expectedDays = (int) (now / GlobalConstants.MILLISECONDS_PER_DAY);
MockProfileInformationCache cache = new MockProfileInformationCache(subdir.getAbsolutePath());
assertFalse(cache.getFile().exists());
cache.beginInitialization();
cache.setBlocklistEnabled(true);
cache.setTelemetryEnabled(false);
cache.setProfileCreationTime(now);
cache.completeInitialization();
assertTrue(cache.getFile().exists());
final AndroidConfigurationProvider configProvider = new AndroidConfigurationProvider(context);
Environment environment = EnvironmentBuilder.getCurrentEnvironment(cache, configProvider);
assertEquals(AppConstants.MOZ_APP_BUILDID, environment.appBuildID);
assertEquals("Android", environment.os);
assertTrue(100 < environment.memoryMB); // Seems like a sane lower bound...
assertTrue(environment.cpuCount >= 1);
assertEquals(1, environment.isBlocklistEnabled);
assertEquals(0, environment.isTelemetryEnabled);
assertEquals(expectedDays, environment.profileCreation);
assertEquals(EnvironmentBuilder.getCurrentEnvironment(cache, configProvider).getHash(),
environment.getHash());
// v3 sanity.
assertEquals(configProvider.hasHardwareKeyboard(), environment.hasHardwareKeyboard);
assertEquals(configProvider.getScreenXInMM(), environment.screenXInMM);
assertTrue(1 < environment.screenXInMM);
assertTrue(2000 > environment.screenXInMM);
cache.beginInitialization();
cache.setBlocklistEnabled(false);
cache.completeInitialization();
assertFalse(EnvironmentBuilder.getCurrentEnvironment(cache, configProvider).getHash()
.equals(environment.getHash()));
}
}

View File

@ -1,146 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.LinkedList;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.background.healthreport.EnvironmentV1.EnvironmentAppender;
import org.mozilla.gecko.background.healthreport.EnvironmentV1.HashAppender;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
import org.mozilla.gecko.sync.Utils;
/**
* Tests the HashAppender functionality. Note that these tests must be run on an Android
* device because the SHA-1 native library needs to be loaded.
*/
public class TestEnvironmentV1HashAppender extends FakeProfileTestCase {
// input and expected values via: http://oauth.googlecode.com/svn/code/c/liboauth/src/sha1.c
private final static String[] INPUTS = new String[] {
"abc",
"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
"" // To be filled in below.
};
static {
final String baseStr = "01234567";
final int repetitions = 80;
final StringBuilder builder = new StringBuilder(baseStr.length() * repetitions);
for (int i = 0; i < 80; ++i) {
builder.append(baseStr);
}
INPUTS[2] = builder.toString();
}
private final static String[] EXPECTEDS = new String[] {
"a9993e364706816aba3e25717850c26c9cd0d89d",
"84983e441c3bd26ebaae4aa1f95129e5e54670f1",
"dea356a2cddd90c7a7ecedc5ebb563934f460452"
};
static {
for (int i = 0; i < EXPECTEDS.length; ++i) {
EXPECTEDS[i] = new Base64(-1, null, false).encodeAsString(Utils.hex2Byte(EXPECTEDS[i]));
}
}
public void testSHA1Hashing() throws Exception {
for (int i = 0; i < INPUTS.length; ++i) {
final String input = INPUTS[i];
final String expected = EXPECTEDS[i];
final HashAppender appender = new HashAppender();
addStringToAppenderInParts(appender, input);
final String result = appender.toString();
assertEquals(expected, result);
}
}
/**
* Tests to ensure output is the same as the former MessageDigest implementation (bug 959652).
*/
public void testAgainstMessageDigestImpl() throws Exception {
// List.add doesn't allow add(null) so we make a LinkedList here.
final LinkedList<String> inputs = new LinkedList<String>(Arrays.asList(INPUTS));
inputs.add(null);
for (final String input : inputs) {
final HashAppender hAppender = new HashAppender();
final MessageDigestHashAppender mdAppender = new MessageDigestHashAppender();
hAppender.append(input);
mdAppender.append(input);
final String hResult = hAppender.toString();
final String mdResult = mdAppender.toString();
assertEquals(mdResult, hResult);
}
}
public void testIntegersAgainstMessageDigestImpl() throws Exception {
final int[] INPUTS = {Integer.MIN_VALUE, -1337, -42, 0, 42, 1337, Integer.MAX_VALUE};
for (final int input : INPUTS) {
final HashAppender hAppender = new HashAppender();
final MessageDigestHashAppender mdAppender = new MessageDigestHashAppender();
hAppender.append(input);
mdAppender.append(input);
final String hResult = hAppender.toString();
final String mdResult = mdAppender.toString();
assertEquals(mdResult, hResult);
}
}
private void addStringToAppenderInParts(final EnvironmentAppender appender, final String input) {
int substrInd = 0;
int substrLength = 1;
while (substrInd < input.length()) {
final int endInd = Math.min(substrInd + substrLength, input.length());
appender.append(input.substring(substrInd, endInd));
substrInd = endInd;
++substrLength;
}
}
// --- COPY-PASTA'D CODE, FOR TESTING PURPOSES. ---
public static class MessageDigestHashAppender extends EnvironmentAppender {
final MessageDigest hasher;
public MessageDigestHashAppender() throws NoSuchAlgorithmException {
// Note to the security-minded reader: we deliberately use SHA-1 here, not
// a stronger hash. These identifiers don't strictly need a cryptographic
// hash function, because there is negligible value in attacking the hash.
// We use SHA-1 because it's *shorter* -- the exact same reason that Git
// chose SHA-1.
hasher = MessageDigest.getInstance("SHA-1");
}
@Override
public void append(String s) {
try {
hasher.update(((s == null) ? "null" : s).getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
// This can never occur. Thanks, Java.
}
}
@Override
public void append(int profileCreation) {
append(Integer.toString(profileCreation, 10));
}
@Override
public String toString() {
// We *could* use ASCII85 but the savings would be negated by the
// inclusion of JSON-unsafe characters like double-quote.
return new Base64(-1, null, false).encodeAsString(hasher.digest());
}
}
}

View File

@ -1,145 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.util.concurrent.BrokenBarrierException;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService;
import org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService;
import org.mozilla.gecko.background.helpers.BackgroundServiceTestCase;
import android.content.Intent;
import android.content.SharedPreferences;
public class TestHealthReportBroadcastService
extends BackgroundServiceTestCase<TestHealthReportBroadcastService.MockHealthReportBroadcastService> {
public static class MockHealthReportBroadcastService extends HealthReportBroadcastService {
@Override
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(sharedPrefsName, GlobalConstants.SHARED_PREFERENCES_MODE);
}
@Override
protected void onHandleIntent(Intent intent) {
super.onHandleIntent(intent);
try {
barrier.await();
} catch (InterruptedException e) {
fail("Awaiting Service thread should not be interrupted.");
} catch (BrokenBarrierException e) {
// This will happen on timeout - do nothing.
}
}
}
public TestHealthReportBroadcastService() {
super(MockHealthReportBroadcastService.class);
}
@Override
public void setUp() throws Exception {
super.setUp();
// We can't mock AlarmManager since it has a package-private constructor, so instead we reset
// the alarm by hand.
cancelAlarm(getUploadIntent());
}
@Override
public void tearDown() throws Exception {
cancelAlarm(getUploadIntent());
super.tearDown();
}
protected Intent getUploadIntent() {
final Intent intent = new Intent(getContext(), HealthReportUploadService.class);
intent.setAction("upload");
return intent;
}
protected Intent getPruneIntent() {
final Intent intent = new Intent(getContext(), HealthReportPruneService.class);
intent.setAction("prune");
return intent;
}
public void testIgnoredUploadPrefIntents() throws Exception {
// Intent without "upload" extra is ignored.
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertFalse(isServiceAlarmSet(getUploadIntent()));
barrier.reset();
// No "profileName" extra.
intent.putExtra("enabled", true)
.removeExtra("profileName");
startService(intent);
await();
assertFalse(isServiceAlarmSet(getUploadIntent()));
barrier.reset();
// No "profilePath" extra.
intent.putExtra("profileName", "profileName")
.removeExtra("profilePath");
startService(intent);
await();
assertFalse(isServiceAlarmSet(getUploadIntent()));
}
public void testUploadPrefIntentDisabled() throws Exception {
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
.putExtra("enabled", false)
.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertFalse(isServiceAlarmSet(getUploadIntent()));
}
public void testUploadPrefIntentEnabled() throws Exception {
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
.putExtra("enabled", true)
.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertTrue(isServiceAlarmSet(getUploadIntent()));
}
public void testUploadServiceCancelled() throws Exception {
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
.putExtra("enabled", true)
.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertTrue(isServiceAlarmSet(getUploadIntent()));
barrier.reset();
intent.putExtra("enabled", false);
startService(intent);
await();
assertFalse(isServiceAlarmSet(getUploadIntent()));
}
public void testPruneService() throws Exception {
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_PRUNE)
.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertTrue(isServiceAlarmSet(getPruneIntent()));
barrier.reset();
}
}

View File

@ -1,662 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.util.ArrayList;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.healthreport.HealthReportStorage;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
import org.mozilla.gecko.background.healthreport.MockHealthReportDatabaseStorage.PrepopulatedMockHealthReportDatabaseStorage;
import org.mozilla.gecko.background.helpers.DBHelpers;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
public class TestHealthReportDatabaseStorage extends FakeProfileTestCase {
private String[] TABLE_NAMES = {
"addons",
"environments",
"measurements",
"fields",
"events_integer",
"events_textual"
};
public static class MockMeasurementFields implements MeasurementFields {
@Override
public Iterable<FieldSpec> getFields() {
ArrayList<FieldSpec> fields = new ArrayList<FieldSpec>();
fields.add(new FieldSpec("testfield1", Field.TYPE_INTEGER_COUNTER));
fields.add(new FieldSpec("testfield2", Field.TYPE_INTEGER_COUNTER));
return fields;
}
}
public void testInitializingProvider() {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
storage.beginInitialization();
// Two providers with the same measurement and field names. Shouldn't conflict.
storage.ensureMeasurementInitialized("testpA.testm", 1, new MockMeasurementFields());
storage.ensureMeasurementInitialized("testpB.testm", 2, new MockMeasurementFields());
storage.finishInitialization();
// Now make sure our stuff is in the DB.
SQLiteDatabase db = storage.getDB();
Cursor c = db.query("measurements", new String[] {"id", "name", "version"}, null, null, null, null, "name");
assertTrue(c.moveToFirst());
assertEquals(2, c.getCount());
Object[][] expected = new Object[][] {
{null, "testpA.testm", 1},
{null, "testpB.testm", 2},
};
DBHelpers.assertCursorContains(expected, c);
c.close();
}
private static final JSONObject EXAMPLE_ADDONS = safeJSONObject(
"{ " +
"\"amznUWL2@amazon.com\": { " +
" \"userDisabled\": false, " +
" \"appDisabled\": false, " +
" \"version\": \"1.10\", " +
" \"type\": \"extension\", " +
" \"scope\": 1, " +
" \"foreignInstall\": false, " +
" \"hasBinaryComponents\": false, " +
" \"installDay\": 15269, " +
" \"updateDay\": 15602 " +
"}, " +
"\"jid0-qBnIpLfDFa4LpdrjhAC6vBqN20Q@jetpack\": { " +
" \"userDisabled\": false, " +
" \"appDisabled\": false, " +
" \"version\": \"1.12.1\", " +
" \"type\": \"extension\", " +
" \"scope\": 1, " +
" \"foreignInstall\": false, " +
" \"hasBinaryComponents\": false, " +
" \"installDay\": 15062, " +
" \"updateDay\": 15580 " +
"} " +
"} ");
private static JSONObject safeJSONObject(String s) {
try {
return new JSONObject(s);
} catch (JSONException e) {
return null;
}
}
public void testEnvironmentsAndFields() throws Exception {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
storage.beginInitialization();
storage.ensureMeasurementInitialized("testpA.testm", 1, new MockMeasurementFields());
storage.ensureMeasurementInitialized("testpB.testn", 1, new MockMeasurementFields());
storage.finishInitialization();
MockDatabaseEnvironment environmentA = storage.getEnvironment();
environmentA.mockInit("v123");
environmentA.setJSONForAddons(EXAMPLE_ADDONS);
final int envA = environmentA.register();
assertEquals(envA, environmentA.register());
// getField memoizes.
assertSame(storage.getField("foo", 2, "bar"),
storage.getField("foo", 2, "bar"));
// It throws if you refer to a non-existent field.
try {
storage.getField("foo", 2, "bar").getID();
fail("Should throw.");
} catch (IllegalStateException ex) {
// Expected.
}
// It returns the field ID for a valid field.
Field field = storage.getField("testpA.testm", 1, "testfield1");
assertTrue(field.getID() >= 0);
// These IDs are stable.
assertEquals(field.getID(), field.getID());
int fieldID = field.getID();
// Before inserting, no events.
assertFalse(storage.hasEventSince(0));
assertFalse(storage.hasEventSince(storage.now));
// Store some data for two environments across two days.
storage.incrementDailyCount(envA, storage.getYesterday(), fieldID, 4);
storage.incrementDailyCount(envA, storage.getYesterday(), fieldID, 1);
storage.incrementDailyCount(envA, storage.getToday(), fieldID, 2);
// After inserting, we have events.
assertTrue(storage.hasEventSince(storage.now - GlobalConstants.MILLISECONDS_PER_DAY));
assertTrue(storage.hasEventSince(storage.now));
// But not in the future.
assertFalse(storage.hasEventSince(storage.now + GlobalConstants.MILLISECONDS_PER_DAY));
MockDatabaseEnvironment environmentB = storage.getEnvironment();
environmentB.mockInit("v234");
environmentB.setJSONForAddons(EXAMPLE_ADDONS);
final int envB = environmentB.register();
assertFalse(envA == envB);
storage.incrementDailyCount(envB, storage.getToday(), fieldID, 6);
storage.incrementDailyCount(envB, storage.getToday(), fieldID, 2);
// Let's make sure everything's there.
Cursor c = storage.getRawEventsSince(storage.getOneDayAgo());
try {
assertTrue(c.moveToFirst());
assertTrue(assertRowEquals(c, storage.getYesterday(), envA, fieldID, 5));
assertTrue(assertRowEquals(c, storage.getToday(), envA, fieldID, 2));
assertFalse(assertRowEquals(c, storage.getToday(), envB, fieldID, 8));
} finally {
c.close();
}
// The stored environment has the provided JSON add-ons bundle.
Cursor e = storage.getEnvironmentRecordForID(envA);
e.moveToFirst();
assertEquals(EXAMPLE_ADDONS.toString(), e.getString(e.getColumnIndex("addonsBody")));
e.close();
e = storage.getEnvironmentRecordForID(envB);
e.moveToFirst();
assertEquals(EXAMPLE_ADDONS.toString(), e.getString(e.getColumnIndex("addonsBody")));
e.close();
// There's only one add-ons bundle in the DB, despite having two environments.
Cursor addons = storage.getDB().query("addons", null, null, null, null, null, null);
assertEquals(1, addons.getCount());
addons.close();
}
/**
* Asserts validity for a storage cursor. Returns whether there is another row to process.
*/
private static boolean assertRowEquals(Cursor c, int day, int env, int field, int value) {
assertEquals(day, c.getInt(0));
assertEquals(env, c.getInt(1));
assertEquals(field, c.getInt(2));
assertEquals(value, c.getLong(3));
return c.moveToNext();
}
/**
* Test robust insertions. This also acts as a test for the getPrepopulatedStorage method,
* allowing faster debugging if this fails and other tests relying on getPrepopulatedStorage
* also fail.
*/
public void testInsertions() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
assertNotNull(storage);
}
public void testForeignKeyConstraints() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
final int envID = storage.getEnvironment().register();
final int counterFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
storage.fieldSpecContainers[0].counter.name).getID();
final int discreteFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
storage.fieldSpecContainers[0].discrete.name).getID();
final int nonExistentEnvID = DBHelpers.getNonExistentID(db, "environments");
final int nonExistentFieldID = DBHelpers.getNonExistentID(db, "fields");
final int nonExistentAddonID = DBHelpers.getNonExistentID(db, "addons");
final int nonExistentMeasurementID = DBHelpers.getNonExistentID(db, "measurements");
ContentValues v = new ContentValues();
v.put("field", counterFieldID);
v.put("env", nonExistentEnvID);
try {
db.insertOrThrow("events_integer", null, v);
fail("Should throw - events_integer(env) is referencing non-existent environments(id)");
} catch (SQLiteConstraintException e) { }
v.put("field", discreteFieldID);
try {
db.insertOrThrow("events_textual", null, v);
fail("Should throw - events_textual(env) is referencing non-existent environments(id)");
} catch (SQLiteConstraintException e) { }
v.put("field", nonExistentFieldID);
v.put("env", envID);
try {
db.insertOrThrow("events_integer", null, v);
fail("Should throw - events_integer(field) is referencing non-existent fields(id)");
} catch (SQLiteConstraintException e) { }
try {
db.insertOrThrow("events_textual", null, v);
fail("Should throw - events_textual(field) is referencing non-existent fields(id)");
} catch (SQLiteConstraintException e) { }
v = new ContentValues();
v.put("addonsID", nonExistentAddonID);
try {
db.insertOrThrow("environments", null, v);
fail("Should throw - environments(addonsID) is referencing non-existent addons(id).");
} catch (SQLiteConstraintException e) { }
v = new ContentValues();
v.put("measurement", nonExistentMeasurementID);
try {
db.insertOrThrow("fields", null, v);
fail("Should throw - fields(measurement) is referencing non-existent measurements(id).");
} catch (SQLiteConstraintException e) { }
}
private int getTotalEventCount(HealthReportStorage storage) {
final Cursor c = storage.getEventsSince(0);
try {
return c.getCount();
} finally {
c.close();
}
}
public void testCascadingDeletions() throws Exception {
PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
SQLiteDatabase db = storage.getDB();
db.delete("environments", null, null);
assertEquals(0, DBHelpers.getRowCount(db, "events_integer"));
assertEquals(0, DBHelpers.getRowCount(db, "events_textual"));
storage = new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
db = storage.getDB();
db.delete("measurements", null, null);
assertEquals(0, DBHelpers.getRowCount(db, "fields"));
assertEquals(0, DBHelpers.getRowCount(db, "events_integer"));
assertEquals(0, DBHelpers.getRowCount(db, "events_textual"));
}
public void testRestrictedDeletions() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
SQLiteDatabase db = storage.getDB();
try {
db.delete("addons", null, null);
fail("Should throw - environment references addons and thus addons cannot be deleted.");
} catch (SQLiteConstraintException e) { }
}
public void testDeleteEverything() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
storage.deleteEverything();
final SQLiteDatabase db = storage.getDB();
for (String table : TABLE_NAMES) {
if (DBHelpers.getRowCount(db, table) != 0) {
fail("Not everything has been deleted for table " + table + ".");
}
}
}
public void testMeasurementRecordingConstraintViolation() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
final int envID = storage.getEnvironment().register();
final int counterFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
storage.fieldSpecContainers[0].counter.name).getID();
final int discreteFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
storage.fieldSpecContainers[0].discrete.name).getID();
final int nonExistentEnvID = DBHelpers.getNonExistentID(db, "environments");
final int nonExistentFieldID = DBHelpers.getNonExistentID(db, "fields");
try {
storage.incrementDailyCount(nonExistentEnvID, storage.getToday(), counterFieldID);
fail("Should throw - event_integer(env) references environments(id), which is given as a non-existent value.");
} catch (IllegalStateException e) { }
try {
storage.recordDailyLast(nonExistentEnvID, storage.getToday(), discreteFieldID, "iu");
fail("Should throw - event_textual(env) references environments(id), which is given as a non-existent value.");
} catch (IllegalStateException e) { }
try {
storage.incrementDailyCount(envID, storage.getToday(), nonExistentFieldID);
fail("Should throw - event_integer(field) references fields(id), which is given as a non-existent value.");
} catch (IllegalStateException e) { }
try {
storage.recordDailyLast(envID, storage.getToday(), nonExistentFieldID, "iu");
fail("Should throw - event_textual(field) references fields(id), which is given as a non-existent value.");
} catch (IllegalStateException e) { }
// Test dropped events due to constraint violations that do not throw (see bug 961526).
final String eventValue = "a value not in the database";
assertFalse(isEventInDB(db, eventValue)); // Better safe than sorry.
storage.recordDailyDiscrete(nonExistentEnvID, storage.getToday(), discreteFieldID, eventValue);
assertFalse(isEventInDB(db, eventValue));
storage.recordDailyDiscrete(envID, storage.getToday(), nonExistentFieldID, "iu");
assertFalse(isEventInDB(db, eventValue));
}
private static boolean isEventInDB(final SQLiteDatabase db, final String value) {
final Cursor c = db.query("events_textual", new String[] {"value"}, "value = ?",
new String[] {value}, null, null, null);
try {
return c.getCount() > 0;
} finally {
c.close();
}
}
// Largely taken from testDeleteEnvAndEventsBefore and testDeleteOrphanedAddons.
public void testDeleteDataBefore() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
// Insert (and delete) an environment not referenced by any events.
ContentValues v = new ContentValues();
v.put("hash", "I really hope this is a unique hash! ^_^");
v.put("addonsID", DBHelpers.getExistentID(db, "addons"));
db.insertOrThrow("environments", null, v);
v.put("hash", "Another unique hash!");
final int curEnv = (int) db.insertOrThrow("environments", null, v);
final ContentValues addonV = new ContentValues();
addonV.put("body", "addon1");
db.insertOrThrow("addons", null, addonV);
// 2 = 1 addon + 1 env.
assertEquals(2, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(8), curEnv));
assertEquals(1, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(8),
DBHelpers.getNonExistentID(db, "environments")));
assertEquals(1, DBHelpers.getRowCount(db, "addons"));
// Insert (and delete) new environment and referencing events.
final long envID = db.insertOrThrow("environments", null, v);
v = new ContentValues();
v.put("date", storage.getGivenDaysAgo(9));
v.put("env", envID);
v.put("field", DBHelpers.getExistentID(db, "fields"));
db.insertOrThrow("events_integer", null, v);
db.insertOrThrow("events_integer", null, v);
assertEquals(16, getTotalEventCount(storage));
final int nonExistentEnvID = DBHelpers.getNonExistentID(db, "environments");
assertEquals(1, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(8), nonExistentEnvID));
assertEquals(14, getTotalEventCount(storage));
// Assert only pre-populated storage is stored.
assertEquals(1, DBHelpers.getRowCount(db, "environments"));
assertEquals(0, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(5), nonExistentEnvID));
assertEquals(12, getTotalEventCount(storage));
assertEquals(0, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(4), nonExistentEnvID));
assertEquals(10, getTotalEventCount(storage));
assertEquals(0, storage.deleteDataBefore(storage.now, nonExistentEnvID));
assertEquals(5, getTotalEventCount(storage));
assertEquals(1, DBHelpers.getRowCount(db, "addons"));
// 2 = 1 addon + 1 env.
assertEquals(2, storage.deleteDataBefore(storage.now + GlobalConstants.MILLISECONDS_PER_DAY,
nonExistentEnvID));
assertEquals(0, getTotalEventCount(storage));
assertEquals(0, DBHelpers.getRowCount(db, "addons"));
}
// Largely taken from testDeleteOrphanedEnv and testDeleteEventsBefore.
public void testDeleteEnvAndEventsBefore() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
// Insert (and delete) an environment not referenced by any events.
ContentValues v = new ContentValues();
v.put("hash", "I really hope this is a unique hash! ^_^");
v.put("addonsID", DBHelpers.getExistentID(db, "addons"));
db.insertOrThrow("environments", null, v);
v.put("hash", "Another unique hash!");
final int curEnv = (int) db.insertOrThrow("environments", null, v);
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(8), curEnv));
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(8),
DBHelpers.getNonExistentID(db, "environments")));
// Insert (and delete) new environment and referencing events.
final long envID = db.insertOrThrow("environments", null, v);
v = new ContentValues();
v.put("date", storage.getGivenDaysAgo(9));
v.put("env", envID);
v.put("field", DBHelpers.getExistentID(db, "fields"));
db.insertOrThrow("events_integer", null, v);
db.insertOrThrow("events_integer", null, v);
assertEquals(16, getTotalEventCount(storage));
final int nonExistentEnvID = DBHelpers.getNonExistentID(db, "environments");
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(8), nonExistentEnvID));
assertEquals(14, getTotalEventCount(storage));
// Assert only pre-populated storage is stored.
assertEquals(1, DBHelpers.getRowCount(db, "environments"));
assertEquals(0, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(5), nonExistentEnvID));
assertEquals(12, getTotalEventCount(storage));
assertEquals(0, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(4), nonExistentEnvID));
assertEquals(10, getTotalEventCount(storage));
assertEquals(0, storage.deleteEnvAndEventsBefore(storage.now, nonExistentEnvID));
assertEquals(5, getTotalEventCount(storage));
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.now + GlobalConstants.MILLISECONDS_PER_DAY,
nonExistentEnvID));
assertEquals(0, getTotalEventCount(storage));
}
public void testDeleteOrphanedEnv() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
final ContentValues v = new ContentValues();
v.put("addonsID", DBHelpers.getExistentID(db, "addons"));
v.put("hash", "unique");
final int envID = (int) db.insert("environments", null, v);
assertEquals(0, storage.deleteOrphanedEnv(envID));
assertEquals(1, storage.deleteOrphanedEnv(storage.env));
this.deleteEvents(db);
assertEquals(1, storage.deleteOrphanedEnv(envID));
}
private void deleteEvents(final SQLiteDatabase db) throws Exception {
db.beginTransaction();
try {
db.delete("events_integer", null, null);
db.delete("events_textual", null, null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void testDeleteEventsBefore() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
assertEquals(2, storage.deleteEventsBefore(Integer.toString(storage.getGivenDaysAgo(5))));
assertEquals(12, getTotalEventCount(storage));
assertEquals(2, storage.deleteEventsBefore(Integer.toString(storage.getGivenDaysAgo(4))));
assertEquals(10, getTotalEventCount(storage));
assertEquals(5, storage.deleteEventsBefore(Integer.toString(storage.getToday())));
assertEquals(5, getTotalEventCount(storage));
assertEquals(5, storage.deleteEventsBefore(Integer.toString(storage.getTomorrow())));
assertEquals(0, getTotalEventCount(storage));
}
public void testDeleteOrphanedAddons() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
final ArrayList<Integer> nonOrphanIDs = new ArrayList<Integer>();
final Cursor c = db.query("addons", new String[] {"id"}, null, null, null, null, null);
try {
assertTrue(c.moveToFirst());
do {
nonOrphanIDs.add(c.getInt(0));
} while (c.moveToNext());
} finally {
c.close();
}
// Ensure we don't delete non-orphans.
assertEquals(0, storage.deleteOrphanedAddons());
// Insert orphans.
final long[] orphanIDs = new long[2];
final ContentValues v = new ContentValues();
v.put("body", "addon1");
orphanIDs[0] = db.insertOrThrow("addons", null, v);
v.put("body", "addon2");
orphanIDs[1] = db.insertOrThrow("addons", null, v);
assertEquals(2, storage.deleteOrphanedAddons());
assertEquals(0, DBHelpers.getRowCount(db, "addons", "ID = ? OR ID = ?",
new String[] {Long.toString(orphanIDs[0]), Long.toString(orphanIDs[1])}));
// Orphan all addons.
db.delete("environments", null, null);
assertEquals(nonOrphanIDs.size(), storage.deleteOrphanedAddons());
assertEquals(0, DBHelpers.getRowCount(db, "addons"));
}
public void testGetEventCount() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
assertEquals(14, storage.getEventCount());
final SQLiteDatabase db = storage.getDB();
this.deleteEvents(db);
assertEquals(0, storage.getEventCount());
}
public void testGetEnvironmentCount() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
assertEquals(1, storage.getEnvironmentCount());
final SQLiteDatabase db = storage.getDB();
db.delete("environments", null, null);
assertEquals(0, storage.getEnvironmentCount());
}
public void testPruneEnvironments() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory, 2);
final SQLiteDatabase db = storage.getDB();
assertEquals(5, DBHelpers.getRowCount(db, "environments"));
assertEquals(5, storage.getEnvironmentCache().size());
storage.pruneEnvironments(1);
assertEquals(0, storage.getEnvironmentCache().size());
assertTrue(!getEnvAppVersions(db).contains("v3"));
storage.pruneEnvironments(2);
assertTrue(!getEnvAppVersions(db).contains("v2"));
assertTrue(!getEnvAppVersions(db).contains("v1"));
storage.pruneEnvironments(1);
assertTrue(!getEnvAppVersions(db).contains("v123"));
storage.pruneEnvironments(1);
assertTrue(!getEnvAppVersions(db).contains("v4"));
}
private ArrayList<String> getEnvAppVersions(final SQLiteDatabase db) {
ArrayList<String> out = new ArrayList<String>();
Cursor c = null;
try {
c = db.query(true, "environments", new String[] {"appVersion"}, null, null, null, null, null, null);
while (c.moveToNext()) {
out.add(c.getString(0));
}
} finally {
if (c != null) {
c.close();
}
}
return out;
}
public void testPruneEvents() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
SQLiteDatabase db = storage.getDB();
assertEquals(14, DBHelpers.getRowCount(db, "events"));
storage.pruneEvents(1); // Delete < 7 days ago.
assertEquals(14, DBHelpers.getRowCount(db, "events"));
storage.pruneEvents(2); // Delete < 5 days ago.
assertEquals(13, DBHelpers.getRowCount(db, "events"));
storage.pruneEvents(5); // Delete < 2 days ago.
assertEquals(9, DBHelpers.getRowCount(db, "events"));
storage.pruneEvents(14); // Delete < today.
assertEquals(5, DBHelpers.getRowCount(db, "events"));
}
public void testVacuum() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
// Need to disable auto_vacuum to allow free page fragmentation. Note that the pragma changes
// only after a vacuum command.
db.execSQL("PRAGMA auto_vacuum=0");
db.execSQL("vacuum");
assertTrue(isAutoVacuumingDisabled(storage));
createFreePages(storage);
storage.vacuum();
assertEquals(0, getFreelistCount(storage));
}
public long getFreelistCount(final MockHealthReportDatabaseStorage storage) {
return storage.getIntFromQuery("PRAGMA freelist_count", null);
}
public boolean isAutoVacuumingDisabled(final MockHealthReportDatabaseStorage storage) {
return storage.getIntFromQuery("PRAGMA auto_vacuum", null) == 0;
}
private void createFreePages(final PrepopulatedMockHealthReportDatabaseStorage storage) throws Exception {
// Insert and delete until DB has free page fragmentation. The loop helps ensure that the
// fragmentation will occur with minimal disk usage. The upper loop limits are arbitrary.
final SQLiteDatabase db = storage.getDB();
for (int i = 10; i <= 1250; i *= 5) {
storage.insertTextualEvents(i);
db.delete("events_textual", "date < ?", new String[] {Integer.toString(i / 2)});
if (getFreelistCount(storage) > 0) {
return;
}
}
fail("Database free pages failed to fragment.");
}
public void testDisableAutoVacuuming() throws Exception {
final PrepopulatedMockHealthReportDatabaseStorage storage =
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
// The pragma changes only after a vacuum command.
db.execSQL("PRAGMA auto_vacuum=1");
db.execSQL("vacuum");
assertEquals(1, storage.getIntFromQuery("PRAGMA auto_vacuum", null));
storage.disableAutoVacuuming();
db.execSQL("vacuum");
assertTrue(isAutoVacuumingDisabled(storage));
}
}

View File

@ -1,523 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.common.DateUtils;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.SparseArray;
public class TestHealthReportGenerator extends FakeProfileTestCase {
@SuppressWarnings("static-method")
public void testOptObject() throws JSONException {
JSONObject o = new JSONObject();
o.put("foo", JSONObject.NULL);
assertEquals(null, o.optJSONObject("foo"));
}
@SuppressWarnings("static-method")
public void testAppend() throws JSONException {
JSONObject o = new JSONObject();
HealthReportUtils.append(o, "yyy", 5);
assertNotNull(o.getJSONArray("yyy"));
assertEquals(5, o.getJSONArray("yyy").getInt(0));
o.put("foo", "noo");
HealthReportUtils.append(o, "foo", "bar");
assertNotNull(o.getJSONArray("foo"));
assertEquals("noo", o.getJSONArray("foo").getString(0));
assertEquals("bar", o.getJSONArray("foo").getString(1));
}
@SuppressWarnings("static-method")
public void testCount() throws JSONException {
JSONObject o = new JSONObject();
HealthReportUtils.count(o, "foo", "a");
HealthReportUtils.count(o, "foo", "b");
HealthReportUtils.count(o, "foo", "a");
HealthReportUtils.count(o, "foo", "c");
HealthReportUtils.count(o, "bar", "a");
HealthReportUtils.count(o, "bar", "d");
JSONObject foo = o.getJSONObject("foo");
JSONObject bar = o.getJSONObject("bar");
assertEquals(2, foo.getInt("a"));
assertEquals(1, foo.getInt("b"));
assertEquals(1, foo.getInt("c"));
assertFalse(foo.has("d"));
assertEquals(1, bar.getInt("a"));
assertEquals(1, bar.getInt("d"));
assertFalse(bar.has("b"));
}
// We don't initialize the env in testHashing, so these are just the default
// values for the Java types, in order.
private static final String EXPECTED_MOCK_BASE_HASH = "000nullnullnullnullnullnullnull"
+ "nullnullnullnullnullnull00000";
// v2 fields.
private static final String EXPECTED_MOCK_BASE_HASH_SUFFIX_V2 = "null" + "null" + 0 + "null";
// v3 fields.
private static final String EXPECTED_MOCK_BASE_HASH_SUFFIX_V3 = "" + 0 + "default" + 0 + 0 + 0 + 0;
public void testHashing() throws JSONException {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
MockDatabaseEnvironment env = new MockDatabaseEnvironment(storage, MockDatabaseEnvironment.MockEnvironmentAppender.class);
env.addons = new JSONObject();
String addonAHash = "{addonA}={appDisabled==falseforeignInstall==false"
+ "hasBinaryComponents==falseinstallDay==15269scope==1"
+ "type==extensionupdateDay==15602userDisabled==false"
+ "version==1.10}";
JSONObject addonA1 = new JSONObject("{" +
"\"userDisabled\": false, " +
"\"appDisabled\": false, " +
"\"version\": \"1.10\", " +
"\"type\": \"extension\", " +
"\"scope\": 1, " +
"\"foreignInstall\": false, " +
"\"hasBinaryComponents\": false, " +
"\"installDay\": 15269, " +
"\"updateDay\": 15602 " +
"}");
// A reordered but otherwise equivalent object.
JSONObject addonA1rev = new JSONObject("{" +
"\"userDisabled\": false, " +
"\"foreignInstall\": false, " +
"\"hasBinaryComponents\": false, " +
"\"installDay\": 15269, " +
"\"type\": \"extension\", " +
"\"scope\": 1, " +
"\"appDisabled\": false, " +
"\"version\": \"1.10\", " +
"\"updateDay\": 15602 " +
"}");
env.addons.put("{addonA}", addonA1);
assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash +
EXPECTED_MOCK_BASE_HASH_SUFFIX_V2 +
EXPECTED_MOCK_BASE_HASH_SUFFIX_V3, env.getHash());
env.addons.put("{addonA}", addonA1rev);
assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash +
EXPECTED_MOCK_BASE_HASH_SUFFIX_V2 +
EXPECTED_MOCK_BASE_HASH_SUFFIX_V3, env.getHash());
}
private void assertJSONDiff(JSONObject source, JSONObject diff) throws JSONException {
assertEquals(source.get("a"), diff.get("a"));
assertFalse(diff.has("b"));
assertEquals(source.get("c"), diff.get("c"));
JSONObject diffD = diff.getJSONObject("d");
assertFalse(diffD.has("aa"));
assertEquals(1, diffD.getJSONArray("bb").getInt(0));
JSONObject diffCC = diffD.getJSONObject("cc");
assertEquals(1, diffCC.length());
assertEquals(1, diffCC.getInt("---"));
}
private static void assertJSONEquals(JSONObject one, JSONObject two) throws JSONException {
if (one == null || two == null) {
assertEquals(two, one);
}
assertEquals(one.length(), two.length());
@SuppressWarnings("unchecked")
Iterator<String> it = one.keys();
while (it.hasNext()) {
String key = it.next();
Object v1 = one.get(key);
Object v2 = two.get(key);
if (v1 instanceof JSONObject) {
assertTrue(v2 instanceof JSONObject);
assertJSONEquals((JSONObject) v1, (JSONObject) v2);
} else {
assertEquals(v1, v2);
}
}
}
@SuppressWarnings("static-method")
public void testNulls() {
assertTrue(JSONObject.NULL.equals(null));
assertTrue(JSONObject.NULL.equals(JSONObject.NULL));
assertFalse(JSONObject.NULL.equals(new JSONObject()));
assertFalse(null == JSONObject.NULL);
}
public void testJSONDiffing() throws JSONException {
String one = "{\"a\": 1, \"b\": 2, \"c\": [1, 2, 3], \"d\": {\"aa\": 5, \"bb\": [], \"cc\": {\"aaa\": null}}, \"e\": {}}";
String two = "{\"a\": 2, \"b\": 2, \"c\": [1, null, 3], \"d\": {\"aa\": 5, \"bb\": [1], \"cc\": {\"---\": 1, \"aaa\": null}}}";
JSONObject jOne = new JSONObject(one);
JSONObject jTwo = new JSONObject(two);
JSONObject diffNull = HealthReportGenerator.diff(jOne, jTwo, true);
JSONObject diffNoNull = HealthReportGenerator.diff(jOne, jTwo, false);
assertJSONDiff(jTwo, diffNull);
assertJSONDiff(jTwo, diffNoNull);
assertTrue(diffNull.isNull("e"));
assertFalse(diffNoNull.has("e"));
// Diffing to null returns the negation object: all the same keys but all null values.
JSONObject negated = new JSONObject("{\"a\": null, \"b\": null, \"c\": null, \"d\": null, \"e\": null}");
JSONObject toNull = HealthReportGenerator.diff(jOne, null, true);
assertJSONEquals(toNull, negated);
// Diffing from null returns the destination object.
JSONObject fromNull = HealthReportGenerator.diff(null, jOne, true);
assertJSONEquals(fromNull, jOne);
}
public void testAddonDiffing() throws JSONException {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(
context,
fakeProfileDirectory);
final MockDatabaseEnvironment env1 = storage.getEnvironment();
env1.mockInit("23");
final MockDatabaseEnvironment env2 = storage.getEnvironment();
env2.mockInit("23");
env1.addons = new JSONObject();
env2.addons = new JSONObject();
JSONObject addonA1 = new JSONObject("{" + "\"userDisabled\": false, "
+ "\"appDisabled\": false, "
+ "\"version\": \"1.10\", "
+ "\"type\": \"extension\", "
+ "\"scope\": 1, "
+ "\"foreignInstall\": false, "
+ "\"hasBinaryComponents\": false, "
+ "\"installDay\": 15269, "
+ "\"updateDay\": 15602 " + "}");
JSONObject addonA2 = new JSONObject("{" + "\"userDisabled\": false, "
+ "\"appDisabled\": false, "
+ "\"version\": \"1.20\", "
+ "\"type\": \"extension\", "
+ "\"scope\": 1, "
+ "\"foreignInstall\": false, "
+ "\"hasBinaryComponents\": false, "
+ "\"installDay\": 15269, "
+ "\"updateDay\": 17602 " + "}");
JSONObject addonB1 = new JSONObject("{" + "\"userDisabled\": false, "
+ "\"appDisabled\": false, "
+ "\"version\": \"1.0\", "
+ "\"type\": \"theme\", "
+ "\"scope\": 1, "
+ "\"foreignInstall\": false, "
+ "\"hasBinaryComponents\": false, "
+ "\"installDay\": 10269, "
+ "\"updateDay\": 10002 " + "}");
JSONObject addonC1 = new JSONObject("{" + "\"userDisabled\": true, "
+ "\"appDisabled\": false, "
+ "\"version\": \"1.50\", "
+ "\"type\": \"plugin\", "
+ "\"scope\": 1, "
+ "\"foreignInstall\": false, "
+ "\"hasBinaryComponents\": true, "
+ "\"installDay\": 12269, "
+ "\"updateDay\": 12602 " + "}");
env1.addons.put("{addonA}", addonA1);
env1.addons.put("{addonB}", addonB1);
env2.addons.put("{addonA}", addonA2);
env2.addons.put("{addonB}", addonB1);
env2.addons.put("{addonC}", addonC1);
JSONObject env2JSON = HealthReportGenerator.jsonify(env2, env1);
JSONObject addons = env2JSON.getJSONObject("org.mozilla.addons.active");
assertTrue(addons.has("{addonA}"));
assertFalse(addons.has("{addonB}")); // Because it's unchanged.
assertTrue(addons.has("{addonC}"));
JSONObject aJSON = addons.getJSONObject("{addonA}");
assertEquals(2, aJSON.length());
assertEquals("1.20", aJSON.getString("version"));
assertEquals(17602, aJSON.getInt("updateDay"));
JSONObject cJSON = addons.getJSONObject("{addonC}");
assertEquals(9, cJSON.length());
}
public void testEnvironments() throws JSONException {
// Hard-coded so you need to update tests!
// If this is the only thing you need to change when revving a version, you
// need more test coverage.
final int expectedVersion = 3;
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
HealthReportGenerator gen = new HealthReportGenerator(storage);
final MockDatabaseEnvironment env1 = storage.getEnvironment();
env1.mockInit("23");
final String env1Hash = env1.getHash();
long now = System.currentTimeMillis();
JSONObject document = gen.generateDocument(0, 0, env1);
String today = new DateUtils.DateFormatter().getDateString(now);
assertFalse(document.has("lastPingDate"));
document = gen.generateDocument(0, HealthReportConstants.EARLIEST_LAST_PING, env1);
assertEquals("2013-05-02", document.get("lastPingDate"));
// True unless test spans midnight...
assertEquals(today, document.get("thisPingDate"));
assertEquals(expectedVersion, document.get("version"));
JSONObject environments = document.getJSONObject("environments");
JSONObject current = environments.getJSONObject("current");
assertTrue(current.has("org.mozilla.profile.age"));
assertTrue(current.has("org.mozilla.sysinfo.sysinfo"));
assertTrue(current.has("org.mozilla.appInfo.appinfo"));
assertTrue(current.has("geckoAppInfo"));
assertTrue(current.has("org.mozilla.addons.active"));
assertTrue(current.has("org.mozilla.addons.counts"));
// Make sure we don't get duplicate environments when an environment has
// been used, and that we get deltas between them.
env1.register();
final MockDatabaseEnvironment env2 = storage.getEnvironment();
env2.mockInit("24");
final String env2Hash = env2.getHash();
assertFalse(env2Hash.equals(env1Hash));
env2.register();
assertEquals(env2Hash, env2.getHash());
assertEquals("2013-05-02", document.get("lastPingDate"));
// True unless test spans midnight...
assertEquals(today, document.get("thisPingDate"));
assertEquals(expectedVersion, document.get("version"));
document = gen.generateDocument(0, HealthReportConstants.EARLIEST_LAST_PING, env2);
environments = document.getJSONObject("environments");
// Now we have two: env1, and env2 (as 'current').
assertTrue(environments.has(env1.getHash()));
assertTrue(environments.has("current"));
assertEquals(2, environments.length());
current = environments.getJSONObject("current");
assertTrue(current.has("org.mozilla.profile.age"));
assertTrue(current.has("org.mozilla.sysinfo.sysinfo"));
assertTrue(current.has("org.mozilla.appInfo.appinfo"));
assertTrue(current.has("geckoAppInfo"));
assertTrue(current.has("org.mozilla.addons.active"));
assertTrue(current.has("org.mozilla.addons.counts"));
// The diff only contains the changed measurement and fields.
JSONObject previous = environments.getJSONObject(env1.getHash());
assertTrue(previous.has("geckoAppInfo"));
final JSONObject previousAppInfo = previous.getJSONObject("geckoAppInfo");
assertEquals(2, previousAppInfo.length());
assertEquals("23", previousAppInfo.getString("version"));
assertEquals(Integer.valueOf(1), (Integer) previousAppInfo.get("_v"));
assertFalse(previous.has("org.mozilla.profile.age"));
assertFalse(previous.has("org.mozilla.sysinfo.sysinfo"));
assertFalse(previous.has("org.mozilla.appInfo.appinfo"));
assertFalse(previous.has("org.mozilla.addons.active"));
assertFalse(previous.has("org.mozilla.addons.counts"));
}
public void testInsertedData() throws JSONException {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
HealthReportGenerator gen = new HealthReportGenerator(storage);
storage.beginInitialization();
final MockDatabaseEnvironment environment = storage.getEnvironment();
String envHash = environment.getHash();
int env = environment.mockInit("23").register();
storage.ensureMeasurementInitialized("org.mozilla.testm5", 1, new MeasurementFields() {
@Override
public Iterable<FieldSpec> getFields() {
ArrayList<FieldSpec> out = new ArrayList<FieldSpec>();
out.add(new FieldSpec("counter", Field.TYPE_INTEGER_COUNTER));
out.add(new FieldSpec("discrete_int", Field.TYPE_INTEGER_DISCRETE));
out.add(new FieldSpec("discrete_str", Field.TYPE_STRING_DISCRETE));
out.add(new FieldSpec("last_int", Field.TYPE_INTEGER_LAST));
out.add(new FieldSpec("last_str", Field.TYPE_STRING_LAST));
out.add(new FieldSpec("counted_str", Field.TYPE_COUNTED_STRING_DISCRETE));
out.add(new FieldSpec("discrete_json", Field.TYPE_JSON_DISCRETE));
return out;
}
});
storage.finishInitialization();
long now = System.currentTimeMillis();
int day = storage.getDay(now);
final String todayString = new DateUtils.DateFormatter().getDateString(now);
int counter = storage.getField("org.mozilla.testm5", 1, "counter").getID();
int discrete_int = storage.getField("org.mozilla.testm5", 1, "discrete_int").getID();
int discrete_str = storage.getField("org.mozilla.testm5", 1, "discrete_str").getID();
int last_int = storage.getField("org.mozilla.testm5", 1, "last_int").getID();
int last_str = storage.getField("org.mozilla.testm5", 1, "last_str").getID();
int counted_str = storage.getField("org.mozilla.testm5", 1, "counted_str").getID();
int discrete_json = storage.getField("org.mozilla.testm5", 1, "discrete_json").getID();
storage.incrementDailyCount(env, day, counter, 2);
storage.incrementDailyCount(env, day, counter, 3);
storage.recordDailyLast(env, day, last_int, 2);
storage.recordDailyLast(env, day, last_str, "a");
storage.recordDailyLast(env, day, last_int, 3);
storage.recordDailyLast(env, day, last_str, "b");
storage.recordDailyDiscrete(env, day, discrete_str, "a");
storage.recordDailyDiscrete(env, day, discrete_str, "b");
storage.recordDailyDiscrete(env, day, discrete_int, 2);
storage.recordDailyDiscrete(env, day, discrete_int, 1);
storage.recordDailyDiscrete(env, day, discrete_int, 3);
storage.recordDailyDiscrete(env, day, counted_str, "aaa");
storage.recordDailyDiscrete(env, day, counted_str, "ccc");
storage.recordDailyDiscrete(env, day, counted_str, "bbb");
storage.recordDailyDiscrete(env, day, counted_str, "aaa");
JSONObject objA = new JSONObject();
objA.put("foo", "bar");
storage.recordDailyDiscrete(env, day, discrete_json, (JSONObject) null);
storage.recordDailyDiscrete(env, day, discrete_json, "null"); // Still works because JSON is a string internally.
storage.recordDailyDiscrete(env, day, discrete_json, objA);
JSONObject document = gen.generateDocument(0, HealthReportConstants.EARLIEST_LAST_PING, environment);
JSONObject today = document.getJSONObject("data").getJSONObject("days").getJSONObject(todayString);
assertEquals(1, today.length());
JSONObject measurement = today.getJSONObject(envHash).getJSONObject("org.mozilla.testm5");
assertEquals(1, measurement.getInt("_v"));
assertEquals(5, measurement.getInt("counter"));
assertEquals(3, measurement.getInt("last_int"));
assertEquals("b", measurement.getString("last_str"));
JSONArray discreteInts = measurement.getJSONArray("discrete_int");
JSONArray discreteStrs = measurement.getJSONArray("discrete_str");
assertEquals(3, discreteInts.length());
assertEquals(2, discreteStrs.length());
assertEquals("a", discreteStrs.get(0));
assertEquals("b", discreteStrs.get(1));
assertEquals(Long.valueOf(2), discreteInts.get(0));
assertEquals(Long.valueOf(1), discreteInts.get(1));
assertEquals(Long.valueOf(3), discreteInts.get(2));
JSONObject counted = measurement.getJSONObject("counted_str");
assertEquals(2, counted.getInt("aaa"));
assertEquals(1, counted.getInt("bbb"));
assertEquals(1, counted.getInt("ccc"));
assertFalse(counted.has("ddd"));
JSONArray discreteJSON = measurement.getJSONArray("discrete_json");
assertEquals(3, discreteJSON.length());
assertEquals(JSONObject.NULL, discreteJSON.get(0));
assertEquals(JSONObject.NULL, discreteJSON.get(1));
assertEquals("bar", discreteJSON.getJSONObject(2).getString("foo"));
}
@Override
protected String getCacheSuffix() {
return File.separator + "health-" + System.currentTimeMillis() + ".profile";
}
public void testEnvironmentDiffing() throws JSONException {
// Manually insert a v1 environment.
final MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
storage.deleteEverything();
final MockDatabaseEnvironment v1env = storage.getEnvironment();
v1env.mockInit("27.0a1");
v1env.version = 1;
v1env.appLocale = "";
v1env.osLocale = "";
v1env.distribution = "";
v1env.acceptLangSet = 0;
final int v1ID = v1env.register();
// Verify.
final String[] cols = new String[] {
"id", "version", "hash",
"osLocale", "acceptLangSet", "appLocale", "distribution"
};
final Cursor c1 = db.query("environments", cols, "id = " + v1ID, null, null, null, null);
String v1envHash;
try {
assertTrue(c1.moveToFirst());
assertEquals(1, c1.getCount());
assertEquals(v1ID, c1.getInt(0));
assertEquals(1, c1.getInt(1));
v1envHash = c1.getString(2);
assertNotNull(v1envHash);
assertEquals("", c1.getString(3));
assertEquals(0, c1.getInt(4));
assertEquals("", c1.getString(5));
assertEquals("", c1.getString(6));
} finally {
c1.close();
}
// Insert a v3 environment.
final MockDatabaseEnvironment v3env = storage.getEnvironment();
v3env.mockInit("31.0a1");
v3env.appLocale = v3env.osLocale = "en_us";
v3env.acceptLangSet = 1;
final int v3ID = v3env.register();
assertFalse(v1ID == v3ID);
final Cursor c2 = db.query("environments", cols, "id = " + v3ID, null, null, null, null);
String v2envHash;
try {
assertTrue(c2.moveToFirst());
assertEquals(1, c2.getCount());
assertEquals(v3ID, c2.getInt(0));
assertEquals(3, c2.getInt(1));
v2envHash = c2.getString(2);
assertNotNull(v2envHash);
assertEquals("en_us", c2.getString(3));
assertEquals(1, c2.getInt(4));
assertEquals("en_us", c2.getString(5));
assertEquals("", c2.getString(6));
} finally {
c2.close();
}
assertFalse(v1envHash.equals(v2envHash));
// Now let's diff based on DB contents.
SparseArray<Environment> envs = storage.getEnvironmentRecordsByID();
JSONObject oldEnv = HealthReportGenerator.jsonify(envs.get(v1ID), null).getJSONObject("org.mozilla.appInfo.appinfo");
JSONObject newEnv = HealthReportGenerator.jsonify(envs.get(v3ID), null).getJSONObject("org.mozilla.appInfo.appinfo");
// Generate the new env as if the old were the current. This should rarely happen in practice.
// Fields supported by the new env but not the old will appear, even if the 'default' for the
// old implementation is equal to the new env's value.
JSONObject newVsOld = HealthReportGenerator.jsonify(envs.get(v3ID), envs.get(v1ID)).getJSONObject("org.mozilla.appInfo.appinfo");
// Generate the old env as if the new were the current. This is normal. Fields not supported by the old
// environment version should not appear in the output.
JSONObject oldVsNew = HealthReportGenerator.jsonify(envs.get(v1ID), envs.get(v3ID)).getJSONObject("org.mozilla.appInfo.appinfo");
assertEquals(2, oldEnv.getInt("_v"));
assertEquals(3, newEnv.getInt("_v"));
assertEquals(2, oldVsNew.getInt("_v"));
assertEquals(3, newVsOld.getInt("_v"));
assertFalse(oldVsNew.has("osLocale"));
assertFalse(oldVsNew.has("appLocale"));
assertFalse(oldVsNew.has("distribution"));
assertFalse(oldVsNew.has("acceptLangIsUserSet"));
assertTrue(newVsOld.has("osLocale"));
assertTrue(newVsOld.has("appLocale"));
assertTrue(newVsOld.has("distribution"));
assertTrue(newVsOld.has("acceptLangIsUserSet"));
}
}

View File

@ -1,261 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import org.mozilla.gecko.background.helpers.DBHelpers;
import org.mozilla.gecko.background.helpers.DBProviderTestCase;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.test.mock.MockContentResolver;
public class TestHealthReportProvider extends DBProviderTestCase<HealthReportProvider> {
protected static final int MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
public TestHealthReportProvider() {
super(HealthReportProvider.class, HealthReportProvider.HEALTH_AUTHORITY);
}
public TestHealthReportProvider(Class<HealthReportProvider> providerClass,
String providerAuthority) {
super(providerClass, providerAuthority);
}
private Uri getCompleteUri(String rest) {
return Uri.parse("content://" + HealthReportProvider.HEALTH_AUTHORITY + rest +
(rest.indexOf('?') == -1 ? "?" : "&") +
"profilePath=" + Uri.encode(fakeProfileDirectory.getAbsolutePath()));
}
private void ensureCount(int expected, Uri uri) {
final MockContentResolver resolver = getMockContentResolver();
Cursor cursor = resolver.query(uri, null, null, null, null);
assertNotNull(cursor);
assertEquals(expected, cursor.getCount());
cursor.close();
}
private void ensureMeasurementCount(int expected) {
final Uri measurements = getCompleteUri("/measurements/");
ensureCount(expected, measurements);
}
private void ensureFieldCount(int expected) {
final Uri fields = getCompleteUri("/fields/");
ensureCount(expected, fields);
}
public void testNonExistentMeasurement() {
assertNotNull(getContext());
Uri u = getCompleteUri("/events/" + 0 + "/" + "testm" + "/" + 3 + "/" + "testf");
ContentValues v = new ContentValues();
v.put("value", 5);
ContentResolver r = getMockContentResolver();
assertNotNull(r);
try {
r.insert(u, v);
fail("Should throw.");
} catch (IllegalStateException e) {
assertTrue(e.getMessage().contains("No field with name testf"));
}
}
public void testEnsureMeasurements() {
ensureMeasurementCount(0);
final MockContentResolver resolver = getMockContentResolver();
// Note that we insert no fields. These are empty measurements.
ContentValues values = new ContentValues();
resolver.insert(getCompleteUri("/fields/testm1/1"), values);
ensureMeasurementCount(1);
resolver.insert(getCompleteUri("/fields/testm1/1"), values);
ensureMeasurementCount(1);
resolver.insert(getCompleteUri("/fields/testm1/3"), values);
ensureMeasurementCount(2);
resolver.insert(getCompleteUri("/fields/testm2/1"), values);
ensureMeasurementCount(3);
Cursor cursor = resolver.query(getCompleteUri("/measurements/"), null, null, null, null);
assertTrue(cursor.moveToFirst());
assertEquals("testm1", cursor.getString(1)); // 'id' is column 0.
assertEquals(1, cursor.getInt(2));
assertTrue(cursor.moveToNext());
assertEquals("testm1", cursor.getString(1));
assertEquals(3, cursor.getInt(2));
assertTrue(cursor.moveToNext());
assertEquals("testm2", cursor.getString(1));
assertEquals(1, cursor.getInt(2));
assertFalse(cursor.moveToNext());
cursor.close();
resolver.delete(getCompleteUri("/measurements/"), null, null);
}
/**
* Return true if the two times occur on the same UTC day.
*/
private static boolean sameDay(long start, long end) {
return Math.floor(start / MILLISECONDS_PER_DAY) ==
Math.floor(end / MILLISECONDS_PER_DAY);
}
private static int getDay(long time) {
return (int) Math.floor(time / MILLISECONDS_PER_DAY);
}
public void testRealData() {
ensureMeasurementCount(0);
long start = System.currentTimeMillis();
int day = getDay(start);
final MockContentResolver resolver = getMockContentResolver();
// Register a provider with four fields.
ContentValues values = new ContentValues();
values.put("counter1", 1);
values.put("counter2", 4);
values.put("last1", 7);
values.put("discrete1", 11);
resolver.insert(getCompleteUri("/fields/testm1/1"), values);
ensureMeasurementCount(1);
ensureFieldCount(4);
final Uri envURI = resolver.insert(getCompleteUri("/environments/"), getTestEnvContentValues());
String envHash = null;
Cursor envCursor = resolver.query(envURI, null, null, null, null);
try {
assertTrue(envCursor.moveToFirst());
envHash = envCursor.getString(2); // id, version, hash, ...
} finally {
envCursor.close();
}
final Uri eventURI = HealthReportUtils.getEventURI(envURI);
Uri discrete1 = eventURI.buildUpon().appendEncodedPath("testm1/1/discrete1").build();
Uri counter1 = eventURI.buildUpon().appendEncodedPath("testm1/1/counter1/counter").build();
Uri counter2 = eventURI.buildUpon().appendEncodedPath("testm1/1/counter2/counter").build();
Uri last1 = eventURI.buildUpon().appendEncodedPath("testm1/1/last1/last").build();
ContentValues discreteS = new ContentValues();
ContentValues discreteI = new ContentValues();
discreteS.put("value", "Some string");
discreteI.put("value", 9);
resolver.insert(discrete1, discreteS);
resolver.insert(discrete1, discreteI);
ContentValues counter = new ContentValues();
resolver.update(counter1, counter, null, null); // Defaults to 1.
resolver.update(counter2, counter, null, null); // Defaults to 1.
counter.put("value", 3);
resolver.update(counter2, counter, null, null); // Increment by 3.
// Interleaving.
discreteS.put("value", "Some other string");
discreteI.put("value", 3);
resolver.insert(discrete1, discreteS);
resolver.insert(discrete1, discreteI);
// Note that we explicitly do not support last-values transitioning between types.
ContentValues last = new ContentValues();
last.put("value", 123);
resolver.update(last1, last, null, null);
last.put("value", 245);
resolver.update(last1, last, null, null);
int expectedRows = 2 + 1 + 4; // Two counters, one last, four entries for discrete.
// Now let's see what comes up in the query!
// We'll do "named" first -- the results include strings.
Cursor cursor = resolver.query(getCompleteUri("/events/?time=" + start), null, null, null, null);
assertEquals(expectedRows, cursor.getCount());
assertTrue(cursor.moveToFirst());
// Let's be safe in case someone runs this test at midnight.
long end = System.currentTimeMillis();
if (!sameDay(start, end)) {
System.out.println("Aborting testAddData: spans midnight.");
cursor.close();
return;
}
// "date", "env", m, mv, f, f_flags, "value"
Object[][] expected = {
{day, envHash, "testm1", 1, "counter1", null, 1},
{day, envHash, "testm1", 1, "counter2", null, 4},
// Discrete values don't preserve order of insertion across types, but
// this actually isn't really permitted -- fields have a single type.
{day, envHash, "testm1", 1, "discrete1", null, 9},
{day, envHash, "testm1", 1, "discrete1", null, 3},
{day, envHash, "testm1", 1, "discrete1", null, "Some string"},
{day, envHash, "testm1", 1, "discrete1", null, "Some other string"},
{day, envHash, "testm1", 1, "last1", null, 245},
};
DBHelpers.assertCursorContains(expected, cursor);
cursor.close();
resolver.delete(getCompleteUri("/measurements/"), null, null);
ensureMeasurementCount(0);
ensureFieldCount(0);
}
private ContentValues getTestEnvContentValues() {
ContentValues v = new ContentValues();
v.put("profileCreation", 0);
v.put("cpuCount", 0);
v.put("memoryMB", 0);
v.put("isBlocklistEnabled", 0);
v.put("isTelemetryEnabled", 0);
v.put("extensionCount", 0);
v.put("pluginCount", 0);
v.put("themeCount", 0);
v.put("architecture", "");
v.put("sysName", "");
v.put("sysVersion", "");
v.put("vendor", "");
v.put("appName", "");
v.put("appID", "");
v.put("appVersion", "");
v.put("appBuildID", "");
v.put("platformVersion", "");
v.put("platformBuildID", "");
v.put("os", "");
v.put("xpcomabi", "");
v.put("updateChannel", "");
// v2.
v.put("distribution", "");
v.put("osLocale", "en_us");
v.put("appLocale", "en_us");
v.put("acceptLangSet", 0);
// v3.
v.put("hasHardwareKeyboard", 0);
v.put("uiMode", 0);
v.put("uiType", "default");
v.put("screenLayout", 0);
v.put("screenXInMM", 100);
v.put("screenYInMM", 140);
return v;
}
}

View File

@ -1,165 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import org.mozilla.gecko.background.helpers.DBHelpers;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
public class TestHealthReportSQLiteOpenHelper extends FakeProfileTestCase {
private MockHealthReportSQLiteOpenHelper helper;
@Override
protected void setUp() throws Exception {
super.setUp();
helper = null;
}
@Override
protected void tearDown() throws Exception {
if (helper != null) {
helper.close();
helper = null;
}
super.tearDown();
}
private MockHealthReportSQLiteOpenHelper createHelper(String name) {
return new MockHealthReportSQLiteOpenHelper(context, fakeProfileDirectory, name);
}
private MockHealthReportSQLiteOpenHelper createHelper(String name, int version) {
return new MockHealthReportSQLiteOpenHelper(context, fakeProfileDirectory, name, version);
}
public void testOpening() {
helper = createHelper("health.db");
SQLiteDatabase db = helper.getWritableDatabase();
assertTrue(db.isOpen());
db.beginTransaction();
db.setTransactionSuccessful();
db.endTransaction();
helper.close();
assertFalse(db.isOpen());
}
private void assertEmptyTable(SQLiteDatabase db, String table, String column) {
Cursor c = db.query(table, new String[] { column },
null, null, null, null, null);
assertNotNull(c);
try {
assertFalse(c.moveToFirst());
} finally {
c.close();
}
}
public void testInit() {
helper = createHelper("health-" + System.currentTimeMillis() + ".db");
SQLiteDatabase db = helper.getWritableDatabase();
assertTrue(db.isOpen());
db.beginTransaction();
try {
// DB starts empty with correct tables.
assertEmptyTable(db, "fields", "name");
assertEmptyTable(db, "measurements", "name");
assertEmptyTable(db, "events_textual", "field");
assertEmptyTable(db, "events_integer", "field");
assertEmptyTable(db, "events", "field");
// Throws for tables that don't exist.
try {
assertEmptyTable(db, "foobarbaz", "name");
} catch (SQLiteException e) {
// Expected.
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void testUpgradeDatabaseFrom4To5() throws Exception {
final String dbName = "health-4To5.db";
helper = createHelper(dbName, 4);
SQLiteDatabase db = helper.getWritableDatabase();
db.beginTransaction();
try {
db.execSQL("PRAGMA foreign_keys=OFF;");
// Despite being referenced, this addon should be deleted because it is NULL.
ContentValues v = new ContentValues();
v.put("body", (String) null);
final long orphanedAddonID = db.insert("addons", null, v);
v.put("body", "addon");
final long addonID = db.insert("addons", null, v);
// environments -> addons
v = new ContentValues();
v.put("hash", "orphanedEnv");
v.put("addonsID", orphanedAddonID);
final long orphanedEnvID = db.insert("environments", null, v);
v.put("hash", "env");
v.put("addonsID", addonID);
final long envID = db.insert("environments", null, v);
v = new ContentValues();
v.put("name", "measurement");
v.put("version", 1);
final long measurementID = db.insert("measurements", null, v);
// fields -> measurements
v = new ContentValues();
v.put("name", "orphanedField");
v.put("measurement", DBHelpers.getNonExistentID(db, "measurements"));
final long orphanedFieldID = db.insert("fields", null, v);
v.put("name", "field");
v.put("measurement", measurementID);
final long fieldID = db.insert("fields", null, v);
// events -> environments, fields
final String[] eventTables = {"events_integer", "events_textual"};
for (String table : eventTables) {
v = new ContentValues();
v.put("env", envID);
v.put("field", fieldID);
db.insert(table, null, v);
v.put("env", orphanedEnvID);
v.put("field", fieldID);
db.insert(table, null, v);
v.put("env", envID);
v.put("field", orphanedFieldID);
db.insert(table, null, v);
v.put("env", orphanedEnvID);
v.put("field", orphanedFieldID);
db.insert(table, null, v);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
helper.close();
}
// Upgrade.
helper = createHelper(dbName, 5);
// Despite only reading from it, open a writable database so we can better replicate what
// might happen in production (most notably, this should enable foreign keys).
db = helper.getWritableDatabase();
assertEquals(1, DBHelpers.getRowCount(db, "addons"));
assertEquals(1, DBHelpers.getRowCount(db, "measurements"));
assertEquals(1, DBHelpers.getRowCount(db, "fields"));
assertEquals(1, DBHelpers.getRowCount(db, "events_integer"));
assertEquals(1, DBHelpers.getRowCount(db, "events_textual"));
}
}

View File

@ -1,189 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.io.IOException;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
public class TestProfileInformationCache extends FakeProfileTestCase {
public final void testEmptyFile() throws Exception {
// createTempFile creates an empty file on disk.
final File emptyFile = File.createTempFile("empty", "pic", this.fakeProfileDirectory);
final MockProfileInformationCache cache = new MockProfileInformationCache(emptyFile);
// Should not throw.
assertNull(cache.readJSON());
}
public final void testInitState() throws IOException {
MockProfileInformationCache cache = new MockProfileInformationCache(this.fakeProfileDirectory.getAbsolutePath());
assertFalse(cache.isInitialized());
assertFalse(cache.needsWrite());
try {
cache.isBlocklistEnabled();
fail("Should throw fetching isBlocklistEnabled.");
} catch (IllegalStateException e) {
// Great!
}
cache.beginInitialization();
assertFalse(cache.isInitialized());
assertTrue(cache.needsWrite());
try {
cache.isBlocklistEnabled();
fail("Should throw fetching isBlocklistEnabled.");
} catch (IllegalStateException e) {
// Great!
}
cache.completeInitialization();
assertTrue(cache.isInitialized());
assertFalse(cache.needsWrite());
}
public final MockProfileInformationCache makeCache(final String suffix) {
File subdir = new File(this.fakeProfileDirectory.getAbsolutePath() + File.separator + suffix);
subdir.mkdir();
return new MockProfileInformationCache(subdir.getAbsolutePath());
}
public final void testPersisting() throws IOException {
MockProfileInformationCache cache = makeCache("testPersisting");
// We start with no file.
assertFalse(cache.getFile().exists());
// Partially populate. Note that this doesn't happen in live code, but
// apparently we can end up with null add-ons JSON on disk, so this
// reproduces that scenario.
cache.beginInitialization();
cache.setBlocklistEnabled(true);
cache.setTelemetryEnabled(true);
cache.setProfileCreationTime(1234L);
cache.completeInitialization();
assertTrue(cache.getFile().exists());
// But reading this from disk won't work, because we were only partially
// initialized. We want to start over.
cache = makeCache("testPersisting");
assertFalse(cache.isInitialized());
assertFalse(cache.restoreUnlessInitialized());
assertFalse(cache.isInitialized());
// Now fully populate, and try again...
cache.beginInitialization();
cache.setBlocklistEnabled(true);
cache.setTelemetryEnabled(true);
cache.setProfileCreationTime(1234L);
cache.setJSONForAddons(new JSONObject());
cache.completeInitialization();
assertTrue(cache.getFile().exists());
// ... and this time we succeed.
cache = makeCache("testPersisting");
assertFalse(cache.isInitialized());
assertTrue(cache.restoreUnlessInitialized());
assertTrue(cache.isInitialized());
assertTrue(cache.isBlocklistEnabled());
assertTrue(cache.isTelemetryEnabled());
assertEquals(1234L, cache.getProfileCreationTime());
// Mutate.
cache.beginInitialization();
assertFalse(cache.isInitialized());
cache.setBlocklistEnabled(false);
cache.setProfileCreationTime(2345L);
cache.completeInitialization();
assertTrue(cache.isInitialized());
cache = makeCache("testPersisting");
assertFalse(cache.isInitialized());
assertTrue(cache.restoreUnlessInitialized());
assertTrue(cache.isInitialized());
assertFalse(cache.isBlocklistEnabled());
assertTrue(cache.isTelemetryEnabled());
assertEquals(2345L, cache.getProfileCreationTime());
}
public final void testVersioning() throws JSONException, IOException {
MockProfileInformationCache cache = makeCache("testVersioning");
final int currentVersion = ProfileInformationCache.FORMAT_VERSION;
final JSONObject json = cache.toJSON();
assertEquals(currentVersion, json.getInt("version"));
// Initialize enough that we can re-load it.
cache.beginInitialization();
cache.setJSONForAddons(new JSONObject());
cache.completeInitialization();
cache.writeJSON(json);
assertTrue(cache.restoreUnlessInitialized());
cache.beginInitialization(); // So that we'll need to read again.
json.put("version", currentVersion + 1);
cache.writeJSON(json);
// We can't restore a future version.
assertFalse(cache.restoreUnlessInitialized());
}
public void testRestoreInvalidJSON() throws Exception {
final MockProfileInformationCache cache = makeCache("invalid");
final JSONObject invalidJSON = new JSONObject();
invalidJSON.put("blocklist", true);
invalidJSON.put("telemetry", false);
invalidJSON.put("profileCreated", 1234567L);
cache.writeJSON(invalidJSON);
assertFalse(cache.restoreUnlessInitialized());
}
private JSONObject getValidCacheJSON() throws Exception {
final JSONObject json = new JSONObject();
json.put("blocklist", true);
json.put("telemetry", false);
json.put("profileCreated", 1234567L);
json.put("addons", new JSONObject());
json.put("version", ProfileInformationCache.FORMAT_VERSION);
return json;
}
public void testRestoreImplicitV1() throws Exception {
assertTrue(ProfileInformationCache.FORMAT_VERSION > 1);
final MockProfileInformationCache cache = makeCache("implicitV1");
final JSONObject json = getValidCacheJSON();
json.remove("version");
cache.writeJSON(json);
// Can't restore v1 (which is implicitly set) since it is not the current version.
assertFalse(cache.restoreUnlessInitialized());
}
public void testRestoreOldVersion() throws Exception {
final int oldVersion = 1;
assertTrue(ProfileInformationCache.FORMAT_VERSION > oldVersion);
final MockProfileInformationCache cache = makeCache("oldVersion");
final JSONObject json = getValidCacheJSON();
json.put("version", oldVersion);
cache.writeJSON(json);
assertFalse(cache.restoreUnlessInitialized());
}
public void testRestoreCurrentVersion() throws Exception {
final MockProfileInformationCache cache = makeCache("currentVersion");
final JSONObject json = getValidCacheJSON();
cache.writeJSON(json);
cache.beginInitialization();
cache.setTelemetryEnabled(true);
cache.completeInitialization();
assertEquals(ProfileInformationCache.FORMAT_VERSION, cache.readJSON().getInt("version"));
}
}

View File

@ -1,110 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.prune;
import java.util.concurrent.BrokenBarrierException;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.helpers.BackgroundServiceTestCase;
import android.content.Intent;
import android.content.SharedPreferences;
public class TestHealthReportPruneService
extends BackgroundServiceTestCase<TestHealthReportPruneService.MockHealthReportPruneService> {
public static class MockHealthReportPruneService extends HealthReportPruneService {
protected MockPrunePolicy prunePolicy;
@Override
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(sharedPrefsName,
GlobalConstants.SHARED_PREFERENCES_MODE);
}
@Override
public void onHandleIntent(Intent intent) {
super.onHandleIntent(intent);
try {
barrier.await();
} catch (InterruptedException e) {
fail("Awaiting thread should not be interrupted.");
} catch (BrokenBarrierException e) {
// This will happen on timeout - do nothing.
}
}
@Override
public PrunePolicy getPrunePolicy(final String profilePath) {
// We don't actually need any storage, so just make it null. Actually
// creating storage requires a valid context; here, we only have a
// MockContext.
final PrunePolicyStorage storage = null;
prunePolicy = new MockPrunePolicy(storage, getSharedPreferences());
return prunePolicy;
}
public boolean wasTickCalled() {
if (prunePolicy == null) {
return false;
}
return prunePolicy.wasTickCalled();
}
}
// TODO: This is a spy - perhaps we should be using a framework for this.
public static class MockPrunePolicy extends PrunePolicy {
private boolean wasTickCalled;
public MockPrunePolicy(final PrunePolicyStorage storage, final SharedPreferences sharedPreferences) {
super(storage, sharedPreferences);
wasTickCalled = false;
}
@Override
public void tick(final long time) {
wasTickCalled = true;
}
public boolean wasTickCalled() {
return wasTickCalled;
}
}
public TestHealthReportPruneService() {
super(MockHealthReportPruneService.class);
}
@Override
public void setUp() throws Exception {
super.setUp();
}
public void testIsIntentValid() throws Exception {
// No profilePath or profileName.
startService(intent);
await();
assertFalse(getService().wasTickCalled());
barrier.reset();
// No profilePath.
intent.putExtra("profileName", "profileName");
startService(intent);
await();
assertFalse(getService().wasTickCalled());
barrier.reset();
// No profileName.
intent.putExtra("profilePath", "profilePath")
.removeExtra("profileName");
startService(intent);
await();
assertFalse(getService().wasTickCalled());
barrier.reset();
intent.putExtra("profileName", "profileName");
startService(intent);
await();
assertTrue(getService().wasTickCalled());
}
}

View File

@ -1,109 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.prune;
import java.io.File;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
import android.content.Context;
public class TestPrunePolicyDatabaseStorage extends FakeProfileTestCase {
public static class MockPrunePolicyDatabaseStorage extends PrunePolicyDatabaseStorage {
public final MockHealthReportDatabaseStorage storage;
public int currentEnvironmentID;
public MockPrunePolicyDatabaseStorage(final Context context, final String profilePath) {
super(context, profilePath);
storage = new MockHealthReportDatabaseStorage(context, new File(profilePath));
currentEnvironmentID = -1;
}
@Override
public HealthReportDatabaseStorage getStorage() {
return storage;
}
@Override
public int getCurrentEnvironmentID() {
return currentEnvironmentID;
}
}
public static class MockHealthReportDatabaseStorage extends HealthReportDatabaseStorage {
private boolean wasPruneEventsCalled = false;
private boolean wasPruneEnvironmentsCalled = false;
private boolean wasDeleteDataBeforeCalled = false;
private boolean wasDisableAutoVacuumingCalled = false;
private boolean wasVacuumCalled = false;
public MockHealthReportDatabaseStorage(final Context context, final File file) {
super(context, file);
}
// We use spies here to avoid doing expensive DB operations (which are tested elsewhere).
@Override
public void pruneEvents(final int count) {
wasPruneEventsCalled = true;
}
@Override
public void pruneEnvironments(final int count) {
wasPruneEnvironmentsCalled = true;
}
@Override
public int deleteDataBefore(final long time, final int curEnv) {
wasDeleteDataBeforeCalled = true;
return -1;
}
@Override
public void disableAutoVacuuming() {
wasDisableAutoVacuumingCalled = true;
}
@Override
public void vacuum() {
wasVacuumCalled = true;
}
}
public MockPrunePolicyDatabaseStorage policyStorage;
@Override
public void setUp() throws Exception {
super.setUp();
policyStorage = new MockPrunePolicyDatabaseStorage(context, "profilePath");
}
@Override
public void tearDown() throws Exception {
super.tearDown();
}
public void testPruneEvents() throws Exception {
policyStorage.pruneEvents(0);
assertTrue(policyStorage.storage.wasPruneEventsCalled);
}
public void testPruneEnvironments() throws Exception {
policyStorage.pruneEnvironments(0);
assertTrue(policyStorage.storage.wasPruneEnvironmentsCalled);
}
public void testDeleteDataBefore() throws Exception {
policyStorage.deleteDataBefore(-1);
assertTrue(policyStorage.storage.wasDeleteDataBeforeCalled);
}
public void testCleanup() throws Exception {
policyStorage.cleanup();
assertTrue(policyStorage.storage.wasDisableAutoVacuumingCalled);
assertTrue(policyStorage.storage.wasVacuumCalled);
}
}

View File

@ -1,206 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.upload;
import java.util.Collection;
import org.mozilla.gecko.background.bagheera.BagheeraRequestDelegate;
import org.mozilla.gecko.background.healthreport.Environment;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
import org.mozilla.gecko.background.healthreport.HealthReportStorage;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.healthreport.MockHealthReportDatabaseStorage.PrepopulatedMockHealthReportDatabaseStorage;
import org.mozilla.gecko.background.healthreport.MockProfileInformationCache;
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient;
import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient.SubmissionsFieldName;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
import org.mozilla.gecko.background.testhelpers.StubDelegate;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.SharedPreferences;
import org.json.JSONException;
import org.json.JSONObject;
public class TestAndroidSubmissionClient extends FakeProfileTestCase {
public static class MockAndroidSubmissionClient extends AndroidSubmissionClient {
protected final PrepopulatedMockHealthReportDatabaseStorage storage;
public SubmissionState submissionState = SubmissionState.SUCCESS;
public DocumentStatus documentStatus = DocumentStatus.VALID;
public boolean hasUploadBeenRequested = true;
public MockAndroidSubmissionClient(final Context context, final SharedPreferences sharedPrefs,
final PrepopulatedMockHealthReportDatabaseStorage storage) {
super(context, sharedPrefs, "profilePath");
this.storage = storage;
}
@Override
public HealthReportDatabaseStorage getStorage(final ContentProviderClient client) {
return storage;
}
@Override
public boolean hasUploadBeenRequested() {
return hasUploadBeenRequested;
}
@Override
protected void uploadPayload(String id, String payload, Collection<String> oldIds,
BagheeraRequestDelegate delegate) {
switch (submissionState) {
case SUCCESS:
delegate.handleSuccess(0, null, id, null);
break;
case FAILURE:
delegate.handleFailure(0, null, null);
break;
case ERROR:
delegate.handleError(null);
break;
default:
throw new IllegalStateException("Unknown submissionState, " + submissionState);
}
}
@Override
public SubmissionsTracker getSubmissionsTracker(final HealthReportStorage storage,
final long localTime, final boolean hasUploadBeenRequested) {
return new MockSubmissionsTracker(storage, localTime, hasUploadBeenRequested);
}
public class MockSubmissionsTracker extends SubmissionsTracker {
public MockSubmissionsTracker (final HealthReportStorage storage, final long localTime,
final boolean hasUploadBeenRequested) {
super(storage, localTime, hasUploadBeenRequested);
}
@Override
public ProfileInformationCache getProfileInformationCache() {
final MockProfileInformationCache cache = new MockProfileInformationCache(profilePath);
cache.setInitialized(true); // Will throw errors otherwise.
return cache;
}
@Override
public TrackingGenerator getGenerator() {
return new MockTrackingGenerator();
}
public class MockTrackingGenerator extends TrackingGenerator {
@Override
public JSONObject generateDocument(final long localTime, final long last,
final String profilePath, ConfigurationProvider config) throws JSONException {
switch (documentStatus) {
case VALID:
return new JSONObject(); // Beyond == null, we don't check for valid FHR documents.
case NULL:
// The overridden method should return null since we return a null has for the current
// Environment.
return super.generateDocument(localTime, last, profilePath, config);
case EXCEPTION:
throw new IllegalStateException("Intended Exception");
default:
throw new IllegalStateException("Unintended Exception");
}
}
// Used in super.generateDocument, where a null document is returned if getHash returns null
@Override
public Environment getCurrentEnvironment() {
return new Environment() {
@Override
public int register() {
return 0;
}
@Override
public String getHash() {
return null;
}
};
}
}
}
}
public final SubmissionsFieldName[] SUBMISSIONS_STATUS_FIELD_NAMES = new SubmissionsFieldName[] {
SubmissionsFieldName.SUCCESS,
SubmissionsFieldName.CLIENT_FAILURE,
SubmissionsFieldName.TRANSPORT_FAILURE,
SubmissionsFieldName.SERVER_FAILURE
};
public static enum SubmissionState { SUCCESS, FAILURE, ERROR }
public static enum DocumentStatus { VALID, NULL, EXCEPTION };
public StubDelegate stubDelegate;
public PrepopulatedMockHealthReportDatabaseStorage storage;
public MockAndroidSubmissionClient client;
public void setUp() throws Exception {
super.setUp();
stubDelegate = new StubDelegate();
storage = new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
client = new MockAndroidSubmissionClient(context, getSharedPreferences(), storage);
}
public int getSubmissionsCount(final SubmissionsFieldName fieldName) {
final int id = fieldName.getID(storage);
return storage.getIntFromQuery("SELECT COUNT(*) FROM events WHERE field = " + id, null);
}
public void testUploadSubmissionsFirstAttemptCount() throws Exception {
client.hasUploadBeenRequested = false;
client.upload(storage.now, null, null, stubDelegate);
assertEquals(1, getSubmissionsCount(SubmissionsFieldName.FIRST_ATTEMPT));
assertEquals(0, getSubmissionsCount(SubmissionsFieldName.CONTINUATION_ATTEMPT));
}
public void testUploadSubmissionsContinuationAttemptCount() throws Exception {
client.upload(storage.now, null, null, stubDelegate);
assertEquals(0, getSubmissionsCount(SubmissionsFieldName.FIRST_ATTEMPT));
assertEquals(1, getSubmissionsCount(SubmissionsFieldName.CONTINUATION_ATTEMPT));
}
/**
* Asserts that the given field name has a count of 1 and all other status (success and failures)
* have a count of 0.
*/
public void assertStatusCount(final SubmissionsFieldName fieldName) {
for (SubmissionsFieldName name : SUBMISSIONS_STATUS_FIELD_NAMES) {
if (name == fieldName) {
assertEquals(1, getSubmissionsCount(name));
} else {
assertEquals(0, getSubmissionsCount(name));
}
}
}
public void testUploadSubmissionsSuccessCount() throws Exception {
client.upload(storage.now, null, null, stubDelegate);
assertStatusCount(SubmissionsFieldName.SUCCESS);
}
public void testUploadNullDocumentSubmissionsFailureCount() throws Exception {
client.documentStatus = DocumentStatus.NULL;
client.upload(storage.now, null, null, stubDelegate);
assertStatusCount(SubmissionsFieldName.CLIENT_FAILURE);
}
public void testUploadDocumentGenerationExceptionSubmissionsFailureCount() throws Exception {
client.documentStatus = DocumentStatus.EXCEPTION;
client.upload(storage.now, null, null, stubDelegate);
assertStatusCount(SubmissionsFieldName.CLIENT_FAILURE);
}
}

View File

@ -1,102 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.upload;
import java.util.concurrent.BrokenBarrierException;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import org.mozilla.gecko.background.helpers.BackgroundServiceTestCase;
import android.content.Intent;
import android.content.SharedPreferences;
public class TestHealthReportUploadService
extends BackgroundServiceTestCase<TestHealthReportUploadService.MockHealthReportUploadService> {
public static class MockHealthReportUploadService extends HealthReportUploadService {
@Override
protected SharedPreferences getSharedPreferences() {
return this.getSharedPreferences(sharedPrefsName,
GlobalConstants.SHARED_PREFERENCES_MODE);
}
@Override
public boolean backgroundDataIsEnabled() {
// When testing, we always want to say we can upload.
return true;
}
@Override
public void onHandleIntent(Intent intent) {
super.onHandleIntent(intent);
try {
barrier.await();
} catch (InterruptedException e) {
fail("Awaiting thread should not be interrupted.");
} catch (BrokenBarrierException e) {
// This will happen on timeout - do nothing.
}
}
}
public TestHealthReportUploadService() {
super(MockHealthReportUploadService.class);
}
protected boolean isFirstRunSet() throws Exception {
return getSharedPreferences().contains(HealthReportConstants.PREF_FIRST_RUN);
}
@Override
public void setUp() throws Exception {
super.setUp();
// First run state is used for comparative testing.
assertFalse(isFirstRunSet());
}
public void testFailedFirstRun() throws Exception {
// Missing "uploadEnabled".
intent.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath");
startService(intent);
await();
assertFalse(isFirstRunSet());
barrier.reset();
// Missing "profilePath".
intent.putExtra("uploadEnabled", true)
.removeExtra("profilePath");
startService(intent);
await();
assertFalse(isFirstRunSet());
barrier.reset();
// Missing "profileName".
intent.putExtra("profilePath", "profilePath")
.removeExtra("profileName");
startService(intent);
assertFalse(isFirstRunSet());
await();
assertFalse(isFirstRunSet());
}
public void testUploadDisabled() throws Exception {
intent.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath")
.putExtra("uploadEnabled", false);
startService(intent);
await();
assertFalse(isFirstRunSet());
}
public void testSuccessfulFirstRun() throws Exception {
intent.putExtra("profileName", "profileName")
.putExtra("profilePath", "profilePath")
.putExtra("uploadEnabled", true);
startService(intent);
await();
assertTrue(isFirstRunSet());
}
}

View File

@ -1,15 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.testhelpers;
import org.mozilla.gecko.background.healthreport.upload.SubmissionClient.Delegate;
public class StubDelegate implements Delegate {
@Override
public void onSoftFailure(long localTime, String id, String reason, Exception e) { }
@Override
public void onHardFailure(long localTime, String id, String reason, Exception e) { }
@Override
public void onSuccess(long localTime, String id) { }
}

View File

@ -1,326 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.prune.test;
import android.content.SharedPreferences;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import org.mozilla.gecko.background.healthreport.prune.PrunePolicy;
import org.mozilla.gecko.background.healthreport.prune.PrunePolicyStorage;
import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(TestRunner.class)
public class TestPrunePolicy {
public static class MockPrunePolicy extends PrunePolicy {
public MockPrunePolicy(final PrunePolicyStorage storage, final SharedPreferences sharedPrefs) {
super(storage, sharedPrefs);
}
@Override
public boolean attemptPruneBySize(final long time) {
return super.attemptPruneBySize(time);
}
@Override
public boolean attemptExpiration(final long time) {
return super.attemptExpiration(time);
}
@Override
protected boolean attemptStorageCleanup(final long time) {
return super.attemptStorageCleanup(time);
}
}
public static class MockPrunePolicyStorage implements PrunePolicyStorage {
public int eventCount = -1;
public int environmentCount = -1;
// TODO: Spies - should we be using a framework?
// TODO: Each method was called with what args?
public boolean wasPruneEventsCalled = false;
public boolean wasPruneEnvironmentsCalled = false;
public boolean wasDeleteDataBeforeCalled = false;
public boolean wasCleanupCalled = false;
public MockPrunePolicyStorage() { }
public void pruneEvents(final int maxNumToPrune) {
wasPruneEventsCalled = true;
}
public void pruneEnvironments(final int numToPrune) {
wasPruneEnvironmentsCalled = true;
}
public int deleteDataBefore(final long time) {
wasDeleteDataBeforeCalled = true;
return -1;
}
public void cleanup() {
wasCleanupCalled = true;
}
public int getEventCount() { return eventCount; }
public int getEnvironmentCount() { return environmentCount; }
public void close() { /* Nothing to cleanup. */ }
}
// An arbitrary value so that each test doesn't need to specify its own time.
public static final long START_TIME = 1000;
public MockPrunePolicy policy;
public MockPrunePolicyStorage storage;
public SharedPreferences sharedPrefs;
@Before
public void setUp() throws Exception {
sharedPrefs = new MockSharedPreferences();
storage = new MockPrunePolicyStorage();
policy = new MockPrunePolicy(storage, sharedPrefs);
}
public boolean attemptPruneBySize(final long time) {
final boolean retval = policy.attemptPruneBySize(time);
// This commit may be deferred over multiple methods so we ensure it runs.
sharedPrefs.edit().commit();
return retval;
}
public boolean attemptExpiration(final long time) {
final boolean retval = policy.attemptExpiration(time);
// This commit may be deferred over multiple methods so we ensure it runs.
sharedPrefs.edit().commit();
return retval;
}
public boolean attemptStorageCleanup(final long time) {
final boolean retval = policy.attemptStorageCleanup(time);
// This commit may be deferred over multiple methods so we ensure it runs.
sharedPrefs.edit().commit();
return retval;
}
@Test
public void testAttemptPruneBySizeInit() throws Exception {
assertFalse(containsNextPruneBySizeTime());
attemptPruneBySize(START_TIME);
// Next time should be initialized.
assertTrue(containsNextPruneBySizeTime());
assertTrue(getNextPruneBySizeTime() > 0);
assertFalse(storage.wasPruneEventsCalled);
assertFalse(storage.wasPruneEnvironmentsCalled);
}
@Test
public void testAttemptPruneBySizeEarly() throws Exception {
final long nextTime = START_TIME + 1;
setNextPruneBySizeTime(nextTime);
attemptPruneBySize(START_TIME);
// We didn't prune so next time remains the same.
assertEquals(nextTime, getNextPruneBySizeTime());
assertFalse(storage.wasPruneEventsCalled);
assertFalse(storage.wasPruneEnvironmentsCalled);
}
@Test
public void testAttemptPruneBySizeSkewed() throws Exception {
setNextPruneBySizeTime(START_TIME + getMinimumTimeBetweenPruneBySizeChecks() + 1);
attemptPruneBySize(START_TIME);
// Skewed so the next time is reset.
assertEquals(START_TIME + getMinimumTimeBetweenPruneBySizeChecks(), getNextPruneBySizeTime());
assertFalse(storage.wasPruneEventsCalled);
assertFalse(storage.wasPruneEnvironmentsCalled);
}
@Test
public void testAttemptPruneBySizeTooFewEnvironments() throws Exception {
setNextPruneBySizeTime(START_TIME - 1);
storage.environmentCount = getMaximumEnvironmentCount(); // Too few to prune.
attemptPruneBySize(START_TIME);
assertEquals(START_TIME + getMinimumTimeBetweenPruneBySizeChecks(), getNextPruneBySizeTime());
assertFalse(storage.wasPruneEnvironmentsCalled);
}
@Test
public void testAttemptPruneBySizeEnvironmentsSuccess() throws Exception {
setNextPruneBySizeTime(START_TIME - 1);
storage.environmentCount = getMaximumEnvironmentCount() + 1;
attemptPruneBySize(START_TIME);
assertEquals(START_TIME + getMinimumTimeBetweenPruneBySizeChecks(), getNextPruneBySizeTime());
assertTrue(storage.wasPruneEnvironmentsCalled);
}
@Test
public void testAttemptPruneBySizeTooFewEvents() throws Exception {
setNextPruneBySizeTime(START_TIME - 1);
storage.eventCount = getMaximumEventCount(); // Too few to prune.
attemptPruneBySize(START_TIME);
assertEquals(START_TIME + getMinimumTimeBetweenPruneBySizeChecks(), getNextPruneBySizeTime());
assertFalse(storage.wasPruneEventsCalled);
}
@Test
public void testAttemptPruneBySizeEventsSuccess() throws Exception {
setNextPruneBySizeTime(START_TIME - 1);
storage.eventCount = getMaximumEventCount() + 1;
attemptPruneBySize(START_TIME);
assertEquals(START_TIME + getMinimumTimeBetweenPruneBySizeChecks(), getNextPruneBySizeTime());
assertTrue(storage.wasPruneEventsCalled);
}
@Test
public void testAttemptExpirationInit() throws Exception {
assertFalse(containsNextExpirationTime());
attemptExpiration(START_TIME);
// Next time should be initialized.
assertTrue(containsNextExpirationTime());
assertTrue(getNextExpirationTime() > 0);
assertFalse(storage.wasDeleteDataBeforeCalled);
}
@Test
public void testAttemptExpirationEarly() throws Exception {
final long nextTime = START_TIME + 1;
setNextExpirationTime(nextTime);
attemptExpiration(START_TIME);
// We didn't prune so next time remains the same.
assertEquals(nextTime, getNextExpirationTime());
assertFalse(storage.wasDeleteDataBeforeCalled);
}
@Test
public void testAttemptExpirationSkewed() throws Exception {
setNextExpirationTime(START_TIME + getMinimumTimeBetweenExpirationChecks() + 1);
attemptExpiration(START_TIME);
// Skewed so the next time is reset.
assertEquals(START_TIME + getMinimumTimeBetweenExpirationChecks(), getNextExpirationTime());
assertFalse(storage.wasDeleteDataBeforeCalled);
}
@Test
public void testAttemptExpirationSuccess() throws Exception {
setNextExpirationTime(START_TIME - 1);
attemptExpiration(START_TIME);
assertEquals(START_TIME + getMinimumTimeBetweenExpirationChecks(), getNextExpirationTime());
assertTrue(storage.wasDeleteDataBeforeCalled);
}
@Test
public void testAttemptCleanupInit() throws Exception {
assertFalse(containsNextCleanupTime());
attemptStorageCleanup(START_TIME);
// Next time should be initialized.
assertTrue(containsNextCleanupTime());
assertTrue(getNextCleanupTime() > 0);
assertFalse(storage.wasCleanupCalled);
}
@Test
public void testAttemptCleanupEarly() throws Exception {
final long nextTime = START_TIME + 1;
setNextCleanupTime(nextTime);
attemptStorageCleanup(START_TIME);
// We didn't prune so next time remains the same.
assertEquals(nextTime, getNextCleanupTime());
assertFalse(storage.wasCleanupCalled);
}
@Test
public void testAttemptCleanupSkewed() throws Exception {
setNextCleanupTime(START_TIME + getMinimumTimeBetweenCleanupChecks() + 1);
attemptStorageCleanup(START_TIME);
// Skewed so the next time is reset.
assertEquals(START_TIME + getMinimumTimeBetweenCleanupChecks(), getNextCleanupTime());
assertFalse(storage.wasCleanupCalled);
}
@Test
public void testAttemptCleanupSuccess() throws Exception {
setNextCleanupTime(START_TIME - 1);
attemptStorageCleanup(START_TIME);
assertEquals(START_TIME + getMinimumTimeBetweenCleanupChecks(), getNextCleanupTime());
assertTrue(storage.wasCleanupCalled);
}
public int getMaximumEnvironmentCount() {
return HealthReportConstants.MAX_ENVIRONMENT_COUNT;
}
public int getMaximumEventCount() {
return HealthReportConstants.MAX_EVENT_COUNT;
}
public long getMinimumTimeBetweenPruneBySizeChecks() {
return HealthReportConstants.MINIMUM_TIME_BETWEEN_PRUNE_BY_SIZE_CHECKS_MILLIS;
}
public long getNextPruneBySizeTime() {
return sharedPrefs.getLong(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME, -1);
}
public void setNextPruneBySizeTime(final long time) {
sharedPrefs.edit().putLong(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME, time).commit();
}
public boolean containsNextPruneBySizeTime() {
return sharedPrefs.contains(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME);
}
public long getMinimumTimeBetweenExpirationChecks() {
return HealthReportConstants.MINIMUM_TIME_BETWEEN_EXPIRATION_CHECKS_MILLIS;
}
public long getNextExpirationTime() {
return sharedPrefs.getLong(HealthReportConstants.PREF_EXPIRATION_TIME, -1);
}
public void setNextExpirationTime(final long time) {
sharedPrefs.edit().putLong(HealthReportConstants.PREF_EXPIRATION_TIME, time).commit();
}
public boolean containsNextExpirationTime() {
return sharedPrefs.contains(HealthReportConstants.PREF_EXPIRATION_TIME);
}
public long getMinimumTimeBetweenCleanupChecks() {
return HealthReportConstants.MINIMUM_TIME_BETWEEN_CLEANUP_CHECKS_MILLIS;
}
public long getNextCleanupTime() {
return sharedPrefs.getLong(HealthReportConstants.PREF_CLEANUP_TIME, -1);
}
public void setNextCleanupTime(final long time) {
sharedPrefs.edit().putLong(HealthReportConstants.PREF_CLEANUP_TIME, time).commit();
}
public boolean containsNextCleanupTime() {
return sharedPrefs.contains(HealthReportConstants.PREF_CLEANUP_TIME);
}
}

View File

@ -1,62 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.test;
import android.database.Cursor;
import android.util.SparseArray;
import org.json.JSONObject;
import org.mozilla.gecko.background.healthreport.Environment;
import org.mozilla.gecko.background.healthreport.HealthReportStorage;
public class HealthReportStorageStub implements HealthReportStorage {
public void close() { throw new UnsupportedOperationException(); }
public int getDay(long time) { throw new UnsupportedOperationException(); }
public int getDay() { throw new UnsupportedOperationException(); }
public Environment getEnvironment() { throw new UnsupportedOperationException(); }
public SparseArray<String> getEnvironmentHashesByID() { throw new UnsupportedOperationException(); }
public SparseArray<Environment> getEnvironmentRecordsByID() { throw new UnsupportedOperationException(); }
public Cursor getEnvironmentRecordForID(int id) { throw new UnsupportedOperationException(); }
public Field getField(String measurement, int measurementVersion, String fieldName) {
throw new UnsupportedOperationException();
}
public SparseArray<Field> getFieldsByID() { throw new UnsupportedOperationException(); }
public void recordDailyLast(int env, int day, int field, JSONObject value) { throw new UnsupportedOperationException(); }
public void recordDailyLast(int env, int day, int field, String value) { throw new UnsupportedOperationException(); }
public void recordDailyLast(int env, int day, int field, int value) { throw new UnsupportedOperationException(); }
public void recordDailyDiscrete(int env, int day, int field, JSONObject value) { throw new UnsupportedOperationException(); }
public void recordDailyDiscrete(int env, int day, int field, String value) { throw new UnsupportedOperationException(); }
public void recordDailyDiscrete(int env, int day, int field, int value) { throw new UnsupportedOperationException(); }
public void incrementDailyCount(int env, int day, int field, int by) { throw new UnsupportedOperationException(); }
public void incrementDailyCount(int env, int day, int field) { throw new UnsupportedOperationException(); }
public boolean hasEventSince(long time) { throw new UnsupportedOperationException(); }
public Cursor getRawEventsSince(long time) { throw new UnsupportedOperationException(); }
public Cursor getEventsSince(long time) { throw new UnsupportedOperationException(); }
public void ensureMeasurementInitialized(String measurement, int version, MeasurementFields fields) {
throw new UnsupportedOperationException();
}
public Cursor getMeasurementVersions() { throw new UnsupportedOperationException(); }
public Cursor getFieldVersions() { throw new UnsupportedOperationException(); }
public Cursor getFieldVersions(String measurement, int measurementVersion) { throw new UnsupportedOperationException(); }
public void deleteEverything() { throw new UnsupportedOperationException(); }
public void deleteEnvironments() { throw new UnsupportedOperationException(); }
public void deleteMeasurements() { throw new UnsupportedOperationException(); }
public int deleteDataBefore(final long time, final int curEnv) { throw new UnsupportedOperationException(); }
public int getEventCount() { throw new UnsupportedOperationException(); }
public int getEnvironmentCount() { throw new UnsupportedOperationException(); }
public void pruneEvents(final int num) { throw new UnsupportedOperationException(); }
public void pruneEnvironments(final int num) { throw new UnsupportedOperationException(); }
public void enqueueOperation(Runnable runnable) { throw new UnsupportedOperationException(); }
}

View File

@ -1,116 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.upload.test;
import android.content.Context;
import android.content.SharedPreferences;
import org.mozilla.gecko.background.healthreport.Environment;
import org.mozilla.gecko.background.healthreport.Environment.UIType;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
import org.mozilla.gecko.background.healthreport.HealthReportStorage;
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
import org.mozilla.gecko.background.healthreport.test.HealthReportStorageStub;
import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient;
public class MockAndroidSubmissionClient extends AndroidSubmissionClient {
public MockAndroidSubmissionClient(final Context context, final SharedPreferences prefs,
final String profilePath) {
super(context, prefs, profilePath, new MockConfigurationProvider());
}
public static class MockConfigurationProvider implements ConfigurationProvider {
@Override
public boolean hasHardwareKeyboard() {
return false;
}
@Override
public UIType getUIType() {
return UIType.DEFAULT;
}
@Override
public int getUIModeType() {
return 0;
}
@Override
public int getScreenLayoutSize() {
return 0;
}
@Override
public int getScreenXInMM() {
return 100;
}
@Override
public int getScreenYInMM() {
return 140;
}
}
public class MockSubmissionsTracker extends SubmissionsTracker {
public MockSubmissionsTracker(final HealthReportStorage storage, final long localTime,
final boolean hasUploadBeenRequested) {
super(storage, localTime, hasUploadBeenRequested);
}
// Override so we don't touch storage to register the current env.
// The returned id value does not matter much.
@Override
public int registerCurrentEnvironment() {
return 0;
}
// Override so we don't touch storage to get cache. Only getCurrentEnviroment uses the cache,
// which we override, so we're free to return null.
@Override
public ProfileInformationCache getProfileInformationCache() {
return null;
}
@Override
public TrackingGenerator getGenerator() {
return new MockTrackingGenerator();
}
public class MockTrackingGenerator extends TrackingGenerator {
// Override so it doesn't fail in the constructor when touching the storage stub (below).
@Override
protected Environment getCurrentEnvironment() {
return new Environment() {
@Override
public int register() {
return 0;
}
};
}
}
}
/**
* Mocked HealthReportStorage class for use within the MockAndroidSubmissionClient and its inner
* classes, to prevent access to real storage.
*/
public static class MockHealthReportStorage extends HealthReportStorageStub {
// Ensures a unique Field ID for SubmissionsFieldName.getID().
@Override
public Field getField(String mName, int mVersion, String fieldName) {
return new Field(mName, mVersion, fieldName, 0) {
@Override
public int getID() throws IllegalStateException {
return fieldName.hashCode();
}
};
}
// Called in the SubmissionsTracker constructor.
@Override
public int getDay(final long millis) {
return 0;
}
}
}

View File

@ -1,176 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.upload.test;
import android.content.SharedPreferences;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import org.mozilla.gecko.background.healthreport.upload.ObsoleteDocumentTracker;
import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(TestRunner.class)
public class TestObsoleteDocumentTracker {
public static class MockObsoleteDocumentTracker extends ObsoleteDocumentTracker {
public MockObsoleteDocumentTracker(SharedPreferences sharedPrefs) {
super(sharedPrefs);
}
@Override
public ExtendedJSONObject getObsoleteIds() {
return super.getObsoleteIds();
}
@Override
public void setObsoleteIds(ExtendedJSONObject ids) {
super.setObsoleteIds(ids);
}
};
public MockObsoleteDocumentTracker tracker;
public MockSharedPreferences sharedPrefs;
@Before
public void setUp() {
sharedPrefs = new MockSharedPreferences();
tracker = new MockObsoleteDocumentTracker(sharedPrefs);
}
@Test
public void testDecrementObsoleteIdAttempts() {
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put("id1", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
ids.put("id2", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
tracker.setObsoleteIds(ids);
assertEquals(ids, tracker.getObsoleteIds());
tracker.decrementObsoleteIdAttempts("id1");
ids.put("id1", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID - 1);
assertEquals(ids, tracker.getObsoleteIds());
for (int i = 0; i < HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID; i++) {
tracker.decrementObsoleteIdAttempts("id1");
}
ids.remove("id1");
assertEquals(ids, tracker.getObsoleteIds());
tracker.removeObsoleteId("id2");
ids.remove("id2");
assertEquals(ids, tracker.getObsoleteIds());
}
@Test
public void testAddObsoleteId() {
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put("id1", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
tracker.addObsoleteId("id1");
assertEquals(ids, tracker.getObsoleteIds());
}
@Test
public void testDecrementObsoleteIdAttemptsSet() {
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put("id1", 5L);
ids.put("id2", 1L);
ids.put("id3", -1L); // This should never happen, but just in case.
tracker.setObsoleteIds(ids);
assertEquals(ids, tracker.getObsoleteIds());
HashSet<String> oldIds = new HashSet<String>();
oldIds.add("id1");
oldIds.add("id2");
tracker.decrementObsoleteIdAttempts(oldIds);
ids.put("id1", 4L);
ids.remove("id2");
assertEquals(ids, tracker.getObsoleteIds());
}
@Test
public void testMaximumObsoleteIds() {
for (int i = 1; i < HealthReportConstants.MAXIMUM_STORED_OBSOLETE_DOCUMENT_IDS + 10; i++) {
tracker.addObsoleteId("id" + i);
assertTrue(tracker.getObsoleteIds().size() <= HealthReportConstants.MAXIMUM_STORED_OBSOLETE_DOCUMENT_IDS);
}
}
@Test
public void testMigration() {
ExtendedJSONObject ids = new ExtendedJSONObject();
assertEquals(ids, tracker.getObsoleteIds());
sharedPrefs.edit()
.putString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, "id")
.commit();
ids.put("id", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
assertEquals(ids, tracker.getObsoleteIds());
assertTrue(sharedPrefs.contains(HealthReportConstants.PREF_OBSOLETE_DOCUMENT_IDS_TO_DELETION_ATTEMPTS_REMAINING));
}
@Test
public void testGetBatchOfObsoleteIds() {
ExtendedJSONObject ids = new ExtendedJSONObject();
for (int i = 0; i < 2 * HealthReportConstants.MAXIMUM_DELETIONS_PER_POST + 10; i++) {
ids.put("id" + (100 - i), Long.valueOf(100 - i));
}
tracker.setObsoleteIds(ids);
Set<String> expected = new HashSet<String>();
for (int i = 0; i < HealthReportConstants.MAXIMUM_DELETIONS_PER_POST; i++) {
expected.add("id" + (100 - i));
}
assertEquals(expected, new HashSet<String>(tracker.getBatchOfObsoleteIds()));
}
@Test
public void testPairComparator() {
// Make sure that malformed entries get sorted first.
ArrayList<Entry<String, Object>> list = new ArrayList<Entry<String,Object>>();
list.add(new SimpleImmutableEntry<String, Object>("a", null));
list.add(new SimpleImmutableEntry<String, Object>("d", Long.valueOf(5)));
list.add(new SimpleImmutableEntry<String, Object>("e", Long.valueOf(1)));
list.add(new SimpleImmutableEntry<String, Object>("c", Long.valueOf(10)));
list.add(new SimpleImmutableEntry<String, Object>("b", "test"));
Collections.sort(list, new ObsoleteDocumentTracker.PairComparator());
ArrayList<String> got = new ArrayList<String>();
for (Entry<String, Object> pair : list) {
got.add(pair.getKey());
}
List<String> exp = Arrays.asList(new String[] { "a", "b", "c", "d", "e" });
assertEquals(exp, got);
}
@Test
public void testLimitObsoleteIds() {
ExtendedJSONObject ids = new ExtendedJSONObject();
for (long i = -HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID; i < HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID; i++) {
long j = 1 + HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID + i;
ids.put("id" + j, j);
}
tracker.setObsoleteIds(ids);
tracker.limitObsoleteIds();
assertEquals(ids.keySet(), tracker.getObsoleteIds().keySet());
ExtendedJSONObject got = tracker.getObsoleteIds();
for (String id : ids.keySet()) {
assertTrue(got.getLong(id).longValue() <= HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
}
}
}

View File

@ -1,437 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.upload.test;
import android.content.SharedPreferences;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
import org.mozilla.gecko.background.healthreport.upload.SubmissionClient;
import org.mozilla.gecko.background.healthreport.upload.SubmissionPolicy;
import org.mozilla.gecko.background.healthreport.upload.test.TestObsoleteDocumentTracker.MockObsoleteDocumentTracker;
import org.mozilla.gecko.background.healthreport.upload.test.TestSubmissionPolicy.MockSubmissionClient.Response;
import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.HashSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(TestRunner.class)
public class TestSubmissionPolicy {
public static class MockSubmissionClient implements SubmissionClient {
public String lastId = null;
public Collection<String> lastOldIds = null;
public enum Response { SUCCESS, SOFT_FAILURE, HARD_FAILURE };
public Response upload = Response.SUCCESS;
public Response delete = Response.SUCCESS;
public Exception exception = null;
protected void response(long localTime, String id, Delegate delegate, Response response) {
lastId = id;
switch (response) {
case SOFT_FAILURE:
delegate.onSoftFailure(localTime, id, "Soft failure.", exception);
break;
case HARD_FAILURE:
delegate.onHardFailure(localTime, id, "Hard failure.", exception);
break;
default:
delegate.onSuccess(localTime, id);
}
}
@Override
public void upload(long localTime, String id, Collection<String> oldIds, Delegate delegate) {
lastOldIds = oldIds;
response(localTime, id, delegate, upload);
}
@Override
public void delete(long localTime, String id, Delegate delegate) {
response(localTime, id, delegate, delete);
}
}
public MockSubmissionClient client;
public SubmissionPolicy policy;
public SharedPreferences sharedPrefs;
public MockObsoleteDocumentTracker tracker;
public void setMinimumTimeBetweenUploads(long time) {
sharedPrefs.edit().putLong(HealthReportConstants.PREF_MINIMUM_TIME_BETWEEN_UPLOADS, time).commit();
}
public void setMinimumTimeBeforeFirstSubmission(long time) {
sharedPrefs.edit().putLong(HealthReportConstants.PREF_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION, time).commit();
}
public void setCurrentDayFailureCount(long count) {
sharedPrefs.edit().putLong(HealthReportConstants.PREF_CURRENT_DAY_FAILURE_COUNT, count).commit();
}
@Before
public void setUp() throws Exception {
sharedPrefs = new MockSharedPreferences();
client = new MockSubmissionClient();
tracker = new MockObsoleteDocumentTracker(sharedPrefs);
policy = new SubmissionPolicy(sharedPrefs, client, tracker, true);
setMinimumTimeBeforeFirstSubmission(0);
}
@Test
public void testNoMinimumTimeBeforeFirstSubmission() {
assertTrue(policy.tick(0));
}
@Test
public void testMinimumTimeBeforeFirstSubmission() {
setMinimumTimeBeforeFirstSubmission(10);
assertFalse(policy.tick(0));
assertEquals(policy.getMinimumTimeBeforeFirstSubmission(), policy.getNextSubmission());
assertFalse(policy.tick(policy.getMinimumTimeBeforeFirstSubmission() - 1));
assertTrue(policy.tick(policy.getMinimumTimeBeforeFirstSubmission()));
}
@Test
public void testNextUpload() {
assertTrue(policy.tick(0));
assertEquals(policy.getMinimumTimeBetweenUploads(), policy.getNextSubmission());
assertFalse(policy.tick(policy.getMinimumTimeBetweenUploads() - 1));
assertTrue(policy.tick(policy.getMinimumTimeBetweenUploads()));
}
@Test
public void testLastUploadRequested() {
assertTrue(policy.tick(0));
assertEquals(0, policy.getLastUploadRequested());
assertFalse(policy.tick(1));
assertEquals(0, policy.getLastUploadRequested());
assertTrue(policy.tick(2*policy.getMinimumTimeBetweenUploads()));
assertEquals(2*policy.getMinimumTimeBetweenUploads(), policy.getLastUploadRequested());
}
@Test
public void testUploadSuccess() throws Exception {
assertTrue(policy.tick(0));
setCurrentDayFailureCount(1);
client.upload = Response.SUCCESS;
assertTrue(policy.tick(2*policy.getMinimumTimeBetweenUploads()));
assertEquals(2*policy.getMinimumTimeBetweenUploads(), policy.getLastUploadRequested());
assertEquals(2*policy.getMinimumTimeBetweenUploads(), policy.getLastUploadSucceeded());
assertTrue(policy.getNextSubmission() > policy.getLastUploadSucceeded());
assertEquals(0, policy.getCurrentDayFailureCount());
assertNotNull(client.lastId);
assertTrue(tracker.getObsoleteIds().containsKey(client.lastId));
}
@Test
public void testUploadSoftFailure() throws Exception {
final long initialUploadTime = 0;
assertTrue(policy.tick(initialUploadTime));
client.upload = Response.SOFT_FAILURE;
final long secondUploadTime = initialUploadTime + policy.getMinimumTimeBetweenUploads();
assertTrue(policy.tick(secondUploadTime));
assertEquals(secondUploadTime, policy.getLastUploadRequested());
assertEquals(secondUploadTime, policy.getLastUploadFailed());
assertEquals(1, policy.getCurrentDayFailureCount());
assertEquals(policy.getLastUploadFailed() + policy.getMinimumTimeAfterFailure(), policy.getNextSubmission());
assertNotNull(client.lastId);
assertTrue(tracker.getObsoleteIds().containsKey(client.lastId));
client.lastId = null;
final long thirdUploadTime = secondUploadTime + policy.getMinimumTimeAfterFailure();
assertTrue(policy.tick(thirdUploadTime));
assertEquals(thirdUploadTime, policy.getLastUploadRequested());
assertEquals(thirdUploadTime, policy.getLastUploadFailed());
assertEquals(2, policy.getCurrentDayFailureCount());
assertEquals(policy.getLastUploadFailed() + policy.getMinimumTimeAfterFailure(), policy.getNextSubmission());
assertNotNull(client.lastId);
assertTrue(tracker.getObsoleteIds().containsKey(client.lastId));
client.lastId = null;
final long fourthUploadTime = thirdUploadTime + policy.getMinimumTimeAfterFailure();
assertTrue(policy.tick(fourthUploadTime));
assertEquals(fourthUploadTime, policy.getLastUploadRequested());
assertEquals(fourthUploadTime, policy.getLastUploadFailed());
assertEquals(0, policy.getCurrentDayFailureCount());
assertEquals(policy.getLastUploadFailed() + policy.getMinimumTimeBetweenUploads(), policy.getNextSubmission());
assertNotNull(client.lastId);
assertTrue(tracker.getObsoleteIds().containsKey(client.lastId));
client.lastId = null;
}
@Test
public void testUploadHardFailure() throws Exception {
assertTrue(policy.tick(0));
client.upload = Response.HARD_FAILURE;
assertTrue(policy.tick(2*policy.getMinimumTimeBetweenUploads()));
assertEquals(2*policy.getMinimumTimeBetweenUploads(), policy.getLastUploadRequested());
assertEquals(2*policy.getMinimumTimeBetweenUploads(), policy.getLastUploadFailed());
assertEquals(0, policy.getCurrentDayFailureCount());
assertEquals(policy.getLastUploadFailed() + policy.getMinimumTimeBetweenUploads(), policy.getNextSubmission());
assertNotNull(client.lastId);
assertTrue(tracker.getObsoleteIds().containsKey(client.lastId));
client.lastId = null;
}
@Test
public void testDisabledNoObsoleteDocuments() throws Exception {
policy = new SubmissionPolicy(sharedPrefs, client, tracker, false);
assertFalse(policy.tick(0));
}
@Test
public void testDisabledObsoleteDocumentsSuccess() throws Exception {
policy = new SubmissionPolicy(sharedPrefs, client, tracker, false);
setMinimumTimeBetweenUploads(policy.getMinimumTimeBetweenUploads() - 1);
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put("id1", 5L);
ids.put("id2", 5L);
tracker.setObsoleteIds(ids);
assertTrue(policy.tick(3));
assertEquals(1, tracker.getObsoleteIds().size());
// Forensic timestamps.
assertEquals(3 + policy.getMinimumTimeBetweenDeletes(), policy.getNextSubmission());
assertEquals(3, policy.getLastDeleteRequested());
assertEquals(-1, policy.getLastDeleteFailed());
assertEquals(3, policy.getLastDeleteSucceeded());
assertTrue(policy.tick(2*policy.getMinimumTimeBetweenDeletes()));
assertEquals(0, tracker.getObsoleteIds().size());
assertFalse(policy.tick(4*policy.getMinimumTimeBetweenDeletes()));
}
@Test
public void testDisabledObsoleteDocumentsSoftFailure() throws Exception {
client.delete = Response.SOFT_FAILURE;
policy = new SubmissionPolicy(sharedPrefs, client, tracker, false);
setMinimumTimeBetweenUploads(policy.getMinimumTimeBetweenUploads() - 2);
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put("id1", 5L);
ids.put("id2", 2L);
tracker.setObsoleteIds(ids);
assertTrue(policy.tick(3));
ids.put("id1", 4L); // policy's choice is deterministic.
assertEquals(ids, tracker.getObsoleteIds());
// Forensic timestamps.
assertEquals(3 + policy.getMinimumTimeBetweenDeletes(), policy.getNextSubmission());
assertEquals(3, policy.getLastDeleteRequested());
assertEquals(3, policy.getLastDeleteFailed());
assertEquals(-1, policy.getLastDeleteSucceeded());
assertTrue(policy.tick(2*policy.getMinimumTimeBetweenDeletes())); // 3, 3
ids.put("id1", 3L);
assertEquals(ids, tracker.getObsoleteIds());
assertTrue(policy.tick(4*policy.getMinimumTimeBetweenDeletes()));
assertTrue(policy.tick(6*policy.getMinimumTimeBetweenDeletes()));
assertTrue(policy.tick(8*policy.getMinimumTimeBetweenDeletes()));
assertTrue(policy.tick(10*policy.getMinimumTimeBetweenDeletes()));
ids.put("id1", 1L);
ids.remove("id2");
assertEquals(ids, tracker.getObsoleteIds());
}
@Test
public void testDisabledObsoleteDocumentsHardFailure() throws Exception {
client.delete = Response.HARD_FAILURE;
policy = new SubmissionPolicy(sharedPrefs, client, tracker, false);
setMinimumTimeBetweenUploads(policy.getMinimumTimeBetweenUploads() - 3);
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put("id1", 5L);
ids.put("id2", 2L);
tracker.setObsoleteIds(ids);
assertTrue(policy.tick(3));
ids.remove("id1"); // policy's choice is deterministic.
assertEquals(ids, tracker.getObsoleteIds());
// Forensic timestamps.
assertEquals(3 + policy.getMinimumTimeBetweenDeletes(), policy.getNextSubmission());
assertEquals(3, policy.getLastDeleteRequested());
assertEquals(3, policy.getLastDeleteFailed());
assertEquals(-1, policy.getLastDeleteSucceeded());
assertTrue(policy.tick(2*policy.getMinimumTimeBetweenDeletes())); // 3.
ids.remove("id2");
assertEquals(ids, tracker.getObsoleteIds());
}
@Test
public void testUploadSuccessMultipleObsoletes() throws Exception {
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put("id1", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
ids.put("id2", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
tracker.setObsoleteIds(ids);
assertTrue(policy.tick(3));
assertEquals(ids.keySet(), new HashSet<String>(client.lastOldIds));
assertNotNull(client.lastId);
ids.remove("id1");
ids.remove("id2");
ids.put(client.lastId, HealthReportConstants.DELETION_ATTEMPTS_PER_KNOWN_TO_BE_ON_SERVER_DOCUMENT_ID);
assertEquals(ids, tracker.getObsoleteIds());
ExtendedJSONObject ids1 = new ExtendedJSONObject(); // First half.
ExtendedJSONObject ids2 = new ExtendedJSONObject(); // Second half.
ExtendedJSONObject ids3 = new ExtendedJSONObject(); // Both halves.
for (int i = 0; i < HealthReportConstants.MAXIMUM_DELETIONS_PER_POST; i++) {
String id1 = "x" + i;
String id2 = "y" + i;
ids1.put(id1, 3*HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID + i);
ids2.put(id2, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID + i);
ids3.put(id1, 3*HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID + i);
ids3.put(id2, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID + i);
}
tracker.setObsoleteIds(ids3);
assertTrue(policy.tick(3 + policy.getMinimumTimeBetweenUploads()));
assertNotNull(client.lastId);
assertEquals(ids1.keySet(), new HashSet<String>(client.lastOldIds));
ids2.put(client.lastId, HealthReportConstants.DELETION_ATTEMPTS_PER_KNOWN_TO_BE_ON_SERVER_DOCUMENT_ID);
assertEquals(ids2, tracker.getObsoleteIds());
}
@Test
public void testUploadFailureMultipleObsoletes() throws Exception {
client.upload = Response.HARD_FAILURE;
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put("id1", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
ids.put("id2", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
tracker.setObsoleteIds(ids);
assertTrue(policy.tick(3));
assertEquals(ids.keySet(), new HashSet<String>(client.lastOldIds));
assertNotNull(client.lastId);
ids.put(client.lastId, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
ids.put("id1", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID - 1);
ids.put("id2", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID - 1);
assertEquals(ids, tracker.getObsoleteIds());
ExtendedJSONObject ids1 = new ExtendedJSONObject(); // First half.
ExtendedJSONObject ids2 = new ExtendedJSONObject(); // Second half.
ExtendedJSONObject ids3 = new ExtendedJSONObject(); // Both halves.
for (int i = 0; i < HealthReportConstants.MAXIMUM_DELETIONS_PER_POST; i++) {
String id1 = "x" + i;
String id2 = "y" + i;
ids1.put(id1, 3*HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID + i);
ids2.put(id2, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID + i);
ids3.put(id1, 3*HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID + i);
ids3.put(id2, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID + i);
}
tracker.setObsoleteIds(ids3);
assertTrue(policy.tick(3 + policy.getMinimumTimeBetweenUploads()));
assertEquals(ids1.keySet(), new HashSet<String>(client.lastOldIds));
assertNotNull(client.lastId);
ids3.put(client.lastId, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
for (String id : ids1.keySet()) {
ids3.put(id, ids1.getLong(id) - 1);
}
assertEquals(ids3, tracker.getObsoleteIds());
}
@Test
public void testUploadLocalFailure() throws Exception {
client.upload = Response.HARD_FAILURE;
client.exception = new UnknownHostException();
// We shouldn't add an id for a local exception.
assertFalse(tracker.hasObsoleteIds());
assertTrue(policy.tick(3));
assertFalse(tracker.hasObsoleteIds());
// And we shouldn't decrement the obsolete-delete attempts map.
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put("id1", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
ids.put("id2", HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
tracker.setObsoleteIds(ids);
assertTrue(policy.tick(3 + policy.getMinimumTimeBetweenUploads()));
assertEquals(ids, tracker.getObsoleteIds());
}
@Test
public void testDisabledLocalFailure() throws Exception {
client.delete = Response.SOFT_FAILURE;
client.exception = new UnknownHostException();
policy = new SubmissionPolicy(sharedPrefs, client, tracker, false);
setMinimumTimeBetweenUploads(policy.getMinimumTimeBetweenUploads() - 1);
ExtendedJSONObject ids = new ExtendedJSONObject();
ids.put("id1", 5L);
ids.put("id2", 3L);
tracker.setObsoleteIds(ids);
assertTrue(policy.tick(3));
// The upload fails locally, so we shouldn't decrement attempts remaining.
assertEquals(ids, tracker.getObsoleteIds());
}
@Test
public void testUploadFailureCountResetAfterOneDay() throws Exception {
client.upload = Response.SOFT_FAILURE;
assertEquals(0, policy.getCurrentDayFailureCount());
assertEquals(-1, policy.getCurrentDayResetTime());
final long initialUploadTime = 0;
assertTrue(policy.tick(initialUploadTime));
final long initialResetTime = initialUploadTime + policy.getMinimumTimeBetweenUploads();
assertEquals(initialResetTime, policy.getCurrentDayResetTime());
assertEquals(1, policy.getCurrentDayFailureCount());
final long secondUploadTime = initialUploadTime + policy.getMinimumTimeAfterFailure();
assertTrue(policy.tick(secondUploadTime));
assertEquals(initialResetTime, policy.getCurrentDayResetTime());
assertEquals(2, policy.getCurrentDayFailureCount());
assertTrue(policy.tick(initialResetTime));
final long secondResetTime = initialResetTime + policy.getMinimumTimeBetweenUploads();
assertEquals(secondResetTime, policy.getCurrentDayResetTime());
assertEquals(1, policy.getCurrentDayFailureCount());
}
@Test
public void testUploadFailureCountResetAfterUploadSuccess() throws Exception {
client.upload = Response.SOFT_FAILURE;
final long uploadTime = 0;
assertTrue(policy.tick(uploadTime));
assertEquals(1, policy.getCurrentDayFailureCount());
client.upload = Response.SUCCESS;
assertTrue(policy.tick(uploadTime + policy.getMinimumTimeAfterFailure()));
assertEquals(-1, policy.getCurrentDayResetTime());
assertEquals(0, policy.getCurrentDayFailureCount());
}
@Test
public void testUploadFailureCountResetAfterUploadHardFailure() throws Exception {
client.upload = Response.SOFT_FAILURE;
final long uploadTime = 0;
assertTrue(policy.tick(uploadTime));
assertEquals(1, policy.getCurrentDayFailureCount());
client.upload = Response.HARD_FAILURE;
assertTrue(policy.tick(uploadTime + policy.getMinimumTimeAfterFailure()));
assertEquals(-1, policy.getCurrentDayResetTime());
assertEquals(0, policy.getCurrentDayFailureCount());
}
}

View File

@ -1,118 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.upload.test;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient.SubmissionsFieldName;
import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient.MockHealthReportStorage;
import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient.MockSubmissionsTracker;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import java.util.Arrays;
import java.util.HashSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@RunWith(TestRunner.class)
public class TestSubmissionsTracker {
protected static class MockHealthReportStorage2 extends MockHealthReportStorage {
public final int FIRST_ATTEMPT_ID = SubmissionsFieldName.FIRST_ATTEMPT.getID(this);
public final int CONTINUATION_ATTEMPT_ID = SubmissionsFieldName.CONTINUATION_ATTEMPT.getID(this);
public final int SUCCESS_ID = SubmissionsFieldName.SUCCESS.getID(this);
public final int CLIENT_FAILURE_ID = SubmissionsFieldName.CLIENT_FAILURE.getID(this);
public final int TRANSPORT_FAILURE_ID = SubmissionsFieldName.TRANSPORT_FAILURE.getID(this);
public final int SERVER_FAILURE_ID = SubmissionsFieldName.SERVER_FAILURE.getID(this);
private final HashSet<Integer> GUARDED_FIELDS = new HashSet<Integer>(Arrays.asList(new Integer[] {
SUCCESS_ID,
CLIENT_FAILURE_ID,
TRANSPORT_FAILURE_ID,
SERVER_FAILURE_ID
}));
public int incrementedFieldID = -1;
private boolean hasIncrementedGuardedFields = false;
@Override
public void incrementDailyCount(int env, int day, int field) {
incrementedFieldID = field;
if (GUARDED_FIELDS.contains(field)) {
if (hasIncrementedGuardedFields) {
fail("incrementDailyCount called twice with the same guarded field.");
}
hasIncrementedGuardedFields = true;
}
}
}
private MockHealthReportStorage2 storage;
private MockAndroidSubmissionClient client;
private MockSubmissionsTracker tracker;
@Before
public void setUp() throws Exception {
storage = new MockHealthReportStorage2();
client = new MockAndroidSubmissionClient(null, null, null);
tracker = client.new MockSubmissionsTracker(storage, 1, false);
storage.incrementedFieldID = -1; // Reset since this is overwritten in the constructor.
}
@Test
public void testIncrementFirstUploadAttemptCount() throws Exception {
storage = new MockHealthReportStorage2();
client = new MockAndroidSubmissionClient(null, null, null);
tracker = client.new MockSubmissionsTracker(storage, 1, false);
assertEquals(storage.FIRST_ATTEMPT_ID, storage.incrementedFieldID);
}
@Test
public void testIncrementContinuationAttemptCount() throws Exception {
storage = new MockHealthReportStorage2();
client = new MockAndroidSubmissionClient(null, null, null);
tracker = client.new MockSubmissionsTracker(storage, 1, true);
assertEquals(storage.CONTINUATION_ATTEMPT_ID, storage.incrementedFieldID);
}
@Test
public void testIncrementUploadSuccessCount() throws Exception {
tracker.incrementUploadSuccessCount();
assertEquals(storage.SUCCESS_ID, storage.incrementedFieldID);
}
@Test
public void testIncrementClientFailureCount() throws Exception {
tracker.incrementUploadClientFailureCount();
assertEquals(storage.CLIENT_FAILURE_ID, storage.incrementedFieldID);
}
@Test
public void testIncrementServerFailureCount() throws Exception {
tracker.incrementUploadServerFailureCount();
assertEquals(storage.SERVER_FAILURE_ID, storage.incrementedFieldID);
}
@Test
public void testIncrementUploadTransportFailureCount() throws Exception {
tracker.incrementUploadTransportFailureCount();
assertEquals(storage.TRANSPORT_FAILURE_ID, storage.incrementedFieldID);
}
@Test
public void testIncrementCountGuardedFields() throws Exception {
// If the storage instance is incremented twice, the overridden HealthReportStorage methods
// will trip a failure assertion.
tracker.incrementUploadSuccessCount();
tracker.incrementUploadClientFailureCount();
tracker.incrementUploadServerFailureCount();
tracker.incrementUploadTransportFailureCount();
// The first field incremented should be the only (and thus last) one incremented.
assertEquals(storage.SUCCESS_ID, storage.incrementedFieldID);
}
}

View File

@ -1,132 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.healthreport.upload.test;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.healthreport.HealthReportStorage;
import org.mozilla.gecko.background.healthreport.upload.AndroidSubmissionClient.SubmissionsTracker.TrackingRequestDelegate;
import org.mozilla.gecko.background.healthreport.upload.test.MockAndroidSubmissionClient.MockHealthReportStorage;
import org.mozilla.gecko.background.testhelpers.StubDelegate;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.util.HashSet;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@RunWith(TestRunner.class)
public class TestTrackingRequestDelegate {
public static class MockAndroidSubmissionClient2 extends MockAndroidSubmissionClient {
public MockAndroidSubmissionClient2() {
super(null, null, null);
}
@Override
public void setLastUploadLocalTimeAndDocumentId(long localTime, String id) { /* Do nothing. */ }
public class MockSubmissionsTracker2 extends MockSubmissionsTracker {
public HashSet<InvocationResult> invocationResults;
public MockSubmissionsTracker2(final HealthReportStorage storage) {
super(storage, 1, false);
reset();
}
public void reset() {
invocationResults = new HashSet<InvocationResult>(InvocationResult.values().length);
}
@Override
public void incrementFirstUploadAttemptCount() { /* Do nothing. */ }
@Override
public void incrementContinuationAttemptCount() { /* Do nothing. */ }
@Override
public void incrementUploadSuccessCount() {
invocationResults.add(InvocationResult.SUCCESS);
}
@Override
public void incrementUploadClientFailureCount() {
invocationResults.add(InvocationResult.CLIENT_FAILURE);
}
@Override
public void incrementUploadTransportFailureCount() {
invocationResults.add(InvocationResult.TRANSPORT_FAILURE);
}
@Override
public void incrementUploadServerFailureCount() {
invocationResults.add(InvocationResult.SERVER_FAILURE);
}
public boolean gotResult(final InvocationResult resultToCheck) {
return invocationResults.contains(resultToCheck);
}
/**
* Asserts that the given result has been received and all others have not been. Passing null
* will assert that no results have been received.
*/
public void assertResult(final InvocationResult expectedResult) {
for (InvocationResult compareResult : InvocationResult.values()) {
if (compareResult == expectedResult) {
assertTrue(gotResult(compareResult));
} else {
assertFalse(gotResult(compareResult));
}
}
}
}
}
public static enum InvocationResult { SUCCESS, CLIENT_FAILURE, TRANSPORT_FAILURE, SERVER_FAILURE }
public MockAndroidSubmissionClient2.MockSubmissionsTracker2 tracker;
public TrackingRequestDelegate delegate;
@Before
public void setUp() throws Exception {
final MockAndroidSubmissionClient2 client = new MockAndroidSubmissionClient2();
final MockHealthReportStorage storage = new MockHealthReportStorage();
tracker = client.new MockSubmissionsTracker2(storage);
delegate = tracker.new TrackingRequestDelegate(new StubDelegate(), 1, true, null);
}
@Test
public void testHandleSuccess() throws Exception {
delegate.handleSuccess(200, null, null, null);
tracker.assertResult(InvocationResult.SUCCESS);
}
@Test
public void testHandleFailure() throws Exception {
delegate.handleFailure(404, null, null);
tracker.assertResult(InvocationResult.SERVER_FAILURE);
}
@Test
public void testHandleError() throws Exception {
delegate.handleError(new IllegalStateException());
tracker.assertResult(InvocationResult.TRANSPORT_FAILURE);
tracker.reset();
final Exception[] clientExceptions = new Exception[] {
new IllegalArgumentException(),
new UnsupportedEncodingException(),
new URISyntaxException("input", "a good raisin")
};
for (Exception e : clientExceptions) {
delegate.handleError(e);
tracker.assertResult(InvocationResult.CLIENT_FAILURE);
tracker.reset();
}
}
}

View File

@ -1,59 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.test;
import ch.boye.httpclientandroidlib.HttpEntity;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.bagheera.BoundedByteArrayEntity;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import java.io.IOException;
import java.util.Arrays;
@RunWith(TestRunner.class)
public class TestBoundedByteArrayEntity {
private static void expectFail(byte[] input, int start, int end) {
try {
new BoundedByteArrayEntity(input, start, end);
Assert.fail("Should have thrown.");
} catch (Exception ex) {
return;
}
}
private static void expectEmpty(byte[] source, int start, int end) {
final HttpEntity entity = new BoundedByteArrayEntity(source, start, end);
Assert.assertEquals(0, entity.getContentLength());
}
private static void expectSubArray(byte[] source, int start, int end) throws IOException {
final HttpEntity entity = new BoundedByteArrayEntity(source, start, end);
int expectedLength = end - start;
byte[] expectedContents = Arrays.copyOfRange(source, start, end);
Assert.assertEquals(expectedLength, entity.getContentLength());
Assert.assertArrayEquals(expectedContents, EntityTestHelper.bytesFromEntity(entity));
}
@Test
public final void testFail() {
byte[] input = new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5, 0x6 };
expectFail(input, -1, 3);
expectFail(input, 3, 2);
expectFail(input, 0, input.length + 1);
expectFail(input, input.length + 1, input.length + 2);
}
@Test
public final void testBounds() throws IOException {
byte[] input = new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5, 0x6 };
expectEmpty(input, 0, 0);
expectEmpty(input, 3, 3);
expectEmpty(input, input.length, input.length);
expectSubArray(input, 0, 3);
expectSubArray(input, 3, input.length);
expectSubArray(input, 0, input.length);
}
}

View File

@ -1,127 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.test;
import ch.boye.httpclientandroidlib.HttpEntity;
import junit.framework.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.bagheera.DeflateHelper;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import java.util.Arrays;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
@RunWith(TestRunner.class)
public class TestDeflation {
public static final String TEST_BODY_A = "";
public static final String TEST_BODY_B = "éíôü}ABCDEFGHaaQRSTUVWXYZá{Zá{";
public static final String TEST_BODY_C = "{}\n";
public static final String TEST_BODY_D =
"{éíôü}ABCDEFGHaaQRSTUVWXYZá{Zá{éíôü}ABCDEFGHaaQRSTUVWXYZá{Zá{éíôü}A" +
"BCDEFGHaaQRSTUVWXYZá{Zá{éíôü}ABCDEFGHaaQRSTUVWXYZá{Zá{éíôü}ABCDEFGH" +
"aQRSTUVWXYZá{Zá{éíôü}ABCDEFGHaaQRSTUVWXYZá{Zá{}\n";
@SuppressWarnings("static-method")
@Test
public void testDeflateRoundtrip() throws Exception {
doDeflateRoundtrip(TEST_BODY_A);
doDeflateRoundtrip(TEST_BODY_B);
doDeflateRoundtrip(TEST_BODY_C);
doDeflateRoundtrip(TEST_BODY_D);
}
@SuppressWarnings("static-method")
@Test
public void testClientRoundtrip() throws Exception {
doEntityRoundtrip(TEST_BODY_A);
doEntityRoundtrip(TEST_BODY_B);
doEntityRoundtrip(TEST_BODY_C);
doEntityRoundtrip(TEST_BODY_D);
}
@SuppressWarnings("static-method")
@Test
/**
* Compare direct deflation to deflation through HttpEntity in DeflateHelper.
*/
public void testEntityVersusDirect() throws Exception {
final String in = TEST_BODY_D;
final byte[] direct = deflateTrimmed(in.getBytes("UTF-8"));
final byte[] entity = EntityTestHelper.bytesFromEntity(DeflateHelper.deflateBody(in));
assertEqualArrays(direct, entity);
}
public static int reinflateBytes(byte[] input, byte[] output, int inLength) throws DataFormatException {
final Inflater inflater = new Inflater();
inflater.setInput(input, 0, inLength);
int resultLength = inflater.inflate(output);
inflater.end();
Logger.debug("reinflateBytes", "Reinflating: " + inLength + " => " + resultLength);
return resultLength;
}
/**
* Deflate the input, returning a new array of the appropriate size.
*/
public static byte[] deflateTrimmed(byte[] input) {
final int byteCount = input.length;
final byte[] deflated = new byte[DeflateHelper.deflateBound(byteCount)];
final int deflatedLength = DeflateHelper.deflate(input, deflated);
final byte[] trimmed = Arrays.copyOf(deflated, deflatedLength);
return trimmed;
}
/**
* Deflate via direct calls to Deflater, then reinflate and verify
* round-tripping.
*/
private static void doDeflateRoundtrip(final String in) throws Exception {
final byte[] input = in.getBytes("UTF-8");
final int charCount = in.length();
final int byteCount = input.length;
// Deflate on short strings requires *more* room. deflateBound takes that
// into account.
final byte[] deflated = new byte[DeflateHelper.deflateBound(byteCount)];
final int deflatedLength = DeflateHelper.deflate(input, deflated);
Logger.debug("doDeflateRoundtrip",
"Deflated " + byteCount + " bytes (" + charCount +
" chars) to " + deflatedLength);
final byte[] result = new byte[byteCount];
final int resultLength = reinflateBytes(deflated, result, deflatedLength);
Logger.debug("doDeflateRoundtrip", "Got: " + resultLength);
final String outputString = new String(result, 0, resultLength, "UTF-8");
Logger.debug("doDeflateRoundtrip", "Comparing: " + in);
Logger.debug("doDeflateRoundtrip", "Comparing: " + outputString);
Assert.assertEquals(in, outputString);
}
private static void doEntityRoundtrip(final String in) throws Exception {
final HttpEntity entity = DeflateHelper.deflateBody(in);
final int contentLength = (int) entity.getContentLength();
final byte[] result = new byte[in.length() + 36]; // We cheat.
final byte[] bytes = EntityTestHelper.bytesFromEntity(entity);
final int resultLength = reinflateBytes(bytes, result, contentLength);
Logger.debug("doEntityRoundtrip", "Entity: " + Arrays.toString(bytes));
Logger.debug("doEntityRoundtrip", "Got: " + resultLength);
final String outputString = new String(result, 0, resultLength, "UTF-8");
Logger.debug("doEntityRoundtrip", "Comparing: " + in);
Logger.debug("doEntityRoundtrip", "Comparing: " + outputString);
Assert.assertEquals(in, outputString);
}
private static void assertEqualArrays(byte[] a, byte[] b) {
Logger.trace("assertEqualArrays", "A: " + Arrays.toString(a));
Logger.trace("assertEqualArrays", "B: " + Arrays.toString(b));
Assert.assertTrue(Arrays.equals(a, b));
}
}

View File

@ -1,15 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.testhelpers;
import org.mozilla.gecko.background.healthreport.upload.SubmissionClient.Delegate;
public class StubDelegate implements Delegate {
@Override
public void onSoftFailure(long localTime, String id, String reason, Exception e) { }
@Override
public void onHardFailure(long localTime, String id, String reason, Exception e) { }
@Override
public void onSuccess(long localTime, String id) { }
}