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;