EN: Add latest API details, improve performance

This commit is contained in:
Marvin W 2020-09-27 11:33:02 +02:00
parent cab09cb238
commit 6afcca0396
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
42 changed files with 1602 additions and 216 deletions

View File

@ -1,6 +1,12 @@
package com.google.android.gms.common.api;
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
import com.google.android.gms.common.api.Status;
package com.google.android.gms.common.api;
import org.microg.gms.common.PublicApi;

View File

@ -1,3 +1,11 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.common.api;
import android.app.Activity;

View File

@ -23,6 +23,5 @@ android {
dependencies {
api project(':play-services-basement')
api project(':play-services-base-api')
}

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
parcelable DailySummary;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
parcelable DiagnosisKeysDataMapping;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification;
parcelable ExposureWindow;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable GetCalibrationConfidenceParams;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable GetDailySummariesParams;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable GetDiagnosisKeysDataMappingParams;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable GetExposureWindowsParams;

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable GetVersionParams;

View File

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.nearby.exposurenotification.DailySummary;
interface IDailySummaryListCallback {
void onResult(in Status status, in List<DailySummary> result);
}

View File

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping;
interface IDiagnosisKeysDataMappingCallback {
void onResult(in Status status, in DiagnosisKeysDataMapping result);
}

View File

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.nearby.exposurenotification.ExposureWindow;
interface IExposureWindowListCallback {
void onResult(in Status status, in List<ExposureWindow> result);
}

View File

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.Status;
interface IIntCallback {
void onResult(in Status status, int result);
}

View File

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.Status;
interface ILongCallback {
void onResult(in Status status, long result);
}

View File

@ -12,6 +12,12 @@ import com.google.android.gms.nearby.exposurenotification.internal.GetTemporaryE
import com.google.android.gms.nearby.exposurenotification.internal.ProvideDiagnosisKeysParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetExposureSummaryParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetExposureInformationParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetExposureWindowsParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetVersionParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetCalibrationConfidenceParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetDailySummariesParams;
import com.google.android.gms.nearby.exposurenotification.internal.SetDiagnosisKeysDataMappingParams;
import com.google.android.gms.nearby.exposurenotification.internal.GetDiagnosisKeysDataMappingParams;
interface INearbyExposureNotificationService{
void start(in StartParams params) = 0;
@ -22,4 +28,11 @@ interface INearbyExposureNotificationService{
void getExposureSummary(in GetExposureSummaryParams params) = 6;
void getExposureInformation(in GetExposureInformationParams params) = 7;
void getExposureWindows(in GetExposureWindowsParams params) = 12;
void getVersion(in GetVersionParams params) = 13;
void getCalibrationConfidence(in GetCalibrationConfidenceParams params) = 14;
void getDailySummaries(in GetDailySummariesParams params) = 15;
void setDiagnosisKeysDataMapping(in SetDiagnosisKeysDataMappingParams params) = 16;
void getDiagnosisKeysDataMapping(in GetDiagnosisKeysDataMappingParams params) = 17;
}

View File

@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
parcelable SetDiagnosisKeysDataMappingParams;

View File

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.gms.common.PublicApi;
/**
* Calibration confidence defined for an {@link ExposureWindow}.
*/
@PublicApi
public @interface CalibrationConfidence {
/**
* No calibration data, using fleet-wide as default options.
*/
int LOWEST = 0;
/**
* Using average calibration over models from manufacturer.
*/
int LOW = 1;
/**
* Using single-antenna orientation for a similar model.
*/
int MEDIUM = 2;
/**
* Using significant calibration data for this model.
*/
int HIGH = 3;
@PublicApi(exclude = true)
int VALUES = 4;
}

View File

@ -0,0 +1,244 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.gms.common.PublicApi;
import org.microg.safeparcel.AutoSafeParcelable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Configuration of per-day summary of exposures.
* <p>
* During summarization the following are computed for each ExposureWindows:
* <ul>
* <li>a weighted duration, computed as
* {@code ( immediateDurationSeconds * immediateDurationWeight ) + ( nearDurationSeconds * nearDurationWeight ) + ( mediumDurationSeconds * mediumDurationWeight ) + ( otherDurationSeconds * otherDurationWeight )}</li>
* <li>a score, computed as
* {@code reportTypeWeights[Tek.reportType] * infectiousnessWeights[infectiousness] * weightedDuration}
* where infectiousness and reportType are set based on the ExposureWindow's diagnosis key and the DiagnosisKeysDataMapping</li>
* </ul>
* <p>
* The {@link ExposureWindow}s are then filtered, removing those with score lower than {@link #getMinimumWindowScore()}.
* <p>
* Scores and weighted durations of the {@link ExposureWindow}s that pass the {@link #getMinimumWindowScore()} are then aggregated over a day to compute the maximum and cumulative scores and duration:
* <ul>
* <li>sumScore = sum(score of ExposureWindows)</li>
* <li>maxScore = max(score of ExposureWindows)</li>
* <li>weightedDurationSum = sum(weighted duration of ExposureWindow)</li>
* </ul>
* Note that when the weights are typically around 100% (1.0), both the scores and the weightedDurationSum can be considered as being expressed in seconds. For example, 15 minutes of exposure with all weights equal to 1.0 would be 60 * 15 = 900 (seconds).
*/
@PublicApi
public class DailySummariesConfig extends AutoSafeParcelable {
@Field(1)
private List<Double> reportTypeWeights;
@Field(2)
private List<Double> infectiousnessWeights;
@Field(3)
private List<Integer> attenuationBucketThresholdDb;
@Field(4)
private List<Double> attenuationBucketWeights;
@Field(5)
private int daysSinceExposureThreshold;
@Field(6)
private double minimumWindowScore;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof DailySummariesConfig)) return false;
DailySummariesConfig that = (DailySummariesConfig) o;
if (daysSinceExposureThreshold != that.daysSinceExposureThreshold) return false;
if (Double.compare(that.minimumWindowScore, minimumWindowScore) != 0) return false;
if (reportTypeWeights != null ? !reportTypeWeights.equals(that.reportTypeWeights) : that.reportTypeWeights != null)
return false;
if (infectiousnessWeights != null ? !infectiousnessWeights.equals(that.infectiousnessWeights) : that.infectiousnessWeights != null)
return false;
if (attenuationBucketThresholdDb != null ? !attenuationBucketThresholdDb.equals(that.attenuationBucketThresholdDb) : that.attenuationBucketThresholdDb != null)
return false;
return attenuationBucketWeights != null ? attenuationBucketWeights.equals(that.attenuationBucketWeights) : that.attenuationBucketWeights == null;
}
/**
* Thresholds defining the BLE attenuation buckets edges.
* <p>
* This list must have 3 elements: the immediate, near, and medium thresholds. See attenuationBucketWeights for more information.
* <p>
* These elements must be between 0 and 255 and come in increasing order.
*/
public List<Integer> getAttenuationBucketThresholdDb() {
return attenuationBucketThresholdDb;
}
/**
* Scoring weights to associate with ScanInstances depending on the attenuation bucket in which their typicalAttenuationDb falls.
* <p>
* This list must have 4 elements, corresponding to the weights for the 4 buckets.
* <ul>
* <li>immediate bucket: -infinity < attenuation <= immediate threshold</li>
* <li>near bucket: immediate threshold < attenuation <= near threshold</li>
* <li>medium bucket: near threshold < attenuation <= medium threshold</li>
* <li>other bucket: medium threshold < attenuation < +infinity</li>
* </ul>
* Each element must be between 0 and 2.5.
*/
public List<Double> getAttenuationBucketWeights() {
return attenuationBucketWeights;
}
/**
* Reserved for future use, behavior will be changed in future revisions. No value should be set, or else 0 should be used.
*/
public int getDaysSinceExposureThreshold() {
return daysSinceExposureThreshold;
}
/**
* Scoring weights to associate with exposures with different Infectiousness.
* <p>
* This map can include weights for the following Infectiousness values:
* <ul>
* <li>STANDARD</li>
* <li>HIGH</li>
* </ul>
* Each element must be between 0 and 2.5.
*/
public Map<Integer, Double> getInfectiousnessWeights() {
HashMap<Integer, Double> map = new HashMap<>();
for (int i = 0; i < infectiousnessWeights.size(); i++) {
map.put(i, infectiousnessWeights.get(i));
}
return map;
}
/**
* Minimum score that {@link ExposureWindow}s must reach in order to be included in the {@link DailySummary.ExposureSummaryData}.
* <p>
* Use 0 to consider all {@link ExposureWindow}s (recommended).
*/
public double getMinimumWindowScore() {
return minimumWindowScore;
}
/**
* Scoring weights to associate with exposures with different ReportTypes.
* <p>
* This map can include weights for the following ReportTypes:
* <ul>
* <li>CONFIRMED_TEST</li>
* <li>CONFIRMED_CLINICAL_DIAGNOSIS</li>
* <li>SELF_REPORT</li>
* <li>RECURSIVE (reserved for future use)</li>
* </ul>
* Each element must be between 0 and 2.5.
*/
public Map<Integer, Double> getReportTypeWeights() {
HashMap<Integer, Double> map = new HashMap<>();
for (int i = 0; i < reportTypeWeights.size(); i++) {
map.put(i, reportTypeWeights.get(i));
}
return map;
}
@Override
public int hashCode() {
int result;
long temp;
result = reportTypeWeights != null ? reportTypeWeights.hashCode() : 0;
result = 31 * result + (infectiousnessWeights != null ? infectiousnessWeights.hashCode() : 0);
result = 31 * result + (attenuationBucketThresholdDb != null ? attenuationBucketThresholdDb.hashCode() : 0);
result = 31 * result + (attenuationBucketWeights != null ? attenuationBucketWeights.hashCode() : 0);
result = 31 * result + daysSinceExposureThreshold;
temp = Double.doubleToLongBits(minimumWindowScore);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
/**
* A builder for {@link DailySummariesConfig}.
*/
public static class DailySummariesConfigBuilder {
private Double[] reportTypeWeights = new Double[ReportType.VALUES];
private Double[] infectiousnessWeights = new Double[Infectiousness.VALUES];
private List<Integer> attenuationBucketThresholdDb;
private List<Double> attenuationBucketWeights;
private int daysSinceExposureThreshold;
private double minimumWindowScore;
public DailySummariesConfigBuilder() {
Arrays.fill(reportTypeWeights, 0.0);
Arrays.fill(infectiousnessWeights, 0.0);
}
public DailySummariesConfig build() {
if (attenuationBucketThresholdDb == null)
throw new IllegalStateException("Must set attenuationBucketThresholdDb");
if (attenuationBucketWeights == null)
throw new IllegalStateException("Must set attenuationBucketWeights");
DailySummariesConfig config = new DailySummariesConfig();
config.reportTypeWeights = Arrays.asList(reportTypeWeights);
config.infectiousnessWeights = Arrays.asList(infectiousnessWeights);
config.attenuationBucketThresholdDb = attenuationBucketThresholdDb;
config.attenuationBucketWeights = attenuationBucketWeights;
config.daysSinceExposureThreshold = daysSinceExposureThreshold;
config.minimumWindowScore = minimumWindowScore;
return config;
}
/**
* See {@link #getAttenuationBucketThresholdDb()} and {@link #getAttenuationBucketWeights()}
*/
public DailySummariesConfigBuilder setAttenuationBuckets(List<Integer> thresholds, List<Double> weights) {
attenuationBucketThresholdDb = new ArrayList<>(thresholds);
attenuationBucketWeights = new ArrayList<>(weights);
return this;
}
/**
* See {@link #getDaysSinceExposureThreshold()}
*/
public DailySummariesConfigBuilder setDaysSinceExposureThreshold(int daysSinceExposureThreshold) {
this.daysSinceExposureThreshold = daysSinceExposureThreshold;
return this;
}
/**
* See {@link #getInfectiousnessWeights()}
*/
public DailySummariesConfigBuilder setInfectiousnessWeight(@Infectiousness int infectiousness, double weight) {
infectiousnessWeights[infectiousness] = weight;
return this;
}
/**
* See {@link #getMinimumWindowScore()}
*/
public DailySummariesConfigBuilder setMinimumWindowScore(double minimumWindowScore) {
this.minimumWindowScore = minimumWindowScore;
return this;
}
/**
* See {@link #getReportTypeWeights()}
*/
public DailySummariesConfigBuilder setReportTypeWeight(@ReportType int reportType, double weight) {
reportTypeWeights[reportType] = weight;
return this;
}
}
public static final Creator<DailySummariesConfig> CREATOR = new AutoCreator<>(DailySummariesConfig.class);
}

View File

@ -0,0 +1,158 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.gms.common.PublicApi;
import org.microg.safeparcel.AutoSafeParcelable;
import java.util.List;
/**
* Daily exposure summary to pass to client side.
*/
@PublicApi
public class DailySummary extends AutoSafeParcelable {
@Field(1)
private int daysSinceEpoch;
@Field(2)
private List<ExposureSummaryData> reportSummaries;
@Field(3)
private ExposureSummaryData summaryData;
private DailySummary() {
}
@PublicApi(exclude = true)
public DailySummary(int daysSinceEpoch, List<ExposureSummaryData> reportSummaries, ExposureSummaryData summaryData) {
this.daysSinceEpoch = daysSinceEpoch;
this.reportSummaries = reportSummaries;
this.summaryData = summaryData;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof DailySummary)) return false;
DailySummary that = (DailySummary) o;
if (daysSinceEpoch != that.daysSinceEpoch) return false;
if (reportSummaries != null ? !reportSummaries.equals(that.reportSummaries) : that.reportSummaries != null)
return false;
return summaryData != null ? summaryData.equals(that.summaryData) : that.summaryData == null;
}
/**
* Returns days since epoch of the {@link ExposureWindow}s that went into this summary.
*/
public int getDaysSinceEpoch() {
return daysSinceEpoch;
}
/**
* Summary of all exposures on this day.
*/
public ExposureSummaryData getSummaryData() {
return summaryData;
}
/**
* Summary of all exposures on this day of a specific diagnosis {@link ReportType}.
*/
public ExposureSummaryData getSummaryDataForReportType(@ReportType int reportType) {
return reportSummaries.get(reportType);
}
@Override
public int hashCode() {
int result = daysSinceEpoch;
result = 31 * result + (reportSummaries != null ? reportSummaries.hashCode() : 0);
result = 31 * result + (summaryData != null ? summaryData.hashCode() : 0);
return result;
}
/**
* Stores different scores for specific {@link ReportType}.
*/
public static class ExposureSummaryData extends AutoSafeParcelable {
@Field(1)
private double maximumScore;
@Field(2)
private double scoreSum;
@Field(3)
private double weightedDurationSum;
private ExposureSummaryData() {
}
@PublicApi(exclude = true)
public ExposureSummaryData(double maximumScore, double scoreSum, double weightedDurationSum) {
this.maximumScore = maximumScore;
this.scoreSum = scoreSum;
this.weightedDurationSum = weightedDurationSum;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ExposureSummaryData)) return false;
ExposureSummaryData that = (ExposureSummaryData) o;
if (Double.compare(that.maximumScore, maximumScore) != 0) return false;
if (Double.compare(that.scoreSum, scoreSum) != 0) return false;
return Double.compare(that.weightedDurationSum, weightedDurationSum) == 0;
}
/**
* Highest score of all {@link ExposureWindow}s aggregated into this summary.
* <p>
* See {@link DailySummariesConfig} for more information about how the per-{@link ExposureWindow} score is computed.
*/
public double getMaximumScore() {
return maximumScore;
}
/**
* Sum of scores for all {@link ExposureWindow}s aggregated into this summary.
* <p>
* See {@link DailySummariesConfig} for more information about how the per-{@link ExposureWindow} score is computed.
*/
public double getScoreSum() {
return scoreSum;
}
/**
* Sum of weighted durations for all {@link ExposureWindow}s aggregated into this summary.
* <p>
* See {@link DailySummariesConfig} for more information about how the per-{@link ExposureWindow} score is computed.
*/
public double getWeightedDurationSum() {
return weightedDurationSum;
}
@Override
public int hashCode() {
int result;
long temp;
temp = Double.doubleToLongBits(maximumScore);
result = (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(scoreSum);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(weightedDurationSum);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
public static final Creator<ExposureSummaryData> CREATOR = new AutoCreator<>(ExposureSummaryData.class);
}
public static final Creator<DailySummary> CREATOR = new AutoCreator<>(DailySummary.class);
}

View File

@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.gms.common.PublicApi;
import org.microg.safeparcel.AutoSafeParcelable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Mappings from diagnosis keys data to concepts returned by the API.
*/
@PublicApi
public class DiagnosisKeysDataMapping extends AutoSafeParcelable {
@Field(1)
private List<Integer> daysSinceOnsetToInfectiousness;
@Field(2)
@ReportType
private int reportTypeWhenMissing;
@Field(3)
@Infectiousness
private int infectiousnessWhenDaysSinceOnsetMissing;
/**
* Mapping from diagnosisKey.daysSinceOnsetOfSymptoms to {@link Infectiousness}.
* <p>
* Infectiousness is computed from this mapping and the tek metadata as - daysSinceOnsetToInfectiousness[{@link TemporaryExposureKey#getDaysSinceOnsetOfSymptoms()}], or - {@link #getInfectiousnessWhenDaysSinceOnsetMissing()} if {@link TemporaryExposureKey#getDaysSinceOnsetOfSymptoms()} is {@link TemporaryExposureKey#DAYS_SINCE_ONSET_OF_SYMPTOMS_UNKNOWN}.
* <p>
* Values of DaysSinceOnsetOfSymptoms that aren't represented in this map are given {@link Infectiousness#NONE} as infectiousness. Exposures with infectiousness equal to {@link Infectiousness#NONE} are dropped.
*/
public Map<Integer, Integer> getDaysSinceOnsetToInfectiousness() {
HashMap<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < daysSinceOnsetToInfectiousness.size(); i++) {
map.put(i, daysSinceOnsetToInfectiousness.get(i));
}
return map;
}
/**
* Infectiousness of TEKs for which onset of symptoms is not set.
* <p>
* See {@link #getDaysSinceOnsetToInfectiousness()} for more info.
*/
public int getInfectiousnessWhenDaysSinceOnsetMissing() {
return infectiousnessWhenDaysSinceOnsetMissing;
}
/**
* Report type to default to when a TEK has no report type set.
* <p>
* This report type gets used when creating the {@link ExposureWindow}s and the {@link DailySummary}s. The system will treat TEKs with missing report types as if they had this provided report type.
*/
public int getReportTypeWhenMissing() {
return reportTypeWhenMissing;
}
/**
* A builder for {@link DiagnosisKeysDataMapping}.
*/
public static class DiagnosisKeysDataMappingBuilder {
private final static int MAX_DAYS = 29;
private List<Integer> daysSinceOnsetToInfectiousness;
@ReportType
private int reportTypeWhenMissing = ReportType.UNKNOWN;
@Infectiousness
private Integer infectiousnessWhenDaysSinceOnsetMissing;
public DiagnosisKeysDataMapping build() {
if (daysSinceOnsetToInfectiousness == null)
throw new IllegalStateException("Must set daysSinceOnsetToInfectiousness");
if (reportTypeWhenMissing == ReportType.UNKNOWN)
throw new IllegalStateException("Must set reportTypeWhenMissing");
if (infectiousnessWhenDaysSinceOnsetMissing == null)
throw new IllegalStateException("Must set infectiousnessWhenDaysSinceOnsetMissing");
DiagnosisKeysDataMapping mapping = new DiagnosisKeysDataMapping();
mapping.daysSinceOnsetToInfectiousness = daysSinceOnsetToInfectiousness;
mapping.reportTypeWhenMissing = reportTypeWhenMissing;
mapping.infectiousnessWhenDaysSinceOnsetMissing = infectiousnessWhenDaysSinceOnsetMissing;
return mapping;
}
public DiagnosisKeysDataMappingBuilder setDaysSinceOnsetToInfectiousness(Map<Integer, Integer> daysSinceOnsetToInfectiousness) {
if (daysSinceOnsetToInfectiousness.size() > MAX_DAYS)
throw new IllegalArgumentException("daysSinceOnsetToInfectiousness exceeds " + MAX_DAYS + " days");
Integer[] values = new Integer[MAX_DAYS];
Arrays.fill(values, 0);
for (Map.Entry<Integer, Integer> entry : daysSinceOnsetToInfectiousness.entrySet()) {
if (entry.getKey() > 14) throw new IllegalArgumentException("invalid day since onset");
values[entry.getKey() + 14] = entry.getValue();
}
this.daysSinceOnsetToInfectiousness = Arrays.asList(values);
return this;
}
public DiagnosisKeysDataMappingBuilder setInfectiousnessWhenDaysSinceOnsetMissing(@Infectiousness int infectiousnessWhenDaysSinceOnsetMissing) {
this.infectiousnessWhenDaysSinceOnsetMissing = infectiousnessWhenDaysSinceOnsetMissing;
return this;
}
public DiagnosisKeysDataMappingBuilder setReportTypeWhenMissing(@ReportType int reportTypeWhenMissing) {
this.reportTypeWhenMissing = reportTypeWhenMissing;
return this;
}
}
public static final Creator<DiagnosisKeysDataMapping> CREATOR = new AutoCreator<>(DiagnosisKeysDataMapping.class);
}

View File

@ -0,0 +1,165 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.gms.common.PublicApi;
import org.microg.safeparcel.AutoSafeParcelable;
import java.util.ArrayList;
import java.util.List;
/**
* A duration of up to 30 minutes during which beacons from a TEK were observed.
* <p>
* Each {@link ExposureWindow} corresponds to a single TEK, but one TEK can lead to several {@link ExposureWindow} due to random 15-30 minutes cuts. See {@link ExposureNotificationClient#getExposureWindows()} for more info.
* <p>
* The TEK itself isn't exposed by the API.
*/
@PublicApi
public class ExposureWindow extends AutoSafeParcelable {
@Field(1)
private long dateMillisSinceEpoch;
@Field(2)
private List<ScanInstance> scanInstances;
@Field(3)
@ReportType
private int reportType;
@Field(4)
@Infectiousness
private int infectiousness;
@Field(5)
@CalibrationConfidence
private int calibrationConfidence;
private ExposureWindow() {
}
private ExposureWindow(long dateMillisSinceEpoch, List<ScanInstance> scanInstances, int reportType, int infectiousness, int calibrationConfidence) {
this.dateMillisSinceEpoch = dateMillisSinceEpoch;
this.scanInstances = scanInstances;
this.reportType = reportType;
this.infectiousness = infectiousness;
this.calibrationConfidence = calibrationConfidence;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ExposureWindow)) return false;
ExposureWindow that = (ExposureWindow) o;
if (dateMillisSinceEpoch != that.dateMillisSinceEpoch) return false;
if (reportType != that.reportType) return false;
if (infectiousness != that.infectiousness) return false;
if (calibrationConfidence != that.calibrationConfidence) return false;
return scanInstances != null ? scanInstances.equals(that.scanInstances) : that.scanInstances == null;
}
/**
* Confidence of the BLE Transmit power calibration of the transmitting device.
*/
@CalibrationConfidence
public int getCalibrationConfidence() {
return calibrationConfidence;
}
/**
* Returns the epoch time in milliseconds the exposure occurred. This will represent the start of a day in UTC.
*/
public long getDateMillisSinceEpoch() {
return dateMillisSinceEpoch;
}
/**
* Infectiousness of the TEK that caused this exposure, computed from the days since onset of symptoms using the daysToInfectiousnessMapping.
*/
@Infectiousness
public int getInfectiousness() {
return infectiousness;
}
/**
* Report Type of the TEK that caused this exposure
* <p>
* TEKs with no report type set are returned with reportType=CONFIRMED_TEST.
* <p>
* TEKs with RECURSIVE report type may be dropped because this report type is reserved for future use.
* <p>
* TEKs with REVOKED or invalid report types do not lead to exposures.
*/
@ReportType
public int getReportType() {
return reportType;
}
/**
* Sightings of this ExposureWindow, time-ordered.
* <p>
* Each sighting corresponds to a scan (of a few seconds) during which a beacon with the TEK causing this exposure was observed.
*/
public List<ScanInstance> getScanInstances() {
return scanInstances;
}
@Override
public int hashCode() {
int result = (int) (dateMillisSinceEpoch ^ (dateMillisSinceEpoch >>> 32));
result = 31 * result + (scanInstances != null ? scanInstances.hashCode() : 0);
result = 31 * result + reportType;
result = 31 * result + infectiousness;
result = 31 * result + calibrationConfidence;
return result;
}
/**
* Builder for ExposureWindow.
*/
public static class Builder {
private long dateMillisSinceEpoch;
private List<ScanInstance> scanInstances;
@ReportType
private int reportType;
@Infectiousness
private int infectiousness;
@CalibrationConfidence
private int calibrationConfidence;
public ExposureWindow build() {
return new ExposureWindow(dateMillisSinceEpoch, scanInstances, reportType, infectiousness, calibrationConfidence);
}
public Builder setCalibrationConfidence(int calibrationConfidence) {
this.calibrationConfidence = calibrationConfidence;
return this;
}
public Builder setDateMillisSinceEpoch(long dateMillisSinceEpoch) {
this.dateMillisSinceEpoch = dateMillisSinceEpoch;
return this;
}
public Builder setInfectiousness(@Infectiousness int infectiousness) {
this.infectiousness = infectiousness;
return this;
}
public Builder setReportType(@ReportType int reportType) {
this.reportType = reportType;
return this;
}
public Builder setScanInstances(List<ScanInstance> scanInstances) {
this.scanInstances = new ArrayList<>(scanInstances);
return this;
}
}
public static final Creator<ExposureWindow> CREATOR = new AutoCreator<>(ExposureWindow.class);
}

View File

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.gms.common.PublicApi;
/**
* Infectiousness defined for an {@link ExposureWindow}.
*/
@PublicApi
public @interface Infectiousness {
int NONE = 0;
int STANDARD = 1;
int HIGH = 2;
@PublicApi(exclude = true)
int VALUES = 3;
}

View File

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.gms.common.PublicApi;
/**
* Report type defined for a {@link TemporaryExposureKey}.
*/
@PublicApi
public @interface ReportType {
int UNKNOWN = 0;
int CONFIRMED_TEST = 1;
int CONFIRMED_CLINICAL_DIAGNOSIS = 2;
int SELF_REPORT = 3;
int RECURSIVE = 4;
int REVOKED = 5;
@PublicApi(exclude = true)
int VALUES = 6;
}

View File

@ -8,6 +8,12 @@
package com.google.android.gms.nearby.exposurenotification;
import org.microg.gms.common.PublicApi;
/**
* Risk level defined for an {@link TemporaryExposureKey}.
*/
@PublicApi
public @interface RiskLevel {
int RISK_LEVEL_INVALID = 0;
int RISK_LEVEL_LOWEST = 1;
@ -18,4 +24,7 @@ public @interface RiskLevel {
int RISK_LEVEL_HIGH = 6;
int RISK_LEVEL_VERY_HIGH = 7;
int RISK_LEVEL_HIGHEST = 8;
@PublicApi(exclude = true)
int VALUES = 9;
}

View File

@ -0,0 +1,93 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
* Notice: Portions of this file are reproduced from work created and shared by Google and used
* according to terms described in the Creative Commons 4.0 Attribution License.
* See https://developers.google.com/readme/policies for details.
*/
package com.google.android.gms.nearby.exposurenotification;
import org.microg.gms.common.PublicApi;
import org.microg.safeparcel.AutoSafeParcelable;
/**
* Information about the sighting of a TEK within a BLE scan (of a few seconds).
* <p>
* The TEK itself isn't exposed by the API.
*/
@PublicApi
public class ScanInstance extends AutoSafeParcelable {
@Field(1)
private int typicalAttenuationDb;
@Field(2)
private int minAttenuationDb;
@Field(3)
private int secondsSinceLastScan;
private ScanInstance() {
}
private ScanInstance(int typicalAttenuationDb, int minAttenuationDb, int secondsSinceLastScan) {
this.typicalAttenuationDb = typicalAttenuationDb;
this.minAttenuationDb = minAttenuationDb;
this.secondsSinceLastScan = secondsSinceLastScan;
}
/**
* Minimum attenuation of all of this TEK's beacons received during the scan, in dB.
*/
public int getMinAttenuationDb() {
return minAttenuationDb;
}
/**
* Seconds elapsed since the previous scan, typically used as a weight.
* <p>
* Two example uses:
* - Summing those values over all sightings of an exposure provides the duration of that exposure.
* - Summing those values over all sightings in a given attenuation range and over all exposures recreates the durationAtBuckets of v1.
* <p>
* Note that the previous scan may not have led to a sighting of that TEK.
*/
public int getSecondsSinceLastScan() {
return secondsSinceLastScan;
}
/**
* Aggregation of the attenuations of all of this TEK's beacons received during the scan, in dB. This is most likely to be an average in the dB domain.
*/
public int getTypicalAttenuationDb() {
return typicalAttenuationDb;
}
/**
* Builder for {@link ScanInstance}.
*/
public static class Builder {
private int typicalAttenuationDb;
private int minAttenuationDb;
private int secondsSinceLastScan;
public ScanInstance build() {
return new ScanInstance(typicalAttenuationDb, minAttenuationDb, secondsSinceLastScan);
}
public ScanInstance.Builder setMinAttenuationDb(int minAttenuationDb) {
this.minAttenuationDb = minAttenuationDb;
return this;
}
public ScanInstance.Builder setSecondsSinceLastScan(int secondsSinceLastScan) {
this.secondsSinceLastScan = secondsSinceLastScan;
return this;
}
public ScanInstance.Builder setTypicalAttenuationDb(int typicalAttenuationDb) {
this.typicalAttenuationDb = typicalAttenuationDb;
return this;
}
}
public static final Creator<ScanInstance> CREATOR = new AutoCreator<>(ScanInstance.class);
}

View File

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import org.microg.safeparcel.AutoSafeParcelable;
public class GetCalibrationConfidenceParams extends AutoSafeParcelable {
@Field(1)
public IIntCallback callback;
private GetCalibrationConfidenceParams() {}
public GetCalibrationConfidenceParams(IIntCallback callback) {
this.callback = callback;
}
public static final Creator<GetCalibrationConfidenceParams> CREATOR = new AutoCreator<>(GetCalibrationConfidenceParams.class);
}

View File

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.nearby.exposurenotification.DailySummariesConfig;
import org.microg.safeparcel.AutoSafeParcelable;
public class GetDailySummariesParams extends AutoSafeParcelable {
@Field(1)
public IDailySummaryListCallback callback;
@Field(2)
public DailySummariesConfig config;
private GetDailySummariesParams() {}
public GetDailySummariesParams(IDailySummaryListCallback callback, DailySummariesConfig config) {
this.callback = callback;
this.config = config;
}
public static final Creator<GetDailySummariesParams> CREATOR = new AutoCreator<>(GetDailySummariesParams.class);
}

View File

@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import org.microg.safeparcel.AutoSafeParcelable;
public class GetDiagnosisKeysDataMappingParams extends AutoSafeParcelable {
@Field(1)
public IDiagnosisKeysDataMappingCallback callback;
private GetDiagnosisKeysDataMappingParams() {}
public GetDiagnosisKeysDataMappingParams(IDiagnosisKeysDataMappingCallback callback) {
this.callback = callback;
}
public static final Creator<GetDiagnosisKeysDataMappingParams> CREATOR = new AutoCreator<>(GetDiagnosisKeysDataMappingParams.class);
}

View File

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import org.microg.safeparcel.AutoSafeParcelable;
public class GetExposureWindowsParams extends AutoSafeParcelable {
@Field(1)
public IExposureWindowListCallback callback;
@Field(2)
public String token;
private GetExposureWindowsParams() {}
public GetExposureWindowsParams(IExposureWindowListCallback callback, String token) {
this.callback = callback;
this.token = token;
}
public static final Creator<GetExposureWindowsParams> CREATOR = new AutoCreator<>(GetExposureWindowsParams.class);
}

View File

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import org.microg.safeparcel.AutoSafeParcelable;
public class GetVersionParams extends AutoSafeParcelable {
@Field(1)
public ILongCallback callback;
private GetVersionParams() {
}
public GetVersionParams(ILongCallback callback) {
this.callback = callback;
}
public static final Creator<GetVersionParams> CREATOR = new AutoCreator<>(GetVersionParams.class);
}

View File

@ -27,6 +27,9 @@ public class ProvideDiagnosisKeysParams extends AutoSafeParcelable {
@Field(5)
public String token;
private ProvideDiagnosisKeysParams() {
}
public ProvideDiagnosisKeysParams(IStatusCallback callback, List<TemporaryExposureKey> keys, List<ParcelFileDescriptor> keyFiles, ExposureConfiguration configuration, String token) {
this(callback, keyFiles, configuration, token);
this.keys = keys;

View File

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.nearby.exposurenotification.internal;
import com.google.android.gms.common.api.internal.IStatusCallback;
import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping;
import org.microg.safeparcel.AutoSafeParcelable;
public class SetDiagnosisKeysDataMappingParams extends AutoSafeParcelable {
@Field(1)
public IStatusCallback callback;
@Field(2)
public DiagnosisKeysDataMapping mapping;
private SetDiagnosisKeysDataMappingParams() {}
public SetDiagnosisKeysDataMappingParams(IStatusCallback callback, DiagnosisKeysDataMapping mapping) {
this.callback = callback;
this.mapping = mapping;
}
public static final Creator<SetDiagnosisKeysDataMappingParams> CREATOR = new AutoCreator<>(SetDiagnosisKeysDataMappingParams.class);
}

View File

@ -48,7 +48,7 @@ class AdvertiserService : Service() {
}
@TargetApi(23)
private var setCallback: AdvertisingSetCallback? = null
private var setCallback: Any? = null
private val trigger = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "android.bluetooth.adapter.action.STATE_CHANGED") {
@ -83,6 +83,7 @@ class AdvertiserService : Service() {
super.onDestroy()
unregisterReceiver(trigger)
stopOrRestartAdvertising()
handler.removeCallbacks(startLaterRunnable)
database.unref()
}
@ -114,7 +115,7 @@ class AdvertiserService : Service() {
0x00 // Reserved
)
VERSION_1_1 -> byteArrayOf(
(version + currentDeviceInfo.confidence * 4).toByte(), // Version and flags
(version + currentDeviceInfo.confidence.toByte() * 4).toByte(), // Version and flags
(currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
0x00, // Reserved
0x00 // Reserved
@ -134,7 +135,7 @@ class AdvertiserService : Service() {
.setTxPowerLevel(AdvertisingSetParameters.TX_POWER_LOW)
.setConnectable(false)
.build()
advertiser.startAdvertisingSet(params, data, null, null, null, setCallback)
advertiser.startAdvertisingSet(params, data, null, null, null, setCallback as AdvertisingSetCallback)
} else {
nextSend = nextSend.coerceAtMost(180000)
val settings = Builder()
@ -189,21 +190,13 @@ class AdvertiserService : Service() {
advertising = false
if (Build.VERSION.SDK_INT >= 26) {
wantStartAdvertising = true
advertiser?.stopAdvertisingSet(setCallback)
advertiser?.stopAdvertisingSet(setCallback as AdvertisingSetCallback)
} else {
advertiser?.stopAdvertising(callback)
}
handler.postDelayed(startLaterRunnable, 1000)
}
companion object {
private const val ACTION_RESTART_ADVERTISING = "org.microg.gms.nearby.exposurenotification.RESTART_ADVERTISING"
fun isNeeded(context: Context): Boolean {
return ExposurePreferences(context).enabled
}
}
@TargetApi(26)
inner class SetCallback : AdvertisingSetCallback() {
override fun onAdvertisingSetStarted(advertisingSet: AdvertisingSet?, txPower: Int, status: Int) {
@ -219,4 +212,13 @@ class AdvertiserService : Service() {
}
}
}
companion object {
private const val ACTION_RESTART_ADVERTISING = "org.microg.gms.nearby.exposurenotification.RESTART_ADVERTISING"
fun isNeeded(context: Context): Boolean {
return ExposurePreferences(context).enabled
}
}
}

View File

@ -5,6 +5,8 @@
package org.microg.gms.nearby.exposurenotification
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.lifecycle.LifecycleService
@ -25,14 +27,21 @@ class CleanupService : LifecycleService() {
}
ExposurePreferences(this@CleanupService).lastCleanup = System.currentTimeMillis()
}
stopSelf()
stop()
}
} else {
stopSelf()
stop()
}
return START_NOT_STICKY
}
fun stop() {
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val pendingIntent = PendingIntent.getService(applicationContext, CleanupService::class.java.name.hashCode(), Intent(applicationContext, CleanupService::class.java), PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT)
alarmManager.set(AlarmManager.RTC, ExposurePreferences(this).lastCleanup + CLEANUP_INTERVAL, pendingIntent)
stopSelf()
}
companion object {
fun isNeeded(context: Context): Boolean {
return ExposurePreferences(context).let {

View File

@ -6,6 +6,7 @@
package org.microg.gms.nearby.exposurenotification
import android.os.ParcelUuid
import com.google.android.gms.nearby.exposurenotification.CalibrationConfidence
import java.util.*
const val TAG = "ExposureNotification"
@ -17,10 +18,10 @@ const val SCANNING_TIME = 20 // Google uses 4s + 13s (if Bluetooth is used by so
const val SCANNING_TIME_MS = SCANNING_TIME * 1000L
const val ROLLING_WINDOW_LENGTH = 10 * 60
const val ROLLING_WINDOW_LENGTH_MS = ROLLING_WINDOW_LENGTH * 1000
const val ROLLING_WINDOW_LENGTH_MS = ROLLING_WINDOW_LENGTH * 1000L
const val ROLLING_PERIOD = 144
const val ALLOWED_KEY_OFFSET_MS = 60 * 60 * 1000
const val MINIMUM_EXPOSURE_DURATION_MS = 0
const val ALLOWED_KEY_OFFSET_MS = 60 * 60 * 1000L
const val MINIMUM_EXPOSURE_DURATION_MS = 0L
const val KEEP_DAYS = 14
const val ACTION_CONFIRM = "org.microg.gms.nearby.exposurenotification.CONFIRM"
@ -37,27 +38,7 @@ const val PERMISSION_EXPOSURE_CALLBACK = "com.google.android.gms.nearby.exposure
const val TX_POWER_LOW = -15
const val ADVERTISER_OFFSET = 60 * 1000
const val CLEANUP_INTERVAL = 24 * 60 * 60 * 1000
const val CLEANUP_INTERVAL = 24 * 60 * 60 * 1000L
const val VERSION_1_0: Byte = 0x40
const val VERSION_1_1: Byte = 0x50
/**
* No calibration data, using fleet-wide as default options.
*/
const val CONFIDENCE_LOWEST: Byte = 0
/**
* Using average calibration over models from manufacturer.
*/
const val CONFIDENCE_LOW: Byte = 1
/**
* Using single-antenna orientation for a similar model.
*/
const val CONFIDENCE_MEDIUM: Byte = 2
/**
* Using significant calibration data for this model.
*/
const val CONFIDENCE_HIGH: Byte = 3

View File

@ -10,14 +10,15 @@ package org.microg.gms.nearby.exposurenotification
import android.os.Build
import android.util.Log
import com.google.android.gms.nearby.exposurenotification.CalibrationConfidence
import kotlin.math.roundToInt
data class DeviceInfo(val oem: String, val model: String, val txPowerCorrection: Byte, val rssiCorrection: Byte, val confidence: Byte = CONFIDENCE_MEDIUM)
data class DeviceInfo(val oem: String, val model: String, val txPowerCorrection: Byte, val rssiCorrection: Byte, @CalibrationConfidence val confidence: Int = CalibrationConfidence.MEDIUM)
private var knownDeviceInfo: DeviceInfo? = null
fun averageCurrentDeviceInfo(oem: String, model: String, deviceInfos: List<DeviceInfo>, confidence: Byte = CONFIDENCE_LOW): DeviceInfo =
DeviceInfo(oem, model, deviceInfos.map { it.txPowerCorrection }.average().roundToInt().toByte(), deviceInfos.map { it.txPowerCorrection }.average().roundToInt().toByte(), CONFIDENCE_LOW)
fun averageCurrentDeviceInfo(oem: String, model: String, deviceInfos: List<DeviceInfo>, @CalibrationConfidence confidence: Int = CalibrationConfidence.LOW): DeviceInfo =
DeviceInfo(oem, model, deviceInfos.map { it.txPowerCorrection }.average().roundToInt().toByte(), deviceInfos.map { it.txPowerCorrection }.average().roundToInt().toByte(), CalibrationConfidence.LOW)
val currentDeviceInfo: DeviceInfo
get() {
@ -36,7 +37,7 @@ val currentDeviceInfo: DeviceInfo
}
else -> {
// Fallback to all device average
averageCurrentDeviceInfo(Build.MANUFACTURER, Build.MODEL, allDeviceInfos, CONFIDENCE_LOWEST)
averageCurrentDeviceInfo(Build.MANUFACTURER, Build.MODEL, allDeviceInfos, CalibrationConfidence.LOWEST)
}
}
Log.i(TAG, "Selected $deviceInfo")

View File

@ -17,12 +17,11 @@ import android.os.Parcelable
import android.util.Log
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import okio.ByteString
import java.io.File
import java.nio.ByteBuffer
import java.util.*
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.*
import kotlin.math.roundToInt
@TargetApi(21)
@ -43,28 +42,41 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
Log.d(TAG, "Upgrading database from $oldVersion to $newVersion")
if (oldVersion < 1) {
Log.d(TAG, "Creating tables for version >= 1")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_ADVERTISEMENTS(rpi BLOB NOT NULL, aem BLOB NOT NULL, timestamp INTEGER NOT NULL, rssi INTEGER NOT NULL, duration INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(rpi, timestamp));")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_ADVERTISEMENTS}_rpi ON $TABLE_ADVERTISEMENTS(rpi);")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_ADVERTISEMENTS}_timestamp ON $TABLE_ADVERTISEMENTS(timestamp);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_APP_LOG(package TEXT NOT NULL, timestamp INTEGER NOT NULL, method TEXT NOT NULL, args TEXT);")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_APP_LOG}_package_timestamp ON $TABLE_APP_LOG(package, timestamp);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK(keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_CONFIGURATIONS(package TEXT NOT NULL, token TEXT NOT NULL, configuration BLOB, PRIMARY KEY(package, token))")
}
if (oldVersion < 2) {
db.execSQL("DROP TABLE IF EXISTS $TABLE_TEK_CHECK;")
db.execSQL("DROP TABLE IF EXISTS $TABLE_DIAGNOSIS;")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK_CHECK(tcid INTEGER PRIMARY KEY, keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, matched INTEGER, UNIQUE(keyData, rollingStartNumber, rollingPeriod));")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_DIAGNOSIS(package TEXT NOT NULL, token TEXT NOT NULL, tcid INTEGER REFERENCES $TABLE_TEK_CHECK(tcid) ON DELETE CASCADE, transmissionRiskLevel INTEGER NOT NULL);")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_DIAGNOSIS}_package_token ON $TABLE_DIAGNOSIS(package, token);")
}
if (oldVersion < 3) {
Log.d(TAG, "Creating tables for version >= 3")
db.execSQL("CREATE TABLE $TABLE_APP_PERMS(package TEXT NOT NULL, sig TEXT NOT NULL, perm TEXT NOT NULL, timestamp INTEGER NOT NULL);")
}
if (oldVersion < 4) {
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_DIAGNOSIS}_tcid ON $TABLE_DIAGNOSIS(tcid);")
if (oldVersion < 5) {
Log.d(TAG, "Dropping legacy tables")
db.execSQL("DROP TABLE IF EXISTS $TABLE_CONFIGURATIONS;")
db.execSQL("DROP TABLE IF EXISTS $TABLE_DIAGNOSIS;")
db.execSQL("DROP TABLE IF EXISTS $TABLE_TEK_CHECK;")
Log.d(TAG, "Creating tables for version >= 3")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TOKENS(tid INTEGER PRIMARY KEY, package TEXT NOT NULL, token TEXT NOT NULL, timestamp INTEGER NOT NULL, configuration BLOB);")
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_${TABLE_TOKENS}_package_token ON $TABLE_TOKENS(package, token);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK_CHECK_SINGLE(tcsid INTEGER PRIMARY KEY, keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, matched INTEGER);")
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_${TABLE_TEK_CHECK_SINGLE}_key ON $TABLE_TEK_CHECK_SINGLE(keyData, rollingStartNumber, rollingPeriod);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK_CHECK_SINGLE_TOKEN(tcsid INTEGER REFERENCES $TABLE_TEK_CHECK_SINGLE(tcsid) ON DELETE CASCADE, tid INTEGER REFERENCES $TABLE_TOKENS(tid) ON DELETE CASCADE, transmissionRiskLevel INTEGER NOT NULL, UNIQUE(tcsid, tid));")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_TEK_CHECK_SINGLE_TOKEN}_tid ON $TABLE_TEK_CHECK_SINGLE_TOKEN(tid);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK_CHECK_FILE(tcfid INTEGER PRIMARY KEY, hash TEXT NOT NULL, endTimestamp INTEGER NOT NULL, keys INTEGER NOT NULL);")
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_${TABLE_TEK_CHECK_FILE}_hash ON $TABLE_TEK_CHECK_FILE(hash);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK_CHECK_FILE_TOKEN(tcfid INTEGER REFERENCES $TABLE_TEK_CHECK_FILE(tcfid) ON DELETE CASCADE, tid INTEGER REFERENCES $TABLE_TOKENS(tid) ON DELETE CASCADE, UNIQUE(tcfid, tid));")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_TEK_CHECK_FILE_TOKEN}_tid ON $TABLE_TEK_CHECK_FILE_TOKEN(tid);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK_CHECK_FILE_MATCH(tcfid INTEGER REFERENCES $TABLE_TEK_CHECK_FILE(tcfid) ON DELETE CASCADE, keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, transmissionRiskLevel INTEGER NOT NULL, UNIQUE(tcfid, keyData, rollingStartNumber, rollingPeriod));")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_TEK_CHECK_FILE_MATCH}_tcfid ON $TABLE_TEK_CHECK_FILE_MATCH(tcfid);")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_TEK_CHECK_FILE_MATCH}_key ON $TABLE_TEK_CHECK_FILE_MATCH(keyData, rollingStartNumber, rollingPeriod);")
}
Log.d(TAG, "Finished database upgrade")
}
fun SQLiteDatabase.delete(table: String, whereClause: String, args: LongArray): Int =
@ -74,23 +86,21 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
}
fun dailyCleanup() = writableDatabase.run {
beginTransaction()
try {
val rollingStartTime = currentRollingStartNumber * ROLLING_WINDOW_LENGTH * 1000 - TimeUnit.DAYS.toMillis(KEEP_DAYS.toLong())
val advertisements = delete(TABLE_ADVERTISEMENTS, "timestamp < ?", longArrayOf(rollingStartTime))
Log.d(TAG, "Deleted on daily cleanup: $advertisements adv")
val appLogEntries = delete(TABLE_APP_LOG, "timestamp < ?", longArrayOf(rollingStartTime))
Log.d(TAG, "Deleted on daily cleanup: $appLogEntries applogs")
val temporaryExposureKeys = delete(TABLE_TEK, "(rollingStartNumber + rollingPeriod) < ?", longArrayOf(rollingStartTime / ROLLING_WINDOW_LENGTH_MS))
Log.d(TAG, "Deleted on daily cleanup: $temporaryExposureKeys teks")
val checkedTemporaryExposureKeys = delete(TABLE_TEK_CHECK, "rollingStartNumber < ?", longArrayOf(rollingStartTime / ROLLING_WINDOW_LENGTH_MS - ROLLING_PERIOD))
Log.d(TAG, "Deleted on daily cleanup: $checkedTemporaryExposureKeys cteks")
val appPerms = delete(TABLE_APP_PERMS, "timestamp < ?", longArrayOf(System.currentTimeMillis() - CONFIRM_PERMISSION_VALIDITY))
Log.d(TAG, "Deleted on daily cleanup: $appPerms perms")
setTransactionSuccessful()
} finally {
endTransaction()
}
val rollingStartTime = currentRollingStartNumber * ROLLING_WINDOW_LENGTH * 1000 - TimeUnit.DAYS.toMillis(KEEP_DAYS.toLong())
val advertisements = delete(TABLE_ADVERTISEMENTS, "timestamp < ?", longArrayOf(rollingStartTime))
Log.d(TAG, "Deleted on daily cleanup: $advertisements adv")
val appLogEntries = delete(TABLE_APP_LOG, "timestamp < ?", longArrayOf(rollingStartTime))
Log.d(TAG, "Deleted on daily cleanup: $appLogEntries applogs")
val temporaryExposureKeys = delete(TABLE_TEK, "(rollingStartNumber + rollingPeriod) < ?", longArrayOf(rollingStartTime / ROLLING_WINDOW_LENGTH_MS))
Log.d(TAG, "Deleted on daily cleanup: $temporaryExposureKeys teks")
val singleCheckedTemporaryExposureKeys = delete(TABLE_TEK_CHECK_SINGLE, "rollingStartNumber < ?", longArrayOf(rollingStartTime / ROLLING_WINDOW_LENGTH_MS - ROLLING_PERIOD))
Log.d(TAG, "Deleted on daily cleanup: $singleCheckedTemporaryExposureKeys tcss")
val fileCheckedTemporaryExposureKeys = delete(TABLE_TEK_CHECK_FILE, "endTimestamp < ?", longArrayOf(rollingStartTime))
Log.d(TAG, "Deleted on daily cleanup: $fileCheckedTemporaryExposureKeys tcfs")
val appPerms = delete(TABLE_APP_PERMS, "timestamp < ?", longArrayOf(System.currentTimeMillis() - CONFIRM_PERMISSION_VALIDITY))
Log.d(TAG, "Deleted on daily cleanup: $appPerms perms")
execSQL("VACUUM;")
Log.d(TAG, "Done vacuuming")
}
fun grantPermission(packageName: String, signatureDigest: String, permission: String, timestamp: Long = System.currentTimeMillis()) = writableDatabase.run {
@ -132,7 +142,11 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
fun deleteAllCollectedAdvertisements() = writableDatabase.run {
delete(TABLE_ADVERTISEMENTS, null, null)
update(TABLE_TEK_CHECK, ContentValues().apply {
delete(TABLE_TEK_CHECK_FILE_MATCH, null, null)
update(TABLE_TEK_CHECK_SINGLE, ContentValues().apply {
put("matched", 0)
}, null, null)
update(TABLE_TEK_CHECK_FILE, ContentValues().apply {
put("matched", 0)
}, null, null)
}
@ -155,15 +169,15 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
})
}
private fun getTekCheckId(key: TemporaryExposureKey, mayInsert: Boolean = false, database: SQLiteDatabase = if (mayInsert) writableDatabase else readableDatabase): Long? = database.run {
private fun getTekCheckSingleId(key: TemporaryExposureKey, mayInsert: Boolean = false, database: SQLiteDatabase = if (mayInsert) writableDatabase else readableDatabase): Long? = database.run {
if (mayInsert) {
insertWithOnConflict(TABLE_TEK_CHECK, "NULL", ContentValues().apply {
insertWithOnConflict(TABLE_TEK_CHECK_SINGLE, "NULL", ContentValues().apply {
put("keyData", key.keyData)
put("rollingStartNumber", key.rollingStartIntervalNumber)
put("rollingPeriod", key.rollingPeriod)
}, CONFLICT_IGNORE)
}
compileStatement("SELECT tcid FROM $TABLE_TEK_CHECK WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ?").use {
compileStatement("SELECT tcsid FROM $TABLE_TEK_CHECK_SINGLE WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ?").use {
it.bindBlob(1, key.keyData)
it.bindLong(2, key.rollingStartIntervalNumber.toLong())
it.bindLong(3, key.rollingPeriod.toLong())
@ -171,57 +185,77 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
}
}
fun storeDiagnosisKey(packageName: String, token: String, key: TemporaryExposureKey, database: SQLiteDatabase = writableDatabase) = database.run {
val tcid = getTekCheckId(key, true, database)
insert(TABLE_DIAGNOSIS, "NULL", ContentValues().apply {
put("package", packageName)
put("token", token)
put("tcid", tcid)
fun getTokenId(packageName: String, token: String, database: SQLiteDatabase = readableDatabase) = database.run {
query(TABLE_TOKENS, arrayOf("tid"), "package = ? AND token = ?", arrayOf(packageName, token), null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
cursor.getLong(0)
} else {
null
}
}
}
private fun storeSingleDiagnosisKey(tid: Long, key: TemporaryExposureKey, database: SQLiteDatabase = writableDatabase) = database.run {
val tcsid = getTekCheckSingleId(key, true, database)
insert(TABLE_TEK_CHECK_SINGLE_TOKEN, "NULL", ContentValues().apply {
put("tid", tid)
put("tcsid", tcsid)
put("transmissionRiskLevel", key.transmissionRiskLevel)
})
}
fun batchStoreDiagnosisKey(packageName: String, token: String, keys: List<TemporaryExposureKey>, database: SQLiteDatabase = writableDatabase) = database.run {
fun batchStoreSingleDiagnosisKey(tid: Long, keys: List<TemporaryExposureKey>, database: SQLiteDatabase = writableDatabase) = database.run {
beginTransaction()
try {
keys.forEach { storeDiagnosisKey(packageName, token, it, database) }
keys.forEach { storeSingleDiagnosisKey(tid, it, database) }
setTransactionSuccessful()
} finally {
endTransaction()
}
}
fun updateDiagnosisKey(packageName: String, token: String, key: TemporaryExposureKey, database: SQLiteDatabase = writableDatabase) = database.run {
val tcid = getTekCheckId(key, false, database) ?: return 0
compileStatement("UPDATE $TABLE_DIAGNOSIS SET transmissionRiskLevel = ? WHERE package = ? AND token = ? AND tcid = ?;").use {
it.bindLong(1, key.transmissionRiskLevel.toLong())
it.bindString(2, packageName)
it.bindString(3, token)
it.bindLong(4, tcid)
it.executeUpdateDelete()
fun getDiagnosisFileId(hash: ByteArray, database: SQLiteDatabase = readableDatabase) = database.run {
val hexHash = ByteString.of(*hash).hex()
query(TABLE_TEK_CHECK_FILE, arrayOf("tcfid"), "hash = ?", arrayOf(hexHash), null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
cursor.getLong(0)
} else {
null
}
}
}
fun batchUpdateDiagnosisKey(packageName: String, token: String, keys: List<TemporaryExposureKey>, database: SQLiteDatabase = writableDatabase) = database.run {
beginTransaction()
try {
keys.forEach { updateDiagnosisKey(packageName, token, it, database) }
setTransactionSuccessful()
} finally {
endTransaction()
fun storeDiagnosisFileUsed(tid: Long, tcfid: Long, database: SQLiteDatabase = writableDatabase) = database.run {
insert(TABLE_TEK_CHECK_FILE_TOKEN, "NULL", ContentValues().apply {
put("tid", tid)
put("tcfid", tcfid)
})
}
fun storeDiagnosisFileUsed(tid: Long, hash: ByteArray, database: SQLiteDatabase = writableDatabase) = database.run {
val hexHash = ByteString.of(*hash).hex()
query(TABLE_TEK_CHECK_FILE, arrayOf("tcfid", "keys"), "hash = ?", arrayOf(hexHash), null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
insert(TABLE_TEK_CHECK_FILE_TOKEN, "NULL", ContentValues().apply {
put("tid", tid)
put("tcfid", cursor.getLong(0))
})
cursor.getLong(1)
} else {
null
}
}
}
private fun listDiagnosisKeysPendingSearch(packageName: String, token: String, database: SQLiteDatabase = readableDatabase) = database.run {
private fun listSingleDiagnosisKeysPendingSearch(tid: Long, database: SQLiteDatabase = readableDatabase) = database.run {
rawQuery("""
SELECT $TABLE_TEK_CHECK.keyData, $TABLE_TEK_CHECK.rollingStartNumber, $TABLE_TEK_CHECK.rollingPeriod
FROM $TABLE_DIAGNOSIS
LEFT JOIN $TABLE_TEK_CHECK ON $TABLE_DIAGNOSIS.tcid = $TABLE_TEK_CHECK.tcid
SELECT $TABLE_TEK_CHECK_SINGLE.keyData, $TABLE_TEK_CHECK_SINGLE.rollingStartNumber, $TABLE_TEK_CHECK_SINGLE.rollingPeriod
FROM $TABLE_TEK_CHECK_SINGLE_TOKEN
LEFT JOIN $TABLE_TEK_CHECK_SINGLE ON $TABLE_TEK_CHECK_SINGLE.tcsid = $TABLE_TEK_CHECK_SINGLE_TOKEN.tcsid
WHERE
$TABLE_DIAGNOSIS.package = ? AND
$TABLE_DIAGNOSIS.token = ? AND
$TABLE_TEK_CHECK.matched IS NULL
""", arrayOf(packageName, token)).use { cursor ->
$TABLE_TEK_CHECK_SINGLE_TOKEN.tid = ? AND
$TABLE_TEK_CHECK_SINGLE.matched IS NULL
""", arrayOf(tid.toString())).use { cursor ->
val list = arrayListOf<TemporaryExposureKey>()
while (cursor.moveToNext()) {
list.add(TemporaryExposureKey.TemporaryExposureKeyBuilder()
@ -234,8 +268,8 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
}
}
private fun applyDiagnosisKeySearchResult(key: TemporaryExposureKey, matched: Boolean, database: SQLiteDatabase = writableDatabase) = database.run {
compileStatement("UPDATE $TABLE_TEK_CHECK SET matched = ? WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ?;").use {
private fun applySingleDiagnosisKeySearchResult(key: TemporaryExposureKey, matched: Boolean, database: SQLiteDatabase = writableDatabase) = database.run {
compileStatement("UPDATE $TABLE_TEK_CHECK_SINGLE SET matched = ? WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ?;").use {
it.bindLong(1, if (matched) 1 else 0)
it.bindBlob(2, key.keyData)
it.bindLong(3, key.rollingStartIntervalNumber.toLong())
@ -244,16 +278,25 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
}
}
private fun listMatchedDiagnosisKeys(packageName: String, token: String, database: SQLiteDatabase = readableDatabase) = database.run {
private fun applyDiagnosisFileKeySearchResult(tcfid: Long, key: TemporaryExposureKey, database: SQLiteDatabase = writableDatabase) = database.run {
insert(TABLE_TEK_CHECK_FILE_MATCH, "NULL", ContentValues().apply {
put("tcfid", tcfid)
put("keyData", key.keyData)
put("rollingStartNumber", key.rollingStartIntervalNumber)
put("rollingPeriod", key.rollingPeriod)
put("transmissionRiskLevel", key.transmissionRiskLevel)
})
}
private fun listMatchedSingleDiagnosisKeys(tid: Long, database: SQLiteDatabase = readableDatabase) = database.run {
rawQuery("""
SELECT $TABLE_TEK_CHECK.keyData, $TABLE_TEK_CHECK.rollingStartNumber, $TABLE_TEK_CHECK.rollingPeriod, $TABLE_DIAGNOSIS.transmissionRiskLevel
FROM $TABLE_DIAGNOSIS
LEFT JOIN $TABLE_TEK_CHECK ON $TABLE_DIAGNOSIS.tcid = $TABLE_TEK_CHECK.tcid
SELECT $TABLE_TEK_CHECK_SINGLE.keyData, $TABLE_TEK_CHECK_SINGLE.rollingStartNumber, $TABLE_TEK_CHECK_SINGLE.rollingPeriod, $TABLE_TEK_CHECK_SINGLE_TOKEN.transmissionRiskLevel
FROM $TABLE_TEK_CHECK_SINGLE_TOKEN
JOIN $TABLE_TEK_CHECK_SINGLE ON $TABLE_TEK_CHECK_SINGLE.tcsid = $TABLE_TEK_CHECK_SINGLE_TOKEN.tcsid
WHERE
$TABLE_DIAGNOSIS.package = ? AND
$TABLE_DIAGNOSIS.token = ? AND
$TABLE_TEK_CHECK.matched = 1
""", arrayOf(packageName, token)).use { cursor ->
$TABLE_TEK_CHECK_SINGLE_TOKEN.tid = ? AND
$TABLE_TEK_CHECK_SINGLE.matched = 1
""", arrayOf(tid.toString())).use { cursor ->
val list = arrayListOf<TemporaryExposureKey>()
while (cursor.moveToNext()) {
list.add(TemporaryExposureKey.TemporaryExposureKeyBuilder()
@ -267,36 +310,118 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
}
}
fun finishMatching(packageName: String, token: String, database: SQLiteDatabase = writableDatabase) {
val start = System.currentTimeMillis()
private fun listMatchedFileDiagnosisKeys(tid: Long, database: SQLiteDatabase = readableDatabase) = database.run {
rawQuery("""
SELECT $TABLE_TEK_CHECK_FILE_MATCH.keyData, $TABLE_TEK_CHECK_FILE_MATCH.rollingStartNumber, $TABLE_TEK_CHECK_FILE_MATCH.rollingPeriod, $TABLE_TEK_CHECK_FILE_MATCH.transmissionRiskLevel
FROM $TABLE_TEK_CHECK_FILE_TOKEN
JOIN $TABLE_TEK_CHECK_FILE_MATCH ON $TABLE_TEK_CHECK_FILE_MATCH.tcfid = $TABLE_TEK_CHECK_FILE_TOKEN.tcfid
WHERE
$TABLE_TEK_CHECK_FILE_TOKEN.tid = ?
""", arrayOf(tid.toString())).use { cursor ->
val list = arrayListOf<TemporaryExposureKey>()
while (cursor.moveToNext()) {
list.add(TemporaryExposureKey.TemporaryExposureKeyBuilder()
.setKeyData(cursor.getBlob(0))
.setRollingStartIntervalNumber(cursor.getLong(1).toInt())
.setRollingPeriod(cursor.getLong(2).toInt())
.setTransmissionRiskLevel(cursor.getLong(3).toInt())
.build())
}
list
}
}
fun finishSingleMatching(tid: Long, database: SQLiteDatabase = writableDatabase): Int {
val workQueue = LinkedBlockingQueue<Runnable>()
val poolSize = Runtime.getRuntime().availableProcessors()
val executor = ThreadPoolExecutor(poolSize, poolSize, 1, TimeUnit.SECONDS, workQueue)
val futures = arrayListOf<Future<*>>()
val keys = listDiagnosisKeysPendingSearch(packageName, token, database)
val keys = listSingleDiagnosisKeysPendingSearch(tid, database)
val oldestRpi = oldestRpi
for (key in keys) {
if (oldestRpi == null || (key.rollingStartIntervalNumber + key.rollingPeriod) * ROLLING_WINDOW_LENGTH_MS + ALLOWED_KEY_OFFSET_MS < oldestRpi) {
if ((key.rollingStartIntervalNumber + key.rollingPeriod) * ROLLING_WINDOW_LENGTH_MS + ALLOWED_KEY_OFFSET_MS < oldestRpi) {
// Early ignore because key is older than since we started scanning.
applyDiagnosisKeySearchResult(key, false, database)
applySingleDiagnosisKeySearchResult(key, false, database)
} else {
futures.add(executor.submit {
applyDiagnosisKeySearchResult(key, findMeasuredExposures(key).isNotEmpty(), database)
applySingleDiagnosisKeySearchResult(key, findMeasuredExposures(key).isNotEmpty(), database)
})
}
}
for (future in futures) {
future.get()
}
val time = (System.currentTimeMillis() - start).toDouble() / 1000.0
executor.shutdown()
Log.d(TAG, "Processed ${keys.size} new keys in ${time}s -> ${(keys.size.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s")
return keys.size
}
fun findAllMeasuredExposures(packageName: String, token: String, database: SQLiteDatabase = readableDatabase): List<MeasuredExposure> {
return listMatchedDiagnosisKeys(packageName, token, database).flatMap { findMeasuredExposures(it, database) }
fun finishFileMatching(tid: Long, hash: ByteArray, endTimestamp: Long, keys: List<TemporaryExposureKey>, updates: List<TemporaryExposureKey>, database: SQLiteDatabase = writableDatabase) = database.run {
beginTransaction()
try {
insert(TABLE_TEK_CHECK_FILE, "NULL", ContentValues().apply {
put("hash", ByteString.of(*hash).hex())
put("endTimestamp", endTimestamp)
put("keys", keys.size + updates.size)
})
val tcfid = getDiagnosisFileId(hash, this) ?: return
val workQueue = LinkedBlockingQueue<Runnable>()
val poolSize = Runtime.getRuntime().availableProcessors()
val executor = ThreadPoolExecutor(poolSize, poolSize, 1, TimeUnit.SECONDS, workQueue)
val futures = arrayListOf<Future<*>>()
val oldestRpi = oldestRpi
var ignored = 0
var processed = 0
var found = 0
for (key in keys) {
if ((key.rollingStartIntervalNumber + key.rollingPeriod) * ROLLING_WINDOW_LENGTH_MS + ALLOWED_KEY_OFFSET_MS < oldestRpi) {
// Early ignore because key is older than since we started scanning.
ignored++;
} else {
futures.add(executor.submit {
if (findMeasuredExposures(key).isNotEmpty()) {
applyDiagnosisFileKeySearchResult(tcfid, key, this)
found++;
}
processed++
})
}
}
for (future in futures) {
future.get()
}
Log.d(TAG, "Processed $processed keys, found $found matches, ignored $ignored keys that are older than our scanning efforts ($oldestRpi)")
executor.shutdown()
for (update in updates) {
val matched = compileStatement("SELECT COUNT(tcsid) FROM $TABLE_TEK_CHECK_FILE_MATCH WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ?").use {
it.bindBlob(1, update.keyData)
it.bindLong(2, update.rollingStartIntervalNumber.toLong())
it.bindLong(3, update.rollingPeriod.toLong())
it.simpleQueryForLong()
}
if (matched > 0) {
applyDiagnosisFileKeySearchResult(tcfid, update, this)
}
}
insert(TABLE_TEK_CHECK_FILE_TOKEN, "NULL", ContentValues().apply {
put("tid", tid)
put("tcfid", tcfid)
})
setTransactionSuccessful()
} finally {
endTransaction()
}
}
fun findAllSingleMeasuredExposures(tid: Long, database: SQLiteDatabase = readableDatabase): List<MeasuredExposure> {
return listMatchedSingleDiagnosisKeys(tid, database).flatMap { findMeasuredExposures(it, database) }
}
fun findAllFileMeasuredExposures(tid: Long, database: SQLiteDatabase = readableDatabase): List<MeasuredExposure> {
return listMatchedFileDiagnosisKeys(tid, database).flatMap { findMeasuredExposures(it, database) }
}
fun findAllMeasuredExposures(tid: Long, database: SQLiteDatabase = readableDatabase) = findAllSingleMeasuredExposures(tid, database) + findAllFileMeasuredExposures(tid, database)
private fun findMeasuredExposures(key: TemporaryExposureKey, database: SQLiteDatabase = readableDatabase): List<MeasuredExposure> {
val allRpis = key.generateAllRpiIds()
val rpis = (0 until key.rollingPeriod).map { i ->
@ -385,21 +510,23 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
return res
}
fun storeConfiguration(packageName: String, token: String, configuration: ExposureConfiguration) = writableDatabase.run {
val update = update(TABLE_CONFIGURATIONS, ContentValues().apply { put("configuration", configuration.marshall()) }, "package = ? AND token = ?", arrayOf(packageName, token))
fun storeConfiguration(packageName: String, token: String, configuration: ExposureConfiguration, database: SQLiteDatabase = writableDatabase) = database.run {
val update = update(TABLE_TOKENS, ContentValues().apply { put("configuration", configuration.marshall()) }, "package = ? AND token = ?", arrayOf(packageName, token))
if (update <= 0) {
insert(TABLE_CONFIGURATIONS, "NULL", ContentValues().apply {
insert(TABLE_TOKENS, "NULL", ContentValues().apply {
put("package", packageName)
put("token", token)
put("timestamp", System.currentTimeMillis())
put("configuration", configuration.marshall())
})
}
getTokenId(packageName, token, database)
}
fun loadConfiguration(packageName: String, token: String): ExposureConfiguration? = readableDatabase.run {
query(TABLE_CONFIGURATIONS, arrayOf("configuration"), "package = ? AND token = ?", arrayOf(packageName, token), null, null, null, null).use { cursor ->
fun loadConfiguration(packageName: String, token: String, database: SQLiteDatabase = readableDatabase): Pair<Long, ExposureConfiguration>? = database.run {
query(TABLE_TOKENS, arrayOf("tid", "configuration"), "package = ? AND token = ?", arrayOf(packageName, token), null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
ExposureConfiguration.CREATOR.unmarshall(cursor.getBlob(0))
cursor.getLong(0) to ExposureConfiguration.CREATOR.unmarshall(cursor.getBlob(1))
} else {
null
}
@ -454,13 +581,13 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
}
}
val oldestRpi: Long?
val oldestRpi: Long
get() = readableDatabase.run {
query(TABLE_ADVERTISEMENTS, arrayOf("MIN(timestamp)"), null, null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
cursor.getLong(0)
} else {
null
System.currentTimeMillis()
}
}
}
@ -532,7 +659,7 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
override fun getWritableDatabase(): SQLiteDatabase {
if (this != instance) {
throw IllegalStateException("Tried to open writable database from secondary instance")
throw IllegalStateException("Tried to open writable database from secondary instance. We are ${hashCode()} but primary is ${instance?.hashCode()}")
}
return super.getWritableDatabase()
}
@ -560,19 +687,32 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
companion object {
private const val DB_NAME = "exposure.db"
private const val DB_VERSION = 4
private const val DB_VERSION = 5
private const val TABLE_ADVERTISEMENTS = "advertisements"
private const val TABLE_APP_LOG = "app_log"
private const val TABLE_TEK = "tek"
private const val TABLE_TEK_CHECK = "tek_check"
private const val TABLE_DIAGNOSIS = "diagnosis"
private const val TABLE_CONFIGURATIONS = "configurations"
private const val TABLE_APP_PERMS = "app_perms"
private const val TABLE_TOKENS = "tokens"
private const val TABLE_TEK_CHECK_SINGLE = "tek_check_single"
private const val TABLE_TEK_CHECK_SINGLE_TOKEN = "tek_check_single_token"
private const val TABLE_TEK_CHECK_FILE = "tek_check_file"
private const val TABLE_TEK_CHECK_FILE_TOKEN = "tek_check_file_token"
private const val TABLE_TEK_CHECK_FILE_MATCH = "tek_check_file_match"
@Deprecated(message = "No longer supported")
private const val TABLE_TEK_CHECK = "tek_check"
@Deprecated(message = "No longer supported")
private const val TABLE_DIAGNOSIS = "diagnosis"
@Deprecated(message = "No longer supported")
private const val TABLE_CONFIGURATIONS = "configurations"
private var instance: ExposureDatabase? = null
fun ref(context: Context): ExposureDatabase = synchronized(this) {
if (instance == null) {
instance = ExposureDatabase(context.applicationContext)
Log.d(TAG, "Created instance ${instance?.hashCode()} of database for ${context.javaClass.name}")
}
instance!!.ref()
}

View File

@ -29,8 +29,10 @@ class ExposureNotificationService : BaseService(TAG, GmsService.NEARBY_EXPOSURE)
return permission
}
checkPermission("android.permission.BLUETOOTH") ?: return
checkPermission("android.permission.INTERNET") ?: return
if (request.packageName != packageName) {
checkPermission("android.permission.BLUETOOTH") ?: return
checkPermission("android.permission.INTERNET") ?: return
}
if (Build.VERSION.SDK_INT < 21) {
callback.onPostInitComplete(FAILED_NOT_SUPPORTED, null, null)
@ -39,7 +41,10 @@ class ExposureNotificationService : BaseService(TAG, GmsService.NEARBY_EXPOSURE)
Log.d(TAG, "handleServiceRequest: " + request.packageName)
callback.onPostInitCompleteWithConnectionInfo(SUCCESS, ExposureNotificationServiceImpl(this, request.packageName), ConnectionInfo().apply {
features = arrayOf(Feature("nearby_exposure_notification", 3))
features = arrayOf(
Feature("nearby_exposure_notification", 3),
Feature("nearby_exposure_notification_get_version", 1)
)
})
}
}

View File

@ -10,25 +10,29 @@ import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.Intent.*
import android.os.*
import android.util.Log
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import com.google.android.gms.nearby.exposurenotification.ExposureInformation
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
import com.google.android.gms.nearby.exposurenotification.ExposureSummary
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import com.google.android.gms.nearby.exposurenotification.internal.*
import org.json.JSONArray
import org.json.JSONObject
import org.microg.gms.common.Constants
import org.microg.gms.common.PackageUtils
import org.microg.gms.nearby.exposurenotification.Constants.*
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyExport
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyProto
import java.io.File
import java.io.InputStream
import java.security.MessageDigest
import java.util.*
import java.util.zip.ZipInputStream
import java.util.zip.ZipFile
import kotlin.math.roundToInt
import kotlin.random.Random
class ExposureNotificationServiceImpl(private val context: Context, private val packageName: String) : INearbyExposureNotificationService.Stub() {
private fun pendingConfirm(permission: String): PendingIntent {
@ -65,6 +69,14 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
}
override fun getVersion(params: GetVersionParams) {
params.callback.onResult(Status.SUCCESS, Constants.MAX_REFERENCE_VERSION.toLong())
}
override fun getCalibrationConfidence(params: GetCalibrationConfidenceParams) {
params.callback.onResult(Status.SUCCESS, currentDeviceInfo.confidence)
}
override fun start(params: StartParams) {
if (ExposurePreferences(context).enabled) return
val status = confirmPermission(CONFIRM_ACTION_START)
@ -90,7 +102,6 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
Log.w(TAG, "Callback failed", e)
}
}
override fun isEnabled(params: IsEnabledParams) {
try {
params.callback.onResult(Status.SUCCESS, ExposurePreferences(context).enabled)
@ -129,67 +140,73 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
.setTransmissionRiskLevel(transmission_risk_level ?: 0)
.build()
private fun storeDiagnosisKeyExport(token: String, export: TemporaryExposureKeyExport): Int = ExposureDatabase.with(context) { database ->
Log.d(TAG, "Importing keys from file ${export.start_timestamp?.let { Date(it * 1000) }} to ${export.end_timestamp?.let { Date(it * 1000) }}")
database.batchStoreDiagnosisKey(packageName, token, export.keys.map { it.toKey() })
database.batchUpdateDiagnosisKey(packageName, token, export.revised_keys.map { it.toKey() })
export.keys.size + export.revised_keys.size
private fun InputStream.copyToFile(outputFile: File) {
outputFile.outputStream().use { output ->
copyTo(output)
output.flush()
}
}
private fun MessageDigest.digest(file: File): ByteArray = file.inputStream().use { input ->
val buf = ByteArray(4096)
var bytes = input.read(buf)
while (bytes != -1) {
update(buf, 0, bytes)
bytes = input.read(buf)
}
digest()
}
override fun provideDiagnosisKeys(params: ProvideDiagnosisKeysParams) {
Log.w(TAG, "provideDiagnosisKeys() with $packageName/${params.token}")
val tid = ExposureDatabase.with(context) { database ->
if (params.configuration != null) {
database.storeConfiguration(packageName, params.token, params.configuration)
} else {
database.getTokenId(packageName, params.token)
}
}
if (tid == null) {
Log.w(TAG, "Unknown token without configuration: $packageName/${params.token}")
try {
params.callback.onResult(Status.INTERNAL_ERROR)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
return
}
Thread(Runnable {
ExposureDatabase.with(context) { database ->
if (params.configuration != null) {
database.storeConfiguration(packageName, params.token, params.configuration)
}
val start = System.currentTimeMillis()
// keys
params.keys?.let { database.batchStoreDiagnosisKey(packageName, params.token, it) }
params.keys?.let { database.batchStoreSingleDiagnosisKey(tid, it) }
var keys = params.keys?.size ?: 0
// Key files
var keys = params.keys?.size ?: 0
val todoKeyFiles = arrayListOf<Pair<File, ByteArray>>()
for (file in params.keyFiles.orEmpty()) {
try {
ZipInputStream(ParcelFileDescriptor.AutoCloseInputStream(file)).use { stream ->
do {
val entry = stream.nextEntry ?: break
if (entry.name == "export.bin") {
val prefix = ByteArray(16)
var totalBytesRead = 0
var bytesRead = 0
while (bytesRead != -1 && totalBytesRead < prefix.size) {
bytesRead = stream.read(prefix, totalBytesRead, prefix.size - totalBytesRead)
if (bytesRead > 0) {
totalBytesRead += bytesRead
}
}
if (totalBytesRead == prefix.size && String(prefix).trim() == "EK Export v1") {
val fileKeys = storeDiagnosisKeyExport(params.token, TemporaryExposureKeyExport.ADAPTER.decode(stream))
keys += fileKeys
} else {
Log.d(TAG, "export.bin had invalid prefix")
}
}
stream.closeEntry()
} while (true);
val cacheFile = File(context.cacheDir, "en-keyfile-${System.currentTimeMillis()}-${Random.nextInt()}.zip")
ParcelFileDescriptor.AutoCloseInputStream(file).use { it.copyToFile(cacheFile) }
val hash = MessageDigest.getInstance("SHA-256").digest(cacheFile)
val storedKeys = database.storeDiagnosisFileUsed(tid, hash)
if (storedKeys != null) {
keys += storedKeys.toInt()
cacheFile.delete()
} else {
todoKeyFiles.add(cacheFile to hash)
}
} catch (e: Exception) {
Log.w(TAG, "Failed parsing file", e)
}
}
val time = (System.currentTimeMillis() - start).toDouble() / 1000.0
Log.d(TAG, "$packageName/${params.token} provided $keys keys in ${time}s -> ${(keys.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s")
database.noteAppAction(packageName, "provideDiagnosisKeys", JSONObject().apply {
put("request_token", params.token)
put("request_keys_size", params.keys?.size)
put("request_keyFiles_size", params.keyFiles?.size)
put("request_keys_count", keys)
}.toString())
database.finishMatching(packageName, params.token)
if (todoKeyFiles.size > 0) {
val time = (System.currentTimeMillis() - start).toDouble() / 1000.0
Log.d(TAG, "$packageName/${params.token} processed $keys keys (${todoKeyFiles.size} files pending) in ${time}s -> ${(keys.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s")
}
Handler(Looper.getMainLooper()).post {
try {
@ -199,7 +216,47 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
}
val match = database.findAllMeasuredExposures(packageName, params.token).isNotEmpty()
var newKeys = if (params.keys != null) database.finishSingleMatching(tid) else 0
for ((cacheFile, hash) in todoKeyFiles) {
ZipFile(cacheFile).use { zip ->
for (entry in zip.entries()) {
if (entry.name == "export.bin") {
val stream = zip.getInputStream(entry)
val prefix = ByteArray(16)
var totalBytesRead = 0
var bytesRead = 0
while (bytesRead != -1 && totalBytesRead < prefix.size) {
bytesRead = stream.read(prefix, totalBytesRead, prefix.size - totalBytesRead)
if (bytesRead > 0) {
totalBytesRead += bytesRead
}
}
if (totalBytesRead == prefix.size && String(prefix).trim() == "EK Export v1") {
val export = TemporaryExposureKeyExport.ADAPTER.decode(stream)
database.finishFileMatching(tid, hash, export.end_timestamp?.let { it * 1000 }
?: System.currentTimeMillis(), export.keys.map { it.toKey() }, export.revised_keys.map { it.toKey() })
keys += export.keys.size + export.revised_keys.size
newKeys += export.keys.size
} else {
Log.d(TAG, "export.bin had invalid prefix")
}
}
}
}
cacheFile.delete()
}
val time = (System.currentTimeMillis() - start).toDouble() / 1000.0
Log.d(TAG, "$packageName/${params.token} processed $keys keys ($newKeys new) in ${time}s -> ${(keys.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s")
database.noteAppAction(packageName, "provideDiagnosisKeys", JSONObject().apply {
put("request_token", params.token)
put("request_keys_size", params.keys?.size)
put("request_keyFiles_size", params.keyFiles?.size)
put("request_keys_count", keys)
}.toString())
val match = database.findAllMeasuredExposures(tid).isNotEmpty()
try {
val intent = Intent(if (match) ACTION_EXPOSURE_STATE_UPDATED else ACTION_EXPOSURE_NOT_FOUND)
@ -215,16 +272,12 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
override fun getExposureSummary(params: GetExposureSummaryParams): Unit = ExposureDatabase.with(context) { database ->
val configuration = database.loadConfiguration(packageName, params.token)
if (configuration == null) {
try {
params.callback.onResult(Status.INTERNAL_ERROR, null)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
return@with
val pair = database.loadConfiguration(packageName, params.token)
val (configuration, exposures) = if (pair != null) {
pair.second to database.findAllMeasuredExposures(pair.first).merge()
} else {
ExposureConfiguration.ExposureConfigurationBuilder().build() to emptyList()
}
val exposures = database.findAllMeasuredExposures(packageName, params.token).merge()
val response = ExposureSummary.ExposureSummaryBuilder()
.setDaysSinceLastExposure(exposures.map { it.daysSinceExposure }.min()?.toInt() ?: 0)
.setMatchedKeyCount(exposures.map { it.key }.distinct().size)
@ -255,18 +308,13 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
override fun getExposureInformation(params: GetExposureInformationParams): Unit = ExposureDatabase.with(context) { database ->
// TODO: Notify user?
val configuration = database.loadConfiguration(packageName, params.token)
if (configuration == null) {
try {
params.callback.onResult(Status.INTERNAL_ERROR, null)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
val pair = database.loadConfiguration(packageName, params.token)
val response = if (pair != null) {
database.findAllMeasuredExposures(pair.first).merge().map {
it.toExposureInformation(pair.second)
}
return@with
}
val response = database.findAllMeasuredExposures(packageName, params.token).merge().map {
it.toExposureInformation(configuration)
} else {
emptyList()
}
database.noteAppAction(packageName, "getExposureInformation", JSONObject().apply {
@ -280,6 +328,26 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
}
override fun getExposureWindows(params: GetExposureWindowsParams) {
Log.w(TAG, "Not yet implemented: getExposureWindows")
params.callback.onResult(Status.INTERNAL_ERROR, emptyList())
}
override fun getDailySummaries(params: GetDailySummariesParams) {
Log.w(TAG, "Not yet implemented: getDailySummaries")
params.callback.onResult(Status.INTERNAL_ERROR, emptyList())
}
override fun setDiagnosisKeysDataMapping(params: SetDiagnosisKeysDataMappingParams) {
Log.w(TAG, "Not yet implemented: setDiagnosisKeysDataMapping")
params.callback.onResult(Status.INTERNAL_ERROR)
}
override fun getDiagnosisKeysDataMapping(params: GetDiagnosisKeysDataMappingParams) {
Log.w(TAG, "Not yet implemented: getDiagnosisKeysDataMapping")
params.callback.onResult(Status.INTERNAL_ERROR, null)
}
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
if (super.onTransact(code, data, reply, flags)) return true
Log.d(TAG, "onTransact [unknown]: $code, $data, $flags")

View File

@ -28,6 +28,8 @@
# Keep AutoSafeParcelables
-keep public class * extends org.microg.safeparcel.AutoSafeParcelable {
@org.microg.safeparcel.SafeParceled *;
@org.microg.safeparcel.SafeParcelable.Field *;
<init>(...);
}
# Keep form data