mirror of
https://github.com/YTVanced/VancedMicroG
synced 2024-11-30 23:23:01 +00:00
Update EN API
This commit is contained in:
parent
e1bb395ff8
commit
aea55a5c90
6 changed files with 169 additions and 42 deletions
|
@ -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()
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue