Skip to content

Commit

Permalink
Merge pull request #209 from DP-3T/develop
Browse files Browse the repository at this point in the history
Version 1.0.9
  • Loading branch information
simonroesch authored Aug 28, 2020
2 parents adae3b1 + adb0e72 commit 286fb39
Show file tree
Hide file tree
Showing 58 changed files with 1,669 additions and 1,209 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
script: ./gradlew connectedProdDebugAndroidTest
script: ./gradlew connectedDevDebugAndroidTest

sonar:
name: "Sonar Analysis"
Expand Down
10 changes: 5 additions & 5 deletions REPRODUCIBLE_BUILDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,31 @@ Download and install [Docker](https://www.docker.com/).

1. Open the SwissCovid app
2. Click on the `i` button in the top-right corner
3. Check the app version in the top right corner, which is the text between 'Version' and the comma (e.g., 1.0.1-pilot), and record its value to be used later
3. Check the app version in the top right corner, which is the text between 'Version' and the comma (e.g., 1.0.8), and record its value to be used later
4. Check the build timestamp in the bottom right corner, which is the number before the slash (e.g., 1591722151141), and record its value to be used later

## Download the App open-source code

1. Make sure you have `git` installed
2. Clone the Github repository
3. Checkout the Tag that corresponds to the version of your SwissCovid app (e.g., 1.0.1-pilot)
3. Checkout the Tag that corresponds to the version of your SwissCovid app (e.g., 1.0.8)

```shell
git clone https://github.com/DP-3T/dp3t-app-android-ch.git ~/dp3t-app-android-ch
cd ~/dp3t-app-android-ch
git checkout 1.0.1-pilot
git checkout 1.0.8
```

## Build the project using Docker

1. Build a Docker Image with the required Android Tools
2. Build the App in the Docker Container while specifying the build timestamp that was recorded earlier (e.g., 1591722151141)
2. Build the App in the Docker Container while specifying the build timestamp that was recorded earlier (e.g., 1595936711208)
3. Copy the freshly-built APK

```shell
cd ~/dp3t-app-android-ch
docker build -t swisscovid-builder .
docker run --rm -v ~/dp3t-app-android-ch:/home/swisscovid -w /home/swisscovid swisscovid-builder gradle assembleProdRelease -PkeystorePassword=securePassword -PkeyAliasPassword=securePassword -PkeystoreFile=build.keystore -PbuildTimestamp=1591722151141
docker run --rm -v ~/dp3t-app-android-ch:/home/swisscovid -w /home/swisscovid swisscovid-builder gradle assembleProdRelease -PkeystorePassword=securePassword -PkeyAliasPassword=securePassword -PkeystoreFile=build.keystore -PbuildTimestamp=1595936711208
cp app/build/outputs/apk/prod/release/app-prod-release.apk swisscovid-built.apk
```

Expand Down
9 changes: 6 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ android {
applicationId "ch.admin.bag.dp3t"
minSdkVersion 23
targetSdkVersion 29
versionCode 10080
versionName "1.0.8"
versionCode 10090
versionName "1.0.9"
resConfigs "en", "fr", "de", "it", "pt", "es", "sq", "bs", "hr", "sr", "rm", "tr", "ti"

buildConfigField "long", "BUILD_TIME", readPropertyWithDefault('buildTimestamp', System.currentTimeMillis()) + 'L'
Expand Down Expand Up @@ -138,7 +138,7 @@ sonarqube {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])

def dp3t_sdk_version = '1.0.3'
def dp3t_sdk_version = '1.0.4'
devImplementation "org.dpppt:dp3t-sdk-android:$dp3t_sdk_version-calibration"
teschtImplementation "org.dpppt:dp3t-sdk-android:$dp3t_sdk_version"
abnahmeImplementation "org.dpppt:dp3t-sdk-android:$dp3t_sdk_version"
Expand Down Expand Up @@ -167,4 +167,7 @@ dependencies {
androidTestImplementation 'androidx.test:core:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.work:work-testing:2.3.4'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.14.7'
androidTestImplementation "androidx.work:work-testing:2.3.4"

}
Binary file removed app/libs/play-services-nearby-18.0.2-eap-v1.3.1.aar
Binary file not shown.
Binary file added app/libs/play-services-nearby-18.0.3-eap.aar
Binary file not shown.
298 changes: 298 additions & 0 deletions app/src/androidTest/java/ch/admin/bag/dp3t/FakeWorkerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
package ch.admin.bag.dp3t;

import android.content.Context;
import android.util.Log;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.work.*;
import androidx.work.testing.SynchronousExecutor;
import androidx.work.testing.TestDriver;
import androidx.work.testing.WorkManagerTestInitHelper;

import java.io.IOException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import org.dpppt.android.sdk.DP3T;
import org.dpppt.android.sdk.internal.AppConfigManager;
import org.dpppt.android.sdk.internal.logger.LogLevel;
import org.dpppt.android.sdk.internal.logger.Logger;
import org.dpppt.android.sdk.models.ApplicationInfo;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import ch.admin.bag.dp3t.networking.FakeWorker;
import ch.admin.bag.dp3t.storage.SecureStorage;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

@RunWith(AndroidJUnit4.class)
public class FakeWorkerTest {

Context context;
MockWebServer server;
TestDriver testDriver;

@Before
public void setup() throws IOException {
context = InstrumentationRegistry.getInstrumentation().getTargetContext();
Logger.init(context, LogLevel.DEBUG);

// Initialize WorkManager for instrumentation tests.
Configuration config = new Configuration.Builder()
// Set log level to Log.DEBUG to make it easier to debug
.setMinimumLoggingLevel(Log.DEBUG)
.setExecutor(new SynchronousExecutor())
.build();
WorkManagerTestInitHelper.initializeTestWorkManager(context, config);

testDriver = WorkManagerTestInitHelper.getTestDriver(context);

server = new MockWebServer();
server.start();

AppConfigManager appConfigManager = AppConfigManager.getInstance(context);
DP3T.init(context, new ApplicationInfo("test", server.url("/bucket/").toString(), server.url("/report/").toString()),
null);
appConfigManager.setTracingEnabled(false);
DP3T.clearData(context);
DP3T.init(context, new ApplicationInfo("test", server.url("/bucket/").toString(), server.url("/report/").toString()),
null);
appConfigManager.setTracingEnabled(true);

SecureStorage.getInstance(context).setTDummy(-1);
}

@Test
public void testInitialTDummy() throws Exception {
List<WorkInfo> initialWorkList = WorkManager.getInstance(context).getWorkInfosByTag(FakeWorker.WORK_TAG).get();
assertEquals(0, initialWorkList.size());

FakeWorker.safeStartFakeWorker(context);
// TDummy is initialized to a time in the future.
assertTrue (SecureStorage.getInstance(context).getTDummy() > System.currentTimeMillis());
}

@Test
public void testCallingReportWhenScheduledIsNotPast() throws Exception {
List<WorkInfo> initialWorkList = WorkManager.getInstance(context).getWorkInfosByTag(FakeWorker.WORK_TAG).get();
assertEquals(0, initialWorkList.size());

long t_dummy = setTDummyToDaysFromNow(1);
AtomicInteger requestCounter = new AtomicInteger(0);

server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
requestCounter.getAndIncrement();
return new MockResponse().setResponseCode(200);
}
});

FakeWorker.safeStartFakeWorker(context);
WorkInfo workInfo = executeWorker();
long new_t_dummy = SecureStorage.getInstance(context).getTDummy();

// Worker succeeds by not executing a request. TDummy stays the same.
assertEquals(WorkInfo.State.SUCCEEDED, workInfo.getState());
assertEquals(0, requestCounter.get());
assertEquals(t_dummy, new_t_dummy);
}

@Test
public void testCallingReportWhenScheduledIsPast() throws Exception {
List<WorkInfo> initialWorkList = WorkManager.getInstance(context).getWorkInfosByTag(FakeWorker.WORK_TAG).get();
assertEquals(0, initialWorkList.size());

long t_dummy = setTDummyToDaysFromNow(-1);
AtomicInteger requestCounter = new AtomicInteger(0);

server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
requestCounter.getAndIncrement();
return new MockResponse().setResponseCode(200);
}
});

FakeWorker.safeStartFakeWorker(context);
WorkInfo workInfo = executeWorker();
long new_t_dummy = SecureStorage.getInstance(context).getTDummy();

// Worker succeeds by executing at least one request. The new_t_dummy must be greater than the old t_dummy.
assertEquals(WorkInfo.State.SUCCEEDED, workInfo.getState());
assertTrue(new_t_dummy > t_dummy);
assertTrue(requestCounter.get() >= 1);
}

@Test
public void testCallingReportWhenScheduledIsPastErrorResponse() throws Exception {
List<WorkInfo> initialWorkList = WorkManager.getInstance(context).getWorkInfosByTag(FakeWorker.WORK_TAG).get();
assertEquals(0, initialWorkList.size());

long t_dummy = setTDummyToDaysFromNow(-1);
AtomicInteger requestCounter = new AtomicInteger(0);

server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
requestCounter.getAndIncrement();
return new MockResponse().setResponseCode(503);
}
});

FakeWorker.safeStartFakeWorker(context);
WorkInfo workInfo = executeWorker();
long new_t_dummy = SecureStorage.getInstance(context).getTDummy();

// The request stays enqueued. T_dummy stays the same and exactly one network request is executed (and fails with Error
// code 503)
assertEquals(WorkInfo.State.ENQUEUED, workInfo.getState());
assertEquals(1, requestCounter.get());
assertEquals(new_t_dummy, t_dummy);
}

@Test
public void testCallingReportWhenScheduledIs2DaysPast() throws Exception {
List<WorkInfo> initialWorkList = WorkManager.getInstance(context).getWorkInfosByTag(FakeWorker.WORK_TAG).get();
assertEquals(0, initialWorkList.size());

long t_dummy = setTDummyToDaysFromNow(-2);

AtomicInteger requestCounter = new AtomicInteger(0);

server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
requestCounter.getAndIncrement();
return new MockResponse().setResponseCode(200);
}
});

FakeWorker.safeStartFakeWorker(context);
WorkInfo workInfo = executeWorker();
long new_t_dummy = SecureStorage.getInstance(context).getTDummy();

// The worker succeeds by dropping the request and creating and executing 0 or more new requests. The new_t_dummy must be
// greater the the old t_dummy.
assertTrue(new_t_dummy > t_dummy);
assertEquals(WorkInfo.State.SUCCEEDED, workInfo.getState());
}

@Test
public void testSyncInterval() {
int iterations = 10000;
long sum = 0;
for (int i = 0; i < iterations; i++) {
sum += FakeWorker.clock.syncInterval();
}
double averageIntervalDays = (double) (sum / iterations) / 1000 / 60 / 60 / 24;

double max = 1.1 / FakeWorker.SAMPLING_RATE;
double min = 0.9 / FakeWorker.SAMPLING_RATE;

assertTrue (averageIntervalDays < max);
assertTrue (averageIntervalDays > min);
}

@Test
public void testCallingReportMultipleDays() throws Exception {
List<WorkInfo> initialWorkList = WorkManager.getInstance(context).getWorkInfosByTag(FakeWorker.WORK_TAG).get();
assertEquals(0, initialWorkList.size());

AtomicInteger requestCounter = new AtomicInteger(0);

server.setDispatcher(new Dispatcher() {
@Override
public MockResponse dispatch(RecordedRequest request) {
requestCounter.getAndIncrement();
return new MockResponse().setResponseCode(200);
}
});

TestClockImpl clock = new TestClockImpl();

//Initial start -> No request should be executed because the syncInterval is set to 2 days in the future and therefore the
// first request is scheduled in two days.
clock.setNextSyncInterval(2);
FakeWorker.safeStartFakeWorker(context, clock);
executeWorker();
assertEquals(0, requestCounter.get());

//Set Time to two days in the future -> Request should be executed.
clock.setClockOffset(2);
executeWorker();
assertEquals(1, requestCounter.get());
requestCounter.set(0);

//At this time one request should be enqueued with execution scheduled in 4 days in the future (2 + 2)

// Set Time to six days in the future and the nextSyncInterval to 0.5 days -> The currently enqueued request should be
// dropped and 4 new requests should be executed, because they all fit within the time window of 48 hours.
clock.setNextSyncInterval(0.5);
clock.setClockOffset(6);
executeWorker();
assertEquals(4, requestCounter.get());
requestCounter.set(0);

//At this point one request is scheduled half a day after current time.

//Setting the Sync interval to 5
clock.setNextSyncInterval(5);

//Executing the worker for 100 consecutive days should have the consequence of 20 requests being made.
for (int i = 7; i < 107; i++) {
clock.setClockOffset(i);
executeWorker();
}
assertEquals(20, requestCounter.get());
}

private WorkInfo executeWorker() throws Exception {
List<WorkInfo> workInfoList = WorkManager.getInstance(context).getWorkInfosByTag(FakeWorker.WORK_TAG).get().stream()
.filter(job -> job.getState() == WorkInfo.State.ENQUEUED).collect(Collectors.toList());
assertEquals(1, workInfoList.size());
UUID requestID = workInfoList.get(0).getId();
testDriver.setInitialDelayMet(requestID);
testDriver.setAllConstraintsMet(requestID);
return WorkManager.getInstance(context).getWorkInfoById(requestID).get();
}


private class TestClockImpl implements FakeWorker.Clock {
private long clockOffset = 0;
private long nextSyncInterval = 0;

public long syncInterval() {
return nextSyncInterval;
}

public void setNextSyncInterval(double days) {
nextSyncInterval = (long) (days * 24 * 60 * 60 * 1000);
}

public void setClockOffset(double days) {
clockOffset = (long) (days * 24 * 60 * 60 * 1000);
}

public long currentTimeMillis() {
return System.currentTimeMillis() + clockOffset;
}

}

private long setTDummyToDaysFromNow(int daysFromNow) {
long t_dummy = System.currentTimeMillis() + daysFromNow * 24 * 60 * 60 * 1000;
SecureStorage.getInstance(context).setTDummy(t_dummy);
return t_dummy;
}

}
5 changes: 2 additions & 3 deletions app/src/dev/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2020 Ubique Innovation AG <https://www.ubique.ch>
~
~ This Source Code Form is subject to the terms of the Mozilla Public
Expand All @@ -10,7 +9,7 @@
-->

<network-security-config>
<base-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<!-- Trust user added CAs while debuggable only -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static int getTitle(NotificationStateError notificationStateError) {
public static int getText(NotificationStateError notificationStateError) {
switch (notificationStateError) {
case NOTIFICATION_STATE_ERROR:
return R.string.meldungen_background_error_text;
return R.string.meldungen_background_error_text_android;
case TRACING_DEACTIVATED:
return R.string.meldungen_tracing_turned_off_warning;
default:
Expand Down
Loading

0 comments on commit 286fb39

Please sign in to comment.