diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/api/ApiException.java b/play-services-basement/src/main/java/com/google/android/gms/common/api/ApiException.java index c508b301..52cccd3c 100644 --- a/play-services-basement/src/main/java/com/google/android/gms/common/api/ApiException.java +++ b/play-services-basement/src/main/java/com/google/android/gms/common/api/ApiException.java @@ -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; diff --git a/play-services-basement/src/main/java/com/google/android/gms/common/api/ResolvableApiException.java b/play-services-basement/src/main/java/com/google/android/gms/common/api/ResolvableApiException.java index 22dc6ed0..d46480d3 100644 --- a/play-services-basement/src/main/java/com/google/android/gms/common/api/ResolvableApiException.java +++ b/play-services-basement/src/main/java/com/google/android/gms/common/api/ResolvableApiException.java @@ -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; diff --git a/play-services-nearby-api/build.gradle b/play-services-nearby-api/build.gradle index 8efb9225..650fe429 100644 --- a/play-services-nearby-api/build.gradle +++ b/play-services-nearby-api/build.gradle @@ -23,6 +23,5 @@ android { dependencies { api project(':play-services-basement') - api project(':play-services-base-api') } diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/DailySummary.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/DailySummary.aidl new file mode 100644 index 00000000..4c7d25b6 --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/DailySummary.aidl @@ -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; diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/DiagnosisKeysDataMapping.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/DiagnosisKeysDataMapping.aidl new file mode 100644 index 00000000..390638fb --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/DiagnosisKeysDataMapping.aidl @@ -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; diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/ExposureWindow.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/ExposureWindow.aidl new file mode 100644 index 00000000..b6247e34 --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/ExposureWindow.aidl @@ -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; diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetCalibrationConfidenceParams.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetCalibrationConfidenceParams.aidl new file mode 100644 index 00000000..641a2237 --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetCalibrationConfidenceParams.aidl @@ -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; diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetDailySummariesParams.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetDailySummariesParams.aidl new file mode 100644 index 00000000..fd47500b --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetDailySummariesParams.aidl @@ -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; diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetDiagnosisKeysDataMappingParams.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetDiagnosisKeysDataMappingParams.aidl new file mode 100644 index 00000000..07316df1 --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetDiagnosisKeysDataMappingParams.aidl @@ -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; diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetExposureWindowsParams.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetExposureWindowsParams.aidl new file mode 100644 index 00000000..1df7e95c --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetExposureWindowsParams.aidl @@ -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; diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetVersionParams.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetVersionParams.aidl new file mode 100644 index 00000000..4c0acbf8 --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/GetVersionParams.aidl @@ -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; diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IDailySummaryListCallback.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IDailySummaryListCallback.aidl new file mode 100644 index 00000000..4f52acb3 --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IDailySummaryListCallback.aidl @@ -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 result); +} diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IDiagnosisKeysDataMappingCallback.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IDiagnosisKeysDataMappingCallback.aidl new file mode 100644 index 00000000..61f3e1ff --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IDiagnosisKeysDataMappingCallback.aidl @@ -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); +} diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IExposureWindowListCallback.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IExposureWindowListCallback.aidl new file mode 100644 index 00000000..2f314877 --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IExposureWindowListCallback.aidl @@ -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 result); +} diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IIntCallback.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IIntCallback.aidl new file mode 100644 index 00000000..3dc8faf3 --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/IIntCallback.aidl @@ -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); +} diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/ILongCallback.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/ILongCallback.aidl new file mode 100644 index 00000000..a17a61df --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/ILongCallback.aidl @@ -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); +} diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/INearbyExposureNotificationService.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/INearbyExposureNotificationService.aidl index 8d13de35..b5dcfbe8 100644 --- a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/INearbyExposureNotificationService.aidl +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/INearbyExposureNotificationService.aidl @@ -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; } diff --git a/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/SetDiagnosisKeysDataMappingParams.aidl b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/SetDiagnosisKeysDataMappingParams.aidl new file mode 100644 index 00000000..82fb3193 --- /dev/null +++ b/play-services-nearby-api/src/main/aidl/com/google/android/gms/nearby/exposurenotification/internal/SetDiagnosisKeysDataMappingParams.aidl @@ -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; diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/CalibrationConfidence.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/CalibrationConfidence.java new file mode 100644 index 00000000..f350b622 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/CalibrationConfidence.java @@ -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; +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/DailySummariesConfig.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/DailySummariesConfig.java new file mode 100644 index 00000000..d1b72102 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/DailySummariesConfig.java @@ -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. + *

+ * During summarization the following are computed for each ExposureWindows: + *

+ *

+ * The {@link ExposureWindow}s are then filtered, removing those with score lower than {@link #getMinimumWindowScore()}. + *

+ * 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: + *

+ * 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 reportTypeWeights; + @Field(2) + private List infectiousnessWeights; + @Field(3) + private List attenuationBucketThresholdDb; + @Field(4) + private List 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. + *

+ * This list must have 3 elements: the immediate, near, and medium thresholds. See attenuationBucketWeights for more information. + *

+ * These elements must be between 0 and 255 and come in increasing order. + */ + public List getAttenuationBucketThresholdDb() { + return attenuationBucketThresholdDb; + } + + /** + * Scoring weights to associate with ScanInstances depending on the attenuation bucket in which their typicalAttenuationDb falls. + *

+ * This list must have 4 elements, corresponding to the weights for the 4 buckets. + *

    + *
  • immediate bucket: -infinity < attenuation <= immediate threshold
  • + *
  • near bucket: immediate threshold < attenuation <= near threshold
  • + *
  • medium bucket: near threshold < attenuation <= medium threshold
  • + *
  • other bucket: medium threshold < attenuation < +infinity
  • + *
+ * Each element must be between 0 and 2.5. + */ + public List 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. + *

+ * This map can include weights for the following Infectiousness values: + *

    + *
  • STANDARD
  • + *
  • HIGH
  • + *
+ * Each element must be between 0 and 2.5. + */ + public Map getInfectiousnessWeights() { + HashMap 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}. + *

+ * Use 0 to consider all {@link ExposureWindow}s (recommended). + */ + public double getMinimumWindowScore() { + return minimumWindowScore; + } + + /** + * Scoring weights to associate with exposures with different ReportTypes. + *

+ * This map can include weights for the following ReportTypes: + *

    + *
  • CONFIRMED_TEST
  • + *
  • CONFIRMED_CLINICAL_DIAGNOSIS
  • + *
  • SELF_REPORT
  • + *
  • RECURSIVE (reserved for future use)
  • + *
+ * Each element must be between 0 and 2.5. + */ + public Map getReportTypeWeights() { + HashMap 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 attenuationBucketThresholdDb; + private List 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 thresholds, List 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 CREATOR = new AutoCreator<>(DailySummariesConfig.class); +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/DailySummary.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/DailySummary.java new file mode 100644 index 00000000..340bc087 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/DailySummary.java @@ -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 reportSummaries; + @Field(3) + private ExposureSummaryData summaryData; + + private DailySummary() { + } + + @PublicApi(exclude = true) + public DailySummary(int daysSinceEpoch, List 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. + *

+ * 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. + *

+ * 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. + *

+ * 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 CREATOR = new AutoCreator<>(ExposureSummaryData.class); + } + + public static final Creator CREATOR = new AutoCreator<>(DailySummary.class); +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/DiagnosisKeysDataMapping.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/DiagnosisKeysDataMapping.java new file mode 100644 index 00000000..71c79fa0 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/DiagnosisKeysDataMapping.java @@ -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 daysSinceOnsetToInfectiousness; + @Field(2) + @ReportType + private int reportTypeWhenMissing; + @Field(3) + @Infectiousness + private int infectiousnessWhenDaysSinceOnsetMissing; + + /** + * Mapping from diagnosisKey.daysSinceOnsetOfSymptoms to {@link Infectiousness}. + *

+ * 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}. + *

+ * 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 getDaysSinceOnsetToInfectiousness() { + HashMap 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. + *

+ * See {@link #getDaysSinceOnsetToInfectiousness()} for more info. + */ + public int getInfectiousnessWhenDaysSinceOnsetMissing() { + return infectiousnessWhenDaysSinceOnsetMissing; + } + + /** + * Report type to default to when a TEK has no report type set. + *

+ * 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 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 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 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 CREATOR = new AutoCreator<>(DiagnosisKeysDataMapping.class); +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/ExposureWindow.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/ExposureWindow.java new file mode 100644 index 00000000..55bac873 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/ExposureWindow.java @@ -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. + *

+ * 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. + *

+ * The TEK itself isn't exposed by the API. + */ +@PublicApi +public class ExposureWindow extends AutoSafeParcelable { + @Field(1) + private long dateMillisSinceEpoch; + @Field(2) + private List 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 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 + *

+ * TEKs with no report type set are returned with reportType=CONFIRMED_TEST. + *

+ * TEKs with RECURSIVE report type may be dropped because this report type is reserved for future use. + *

+ * TEKs with REVOKED or invalid report types do not lead to exposures. + */ + @ReportType + public int getReportType() { + return reportType; + } + + /** + * Sightings of this ExposureWindow, time-ordered. + *

+ * Each sighting corresponds to a scan (of a few seconds) during which a beacon with the TEK causing this exposure was observed. + */ + public List 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 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 scanInstances) { + this.scanInstances = new ArrayList<>(scanInstances); + return this; + } + } + + public static final Creator CREATOR = new AutoCreator<>(ExposureWindow.class); +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/Infectiousness.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/Infectiousness.java new file mode 100644 index 00000000..b63031d3 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/Infectiousness.java @@ -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; +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/ReportType.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/ReportType.java new file mode 100644 index 00000000..e14d2bea --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/ReportType.java @@ -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; +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/RiskLevel.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/RiskLevel.java index 6e7c288d..f41dfbb2 100644 --- a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/RiskLevel.java +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/RiskLevel.java @@ -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; } diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/ScanInstance.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/ScanInstance.java new file mode 100644 index 00000000..c2de9411 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/ScanInstance.java @@ -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). + *

+ * 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. + *

+ * 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. + *

+ * 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 CREATOR = new AutoCreator<>(ScanInstance.class); +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetCalibrationConfidenceParams.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetCalibrationConfidenceParams.java new file mode 100644 index 00000000..8002b4b3 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetCalibrationConfidenceParams.java @@ -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 CREATOR = new AutoCreator<>(GetCalibrationConfidenceParams.class); +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetDailySummariesParams.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetDailySummariesParams.java new file mode 100644 index 00000000..e57c13be --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetDailySummariesParams.java @@ -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 CREATOR = new AutoCreator<>(GetDailySummariesParams.class); +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetDiagnosisKeysDataMappingParams.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetDiagnosisKeysDataMappingParams.java new file mode 100644 index 00000000..9e9293e5 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetDiagnosisKeysDataMappingParams.java @@ -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 CREATOR = new AutoCreator<>(GetDiagnosisKeysDataMappingParams.class); +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetExposureWindowsParams.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetExposureWindowsParams.java new file mode 100644 index 00000000..e4cf4954 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetExposureWindowsParams.java @@ -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 CREATOR = new AutoCreator<>(GetExposureWindowsParams.class); +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetVersionParams.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetVersionParams.java new file mode 100644 index 00000000..696d9377 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/GetVersionParams.java @@ -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 CREATOR = new AutoCreator<>(GetVersionParams.class); +} diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/ProvideDiagnosisKeysParams.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/ProvideDiagnosisKeysParams.java index d291d8c9..f429d712 100644 --- a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/ProvideDiagnosisKeysParams.java +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/ProvideDiagnosisKeysParams.java @@ -27,6 +27,9 @@ public class ProvideDiagnosisKeysParams extends AutoSafeParcelable { @Field(5) public String token; + private ProvideDiagnosisKeysParams() { + } + public ProvideDiagnosisKeysParams(IStatusCallback callback, List keys, List keyFiles, ExposureConfiguration configuration, String token) { this(callback, keyFiles, configuration, token); this.keys = keys; diff --git a/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/SetDiagnosisKeysDataMappingParams.java b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/SetDiagnosisKeysDataMappingParams.java new file mode 100644 index 00000000..cfc868b7 --- /dev/null +++ b/play-services-nearby-api/src/main/java/com/google/android/gms/nearby/exposurenotification/internal/SetDiagnosisKeysDataMappingParams.java @@ -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 CREATOR = new AutoCreator<>(SetDiagnosisKeysDataMappingParams.class); +} diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt index 052da82c..87373f1c 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt @@ -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 + } + } } diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/CleanupService.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/CleanupService.kt index 3303c5f1..fad33772 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/CleanupService.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/CleanupService.kt @@ -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 { diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/Constants.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/Constants.kt index b700aece..ca695aac 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/Constants.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/Constants.kt @@ -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 diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/DeviceInfo.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/DeviceInfo.kt index e16c4c20..5d9927e2 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/DeviceInfo.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/DeviceInfo.kt @@ -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, 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, @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") diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt index a99a2ec7..0790776a 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt @@ -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, database: SQLiteDatabase = writableDatabase) = database.run { + fun batchStoreSingleDiagnosisKey(tid: Long, keys: List, 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, 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() 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() 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() + 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() val poolSize = Runtime.getRuntime().availableProcessors() val executor = ThreadPoolExecutor(poolSize, poolSize, 1, TimeUnit.SECONDS, workQueue) val futures = arrayListOf>() - 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 { - return listMatchedDiagnosisKeys(packageName, token, database).flatMap { findMeasuredExposures(it, database) } + fun finishFileMatching(tid: Long, hash: ByteArray, endTimestamp: Long, keys: List, updates: List, 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() + val poolSize = Runtime.getRuntime().availableProcessors() + val executor = ThreadPoolExecutor(poolSize, poolSize, 1, TimeUnit.SECONDS, workQueue) + val futures = arrayListOf>() + 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 { + return listMatchedSingleDiagnosisKeys(tid, database).flatMap { findMeasuredExposures(it, database) } + } + + fun findAllFileMeasuredExposures(tid: Long, database: SQLiteDatabase = readableDatabase): List { + 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 { 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? = 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() } diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt index f62e2152..6e443fa1 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt @@ -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) + ) }) } } diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt index 331ec5b3..a054a336 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt @@ -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>() 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") diff --git a/proguard.flags b/proguard.flags index 1b6700d5..6cc3f1f2 100644 --- a/proguard.flags +++ b/proguard.flags @@ -28,6 +28,8 @@ # Keep AutoSafeParcelables -keep public class * extends org.microg.safeparcel.AutoSafeParcelable { @org.microg.safeparcel.SafeParceled *; + @org.microg.safeparcel.SafeParcelable.Field *; + (...); } # Keep form data