EN: Make internal structures closer to ExposureWindow mode

This commit is contained in:
Marvin W 2020-09-04 00:13:11 +02:00
parent ec877f7a53
commit 876e32acd5
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
7 changed files with 69 additions and 38 deletions

View File

@ -12,6 +12,7 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="com.google.android.gms.nearby.exposurenotification.EXPOSURE_CALLBACK" />
<application>

View File

@ -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?) {

View File

@ -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<MeasuredExposure> {
val list = arrayListOf<MeasuredExposure>()
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<ByteArray>, minTime: Long, maxTime: Long): List<MeasuredExposure> = readableDatabase.run {
fun findExposures(rpis: List<ByteArray>, minTime: Long, maxTime: Long): List<PlainExposure> = 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<MeasuredExposure>()
val list = arrayListOf<PlainExposure>()
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
}
}
}

View File

@ -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)
}

View File

@ -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<MeasuredExposure>.merge(): List<MergedExposure> {
val keys = map { it.key }.distinct()
val result = arrayListOf<MergedExposure>()
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<MergedSubExposure>) {
@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
}
}

View File

@ -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)

View File

@ -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;