Update EN API

This commit is contained in:
Marvin W 2020-08-18 23:54:14 +02:00
parent e1bb395ff8
commit aea55a5c90
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
6 changed files with 169 additions and 42 deletions

View File

@ -45,7 +45,7 @@ class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() {
val (totalRpiCount, rpiHistogram) = withContext(Dispatchers.IO) {
ExposureDatabase.with(requireContext()) { database ->
val map = linkedMapOf<String, Float>()
val lowestDate = Math.round((Date().time / 24 / 60 / 60 / 1000 - 13).toDouble()) * 24 * 60 * 60 * 1000
val lowestDate = Math.round((System.currentTimeMillis() / 24 / 60 / 60 / 1000 - 13).toDouble()) * 24 * 60 * 60 * 1000
for (i in 0..13) {
val date = Calendar.getInstance().apply { this.time = Date(lowestDate + i * 24 * 60 * 60 * 1000) }.get(Calendar.DAY_OF_MONTH)
val str = when (i) {
@ -58,6 +58,7 @@ class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() {
val refDateHigh = Calendar.getInstance().apply { this.time = Date(lowestDate + 13 * 24 * 60 * 60 * 1000) }.get(Calendar.DAY_OF_MONTH)
for (entry in database.rpiHistogram) {
val time = Date(entry.key * 24 * 60 * 60 * 1000)
if (time.time < lowestDate) continue // Ignore old data
val date = Calendar.getInstance().apply { this.time = time }.get(Calendar.DAY_OF_MONTH)
val str = when (date) {
refDateLow, refDateHigh -> DateFormat.format(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMd"), entry.key * 24 * 60 * 60 * 1000).toString()

View File

@ -30,9 +30,6 @@
<receiver android:name="org.microg.gms.nearby.exposurenotification.ServiceTrigger">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.AIRPLANE_MODE" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="android.net.conn.BACKGROUND_DATA_SETTING_CHANGED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_RESTARTED" />

View File

@ -9,7 +9,10 @@ import android.annotation.TargetApi
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.*
import android.bluetooth.le.AdvertiseSettings.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.util.Log
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
@ -25,10 +28,27 @@ import kotlin.coroutines.suspendCoroutine
@TargetApi(21)
class AdvertiserService : LifecycleService() {
private val version = VERSION_1_0
private var looping = false
private var callback: AdvertiseCallback? = null
private val advertiser: BluetoothLeAdvertiser
private val advertiser: BluetoothLeAdvertiser?
get() = BluetoothAdapter.getDefaultAdapter().bluetoothLeAdvertiser
private lateinit var database: ExposureDatabase
private val trigger = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "android.bluetooth.adapter.action.STATE_CHANGED") {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
BluetoothAdapter.STATE_TURNING_OFF, BluetoothAdapter.STATE_OFF -> stopAdvertising()
BluetoothAdapter.STATE_ON -> {
if (looping) {
lifecycleScope.launchWhenStarted { restartAdvertising() }
} else {
loopAdvertising()
}
}
}
}
}
}
private suspend fun BluetoothLeAdvertiser.startAdvertising(settings: AdvertiseSettings, advertiseData: AdvertiseData): AdvertiseCallback = suspendCoroutine {
startAdvertising(settings, advertiseData, object : AdvertiseCallback() {
@ -45,12 +65,13 @@ class AdvertiserService : LifecycleService() {
override fun onCreate() {
super.onCreate()
database = ExposureDatabase.ref(this)
registerReceiver(trigger, IntentFilter().also { it.addAction("android.bluetooth.adapter.action.STATE_CHANGED") })
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
if (ExposurePreferences(this).advertiserEnabled) {
startAdvertising()
loopAdvertising()
} else {
stopSelf()
}
@ -59,40 +80,63 @@ class AdvertiserService : LifecycleService() {
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(trigger)
stopAdvertising()
database.unref()
}
fun startAdvertising() {
@Synchronized
fun loopAdvertising() {
if (looping) return
looping = true
lifecycleScope.launchWhenStarted {
do {
val aem = when (version) {
VERSION_1_0 -> byteArrayOf(
version, // Version and flags
(currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
0x00, // Reserved
0x00 // Reserved
)
VERSION_1_1 -> byteArrayOf(
(version + currentDeviceInfo.confidence * 4).toByte(), // Version and flags
(currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
0x00, // Reserved
0x00 // Reserved
)
else -> return@launchWhenStarted
}
val payload = database.generateCurrentPayload(aem)
var nextSend = nextKeyMillis.coerceAtMost(180000)
startAdvertising(payload, nextSend.toInt())
delay(nextSend)
} while (callback != null)
Log.d(TAG, "Looping advertising")
try {
do {
val aem = when (version) {
VERSION_1_0 -> byteArrayOf(
version, // Version and flags
(currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
0x00, // Reserved
0x00 // Reserved
)
VERSION_1_1 -> byteArrayOf(
(version + currentDeviceInfo.confidence * 4).toByte(), // Version and flags
(currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
0x00, // Reserved
0x00 // Reserved
)
else -> return@launchWhenStarted
}
val payload = database.generateCurrentPayload(aem)
var nextSend = nextKeyMillis.coerceAtMost(180000)
startAdvertising(payload, nextSend.toInt())
if (callback != null) delay(nextSend)
} while (callback != null)
} catch (e: Exception) {
Log.w(TAG, "Error during advertising loop", e)
}
Log.d(TAG, "No longer advertising")
synchronized(this@AdvertiserService) {
looping = false
}
}
}
var startTime = System.currentTimeMillis()
var sendingBytes = ByteArray(0)
var sendingNext = 0
suspend fun startAdvertising(bytes: ByteArray, nextSend: Int) {
startTime = System.currentTimeMillis()
sendingBytes = bytes
sendingNext = nextSend
continueAdvertising(bytes, nextSend)
}
private suspend fun continueAdvertising(bytes: ByteArray, nextSend: Int) {
stopAdvertising()
val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, bytes).build()
val settings = AdvertiseSettings.Builder()
val settings = Builder()
.setTimeout(nextSend)
.setAdvertiseMode(ADVERTISE_MODE_LOW_POWER)
.setTxPowerLevel(ADVERTISE_TX_POWER_MEDIUM)
@ -100,18 +144,42 @@ class AdvertiserService : LifecycleService() {
.build()
val (uuid, aem) = ByteBuffer.wrap(bytes).let { UUID(it.long, it.long) to it.int }
Log.d(TAG, "RPI: $uuid, Version: 0x${version.toString(16)}, TX Power: ${currentDeviceInfo.txPowerCorrection + TX_POWER_LOW}, AEM: 0x${aem.toLong().let { if (it < 0) 0x100000000L + it else it }.toString(16)}, Timeout: ${nextSend}ms")
callback = advertiser.startAdvertising(settings, data)
callback = advertiser?.startAdvertising(settings, data)
}
suspend fun restartAdvertising() {
val startTime = startTime
val bytes = sendingBytes
val next = sendingNext
if (next == 0 || bytes.isEmpty()) return
val nextSend = (startTime - System.currentTimeMillis() + next).toInt()
if (nextSend < 5000) return
continueAdvertising(bytes, nextSend)
}
override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
writer?.println("Looping: $looping")
writer?.println("Active: ${callback != null}")
writer?.println("Currently advertising: ${database.currentRpiId}")
writer?.println("Next key change in ${nextKeyMillis}ms")
try {
val startTime = startTime
val bytes = sendingBytes
val (uuid, aem) = ByteBuffer.wrap(bytes).let { UUID(it.long, it.long) to it.int }
writer?.println("""
Last advertising:
Since: ${Date(startTime)}
RPI: $uuid
Version: 0x${version.toString(16)}
TX Power: ${currentDeviceInfo.txPowerCorrection + TX_POWER_LOW}
AEM: 0x${aem.toLong().let { if (it < 0) 0x100000000L + it else it }.toString(16)}
""".trimIndent())
} catch (e: Exception) {
writer?.println("Last advertising: ${e.message ?: e.toString()}")
}
}
@Synchronized
fun stopAdvertising() {
callback?.let { advertiser.stopAdvertising(it) }
callback?.let { advertiser?.stopAdvertising(it) }
callback = null
}
}

View File

@ -23,8 +23,8 @@ val currentDeviceInfo: DeviceInfo
get() {
var deviceInfo = knownDeviceInfo
if (deviceInfo == null) {
val byOem = allDeviceInfos.filter { it.oem == Build.MANUFACTURER }
val exactMatch = byOem.find { it.model == Build.MODEL }
val byOem = allDeviceInfos.filter { it.oem.equalsIgnoreCase(Build.MANUFACTURER) }
val exactMatch = byOem.find { it.model.equalsIgnoreCase(Build.MODEL) }
deviceInfo = when {
exactMatch != null -> {
// Exact match
@ -45,6 +45,9 @@ val currentDeviceInfo: DeviceInfo
return deviceInfo
}
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private fun String.equalsIgnoreCase(other: String): Boolean = (this as java.lang.String).equalsIgnoreCase(other)
/*
* Derived from en-calibration-2020-06-13.csv published via
* https://developers.google.com/android/exposure-notifications/ble-attenuation-computation#device-list

View File

@ -8,42 +8,86 @@ package org.microg.gms.nearby.exposurenotification
import android.annotation.TargetApi
import android.app.Service
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothAdapter.*
import android.bluetooth.le.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.IBinder
import android.util.Log
import java.io.FileDescriptor
import java.io.PrintWriter
import java.util.*
@TargetApi(21)
class ScannerService : Service() {
private var started = false
private var startTime = 0L
private var seenAdvertisements = 0L
private lateinit var database: ExposureDatabase
private val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
val data = result?.scanRecord?.serviceData?.get(SERVICE_UUID) ?: return
if (data.size < 16) return // Ignore invalid advertisements
database.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi)
result?.let { onScanResult(it) }
}
override fun onBatchScanResults(results: MutableList<ScanResult>) {
Log.d(TAG, "onBatchScanResults: ${results.size}")
for (result in results) {
onScanResult(result)
}
}
override fun onScanFailed(errorCode: Int) {
Log.w(TAG, "onScanFailed: $errorCode")
stopScan()
}
}
private val scanner: BluetoothLeScanner
get() = BluetoothAdapter.getDefaultAdapter().bluetoothLeScanner
private val trigger = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "android.bluetooth.adapter.action.STATE_CHANGED") {
when (intent.getIntExtra(EXTRA_STATE, -1)) {
STATE_TURNING_OFF, STATE_OFF -> stopScan()
STATE_ON -> startScanIfNeeded()
}
}
}
}
private val scanner: BluetoothLeScanner?
get() = getDefaultAdapter().bluetoothLeScanner
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
startScanIfNeeded()
return START_STICKY
}
fun onScanResult(result: ScanResult) {
val data = result.scanRecord?.serviceData?.get(SERVICE_UUID) ?: return
if (data.size < 16) return // Ignore invalid advertisements
database.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi)
seenAdvertisements++
}
fun startScanIfNeeded() {
if (ExposurePreferences(this).scannerEnabled) {
startScan()
} else {
stopSelf()
}
return START_STICKY
}
override fun onCreate() {
super.onCreate()
database = ExposureDatabase.ref(this)
registerReceiver(trigger, IntentFilter().also { it.addAction("android.bluetooth.adapter.action.STATE_CHANGED") })
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(trigger)
stopScan()
database.unref()
}
@ -55,6 +99,8 @@ class ScannerService : Service() {
@Synchronized
private fun startScan() {
if (started) return
val scanner = scanner ?: return
Log.d(TAG, "Starting scanner for service $SERVICE_UUID")
scanner.startScan(
listOf(ScanFilter.Builder()
.setServiceUuid(SERVICE_UUID)
@ -66,12 +112,22 @@ class ScannerService : Service() {
callback
)
started = true
startTime = System.currentTimeMillis()
}
@Synchronized
private fun stopScan() {
if (!started) return
scanner.stopScan(callback)
Log.d(TAG, "Stopping scanner for service $SERVICE_UUID")
started = false
scanner?.stopScan(callback)
}
override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
writer?.println("Started: $started")
if (started) {
writer?.println("Since ${Date(startTime)}")
writer?.println("Seen advertisements: $seenAdvertisements")
}
}
}

View File

@ -9,11 +9,13 @@ import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import org.microg.gms.common.ForegroundServiceContext
class ServiceTrigger : BroadcastReceiver() {
@SuppressLint("UnsafeProtectedBroadcastReceiver")
override fun onReceive(context: Context, intent: Intent?) {
Log.d(TAG, "ServiceTrigger: $intent")
if (ExposurePreferences(context).scannerEnabled) {
ForegroundServiceContext(context).startService(Intent(context, ScannerService::class.java))
}