mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-15 14:25:52 +00:00
Bug 1183320
- Remove FHR from android/services r=rnewman
This commit is contained in:
parent
9433e40e51
commit
00f8f44c83
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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>
|
@ -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" />
|
@ -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>
|
@ -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" />
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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',
|
||||
]
|
||||
|
@ -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]
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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) { }
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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(); }
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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) { }
|
||||
}
|
Loading…
Reference in New Issue
Block a user