diff --git a/play-services-nearby-core/src/main/AndroidManifest.xml b/play-services-nearby-core/src/main/AndroidManifest.xml
index e40ecda8..1e597fbb 100644
--- a/play-services-nearby-core/src/main/AndroidManifest.xml
+++ b/play-services-nearby-core/src/main/AndroidManifest.xml
@@ -12,6 +12,7 @@
+
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 d3e0b2e8..b1802a1b 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
@@ -36,7 +36,7 @@ class AdvertiserService : LifecycleService() {
private var looping = false
private var callback: AdvertiseCallback? = null
private val advertiser: BluetoothLeAdvertiser?
- get() = BluetoothAdapter.getDefaultAdapter().bluetoothLeAdvertiser
+ get() = BluetoothAdapter.getDefaultAdapter()?.bluetoothLeAdvertiser
private lateinit var database: ExposureDatabase
private val trigger = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
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 4f435efe..b8b89527 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
@@ -12,15 +12,11 @@ import android.database.sqlite.SQLiteCursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE
import android.database.sqlite.SQLiteOpenHelper
-import android.database.sqlite.SQLiteStatement
-import android.os.Build
import android.os.Parcel
import android.os.Parcelable
-import android.text.TextUtils
import android.util.Log
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
-import org.microg.gms.common.PackageUtils
import java.nio.ByteBuffer
import java.util.*
import java.util.concurrent.Future
@@ -224,7 +220,7 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
val keys = listDiagnosisKeysPendingSearch(packageName, token)
val oldestRpi = oldestRpi
for (key in keys) {
- if (oldestRpi == null || key.rollingStartIntervalNumber * ROLLING_WINDOW_LENGTH_MS - ALLOWED_KEY_OFFSET_MS < oldestRpi) {
+ if (oldestRpi == null || (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)
} else {
@@ -250,50 +246,48 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
}
fun findMeasuredExposures(key: TemporaryExposureKey): List {
- val list = arrayListOf()
val allRpis = key.generateAllRpiIds()
val rpis = (0 until key.rollingPeriod).map { i ->
val pos = i * 16
allRpis.sliceArray(pos until (pos + 16))
}
- val measures = findMeasuredExposures(rpis, key.rollingStartIntervalNumber.toLong() * ROLLING_WINDOW_LENGTH_MS - ALLOWED_KEY_OFFSET_MS, (key.rollingStartIntervalNumber.toLong() + key.rollingPeriod) * ROLLING_WINDOW_LENGTH_MS + ALLOWED_KEY_OFFSET_MS)
- measures.filter {
- val index = rpis.indexOf(it.rpi)
+ val measures = findExposures(rpis, key.rollingStartIntervalNumber.toLong() * ROLLING_WINDOW_LENGTH_MS - ALLOWED_KEY_OFFSET_MS, (key.rollingStartIntervalNumber.toLong() + key.rollingPeriod) * ROLLING_WINDOW_LENGTH_MS + ALLOWED_KEY_OFFSET_MS)
+ return measures.filter {
+ val index = rpis.indexOfFirst { rpi -> rpi.contentEquals(it.rpi) }
val targetTimestamp = (key.rollingStartIntervalNumber + index).toLong() * ROLLING_WINDOW_LENGTH_MS
- it.timestamp > targetTimestamp - ALLOWED_KEY_OFFSET_MS && it.timestamp < targetTimestamp + ALLOWED_KEY_OFFSET_MS
+ it.timestamp >= targetTimestamp - ALLOWED_KEY_OFFSET_MS && it.timestamp <= targetTimestamp + ROLLING_WINDOW_LENGTH_MS + ALLOWED_KEY_OFFSET_MS
}.mapNotNull {
val decrypted = key.cryptAem(it.rpi, it.aem)
if (decrypted[0] == 0x40.toByte() || decrypted[0] == 0x50.toByte()) {
val txPower = decrypted[1]
- it.copy(key = key, notCorrectedAttenuation = txPower - it.rssi)
+ MeasuredExposure(it.timestamp, it.duration, it.rssi, txPower.toInt(), key)
} else {
Log.w(TAG, "Unknown AEM version ${decrypted[0]}, ignoring")
null
}
}
- return list
}
- fun findMeasuredExposures(rpis: List, minTime: Long, maxTime: Long): List = readableDatabase.run {
+ fun findExposures(rpis: List, minTime: Long, maxTime: Long): List = readableDatabase.run {
if (rpis.isEmpty()) return emptyList()
val qs = rpis.map { "?" }.joinToString(",")
queryWithFactory({ _, cursorDriver, editTable, query ->
query.bindLong(1, minTime)
query.bindLong(2, maxTime)
- for (i in (3..(rpis.size + 2))) {
- query.bindBlob(i, rpis[i - 3])
+ rpis.forEachIndexed { index, rpi ->
+ query.bindBlob(index + 3, rpi)
}
SQLiteCursor(cursorDriver, editTable, query)
}, false, TABLE_ADVERTISEMENTS, arrayOf("rpi", "aem", "timestamp", "duration", "rssi"), "timestamp > ? AND timestamp < ? AND rpi IN ($qs)", null, null, null, null, null).use { cursor ->
- val list = arrayListOf()
+ val list = arrayListOf()
while (cursor.moveToNext()) {
- list.add(MeasuredExposure(cursor.getBlob(1), cursor.getBlob(2), cursor.getLong(3), cursor.getLong(4), cursor.getInt(5)))
+ list.add(PlainExposure(cursor.getBlob(0), cursor.getBlob(1), cursor.getLong(2), cursor.getLong(3), cursor.getInt(4)))
}
list
}
}
- fun findMeasuredExposure(rpi: ByteArray, minTime: Long, maxTime: Long): MeasuredExposure? = readableDatabase.run {
+ fun findExposure(rpi: ByteArray, minTime: Long, maxTime: Long): PlainExposure? = readableDatabase.run {
queryWithFactory({ _, cursorDriver, editTable, query ->
query.bindBlob(1, rpi)
query.bindLong(2, minTime)
@@ -301,7 +295,7 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
SQLiteCursor(cursorDriver, editTable, query)
}, false, TABLE_ADVERTISEMENTS, arrayOf("aem", "timestamp", "duration", "rssi"), "rpi = ? AND timestamp > ? AND timestamp < ?", null, null, null, null, null).use { cursor ->
if (cursor.moveToNext()) {
- MeasuredExposure(rpi, cursor.getBlob(0), cursor.getLong(1), cursor.getLong(2), cursor.getInt(3))
+ PlainExposure(rpi, cursor.getBlob(0), cursor.getLong(1), cursor.getLong(2), cursor.getInt(3))
} else {
null
}
@@ -518,4 +512,3 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
}
}
}
-
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 a14bf339..28aac77b 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
@@ -171,7 +171,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
if (totalBytesRead == prefix.size && String(prefix).trim() == "EK Export v1") {
val fileKeys = storeDiagnosisKeyExport(params.token, TemporaryExposureKeyExport.ADAPTER.decode(stream))
- keys + fileKeys
+ keys += fileKeys
} else {
Log.d(TAG, "export.bin had invalid prefix")
}
@@ -209,7 +209,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
intent.putExtra(EXTRA_TOKEN, params.token)
intent.`package` = packageName
Log.d(TAG, "Sending $intent")
- context.sendOrderedBroadcast(intent, PERMISSION_EXPOSURE_CALLBACK)
+ context.sendOrderedBroadcast(intent, null)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
@@ -227,7 +227,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
return@with
}
- val exposures = database.findAllMeasuredExposures(packageName, params.token)
+ 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)
@@ -268,7 +268,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
return@with
}
- val response = database.findAllMeasuredExposures(packageName, params.token).map {
+ val response = database.findAllMeasuredExposures(packageName, params.token).merge().map {
it.toExposureInformation(configuration)
}
diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/MeasuredExposure.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/MeasuredExposure.kt
index 7bb4a324..415baecf 100644
--- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/MeasuredExposure.kt
+++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/MeasuredExposure.kt
@@ -11,19 +11,51 @@ import com.google.android.gms.nearby.exposurenotification.RiskLevel
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import java.util.concurrent.TimeUnit
-data class MeasuredExposure(val rpi: ByteArray, val aem: ByteArray, val timestamp: Long, val duration: Long, val rssi: Int, val notCorrectedAttenuation: Int = 0, val key: TemporaryExposureKey? = null) {
+data class PlainExposure(val rpi: ByteArray, val aem: ByteArray, val timestamp: Long, val duration: Long, val rssi: Int)
+
+data class MeasuredExposure(val timestamp: Long, val duration: Long, val rssi: Int, val txPower: Int, val key: TemporaryExposureKey) {
+ val attenuation
+ get() = txPower - (rssi + currentDeviceInfo.rssiCorrection)
+}
+
+fun List.merge(): List {
+ val keys = map { it.key }.distinct()
+ val result = arrayListOf()
+ for (key in keys) {
+ var merged: MergedExposure? = null
+ for (exposure in filter { it.key == key }.sortedBy { it.timestamp }) {
+ if (merged == null) {
+ merged = MergedExposure(key, exposure.timestamp, listOf(MergedSubExposure(exposure.attenuation, exposure.duration)))
+ } else if (merged.timestamp + MergedExposure.MAXIMUM_DURATION + ROLLING_WINDOW_LENGTH_MS > exposure.timestamp) {
+ merged += exposure
+ }
+ if (merged.durationInMinutes > 30) {
+ result.add(merged)
+ merged = null
+ }
+ }
+ if (merged != null) {
+ result.add(merged)
+ }
+ }
+ return result
+}
+
+internal data class MergedSubExposure(val attenuation: Int, val duration: Long)
+
+data class MergedExposure internal constructor(val key: TemporaryExposureKey, val timestamp: Long, internal val subs: List) {
@RiskLevel
val transmissionRiskLevel: Int
- get() = key?.transmissionRiskLevel ?: RiskLevel.RISK_LEVEL_INVALID
-
+ get() = key.transmissionRiskLevel
+
val durationInMinutes
- get() = TimeUnit.MILLISECONDS.toMinutes(duration)
+ get() = TimeUnit.MILLISECONDS.toMinutes(subs.map { it.duration }.sum())
val daysSinceExposure
get() = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - timestamp)
val attenuation
- get() = notCorrectedAttenuation - currentDeviceInfo.rssiCorrection
+ get() = subs.map { it.attenuation }.min()!!
fun getAttenuationRiskScore(configuration: ExposureConfiguration): Int {
return when {
@@ -83,20 +115,26 @@ data class MeasuredExposure(val rpi: ByteArray, val aem: ByteArray, val timestam
}
fun getAttenuationDurations(configuration: ExposureConfiguration): IntArray {
- return when {
- attenuation < configuration.durationAtAttenuationThresholds[0] -> intArrayOf(durationInMinutes.toInt(), 0, 0)
- attenuation < configuration.durationAtAttenuationThresholds[1] -> intArrayOf(0, durationInMinutes.toInt(), 0)
- else -> intArrayOf(0, 0, durationInMinutes.toInt())
- }
+ return intArrayOf(
+ TimeUnit.MILLISECONDS.toMinutes(subs.filter { it.attenuation < configuration.durationAtAttenuationThresholds[0] }.map { it.duration }.sum()).toInt(),
+ TimeUnit.MILLISECONDS.toMinutes(subs.filter { it.attenuation >= configuration.durationAtAttenuationThresholds[0] && it.attenuation < configuration.durationAtAttenuationThresholds[1] }.map { it.duration }.sum()).toInt(),
+ TimeUnit.MILLISECONDS.toMinutes(subs.filter { it.attenuation >= configuration.durationAtAttenuationThresholds[1] }.map { it.duration }.sum()).toInt()
+ )
}
fun toExposureInformation(configuration: ExposureConfiguration): ExposureInformation =
ExposureInformation.ExposureInformationBuilder()
- .setDateMillisSinceEpoch(timestamp)
+ .setDateMillisSinceEpoch(key.rollingStartIntervalNumber.toLong() * ROLLING_WINDOW_LENGTH_MS)
.setDurationMinutes(durationInMinutes.toInt())
.setAttenuationValue(attenuation)
.setTransmissionRiskLevel(transmissionRiskLevel)
.setTotalRiskScore(getRiskScore(configuration))
.setAttenuationDurations(getAttenuationDurations(configuration))
.build()
+
+ operator fun plus(exposure: MeasuredExposure): MergedExposure = copy(subs = subs + MergedSubExposure(exposure.attenuation, exposure.duration))
+
+ companion object {
+ const val MAXIMUM_DURATION = 30 * 60 * 1000
+ }
}
diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt
index 470beca7..0e7a2ef0 100644
--- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt
+++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt
@@ -57,7 +57,7 @@ class ScannerService : Service() {
}
private val scanner: BluetoothLeScanner?
- get() = getDefaultAdapter().bluetoothLeScanner
+ get() = getDefaultAdapter()?.bluetoothLeScanner
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
diff --git a/play-services-nearby-core/src/test/java/org/microg/gms/nearby/exposurenotification/CryptoTest.java b/play-services-nearby-core/src/test/java/org/microg/gms/nearby/exposurenotification/CryptoTest.java
index 8b9b78a4..dc92ffbe 100644
--- a/play-services-nearby-core/src/test/java/org/microg/gms/nearby/exposurenotification/CryptoTest.java
+++ b/play-services-nearby-core/src/test/java/org/microg/gms/nearby/exposurenotification/CryptoTest.java
@@ -8,7 +8,6 @@ package org.microg.gms.nearby.exposurenotification;
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey;
-import junit.framework.Test;
import junit.framework.TestCase;
import org.junit.Assert;