0
0
Fork 0
mirror of https://github.com/YTVanced/VancedMicroG synced 2024-11-30 23:23:01 +00:00

EN: Support SDK 26+ AdvertisingSet, use scheduled alarms for improved scanning in idle

This commit is contained in:
Marvin W 2020-09-12 21:51:37 +02:00
parent f10214ef8a
commit da9a3e714d
No known key found for this signature in database
GPG key ID: 072E9235DB996F2A
3 changed files with 168 additions and 125 deletions

View file

@ -6,66 +6,61 @@
package org.microg.gms.nearby.exposurenotification package org.microg.gms.nearby.exposurenotification
import android.annotation.TargetApi import android.annotation.TargetApi
import android.app.AlarmManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_ONE_SHOT
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.Service
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.AdvertiseCallback import android.bluetooth.le.*
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.bluetooth.le.AdvertiseSettings.* import android.bluetooth.le.AdvertiseSettings.*
import android.bluetooth.le.BluetoothLeAdvertiser
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.*
import android.util.Log import android.util.Log
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import org.microg.gms.common.ForegroundServiceContext import org.microg.gms.common.ForegroundServiceContext
import java.io.FileDescriptor import java.io.FileDescriptor
import java.io.PrintWriter import java.io.PrintWriter
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.* import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
@TargetApi(21) @TargetApi(21)
class AdvertiserService : LifecycleService() { class AdvertiserService : Service() {
private val version = VERSION_1_0 private val version = VERSION_1_0
private var looping = false private var advertising = false
private var callback: AdvertiseCallback? = null private var wantStartAdvertising = false
private val advertiser: BluetoothLeAdvertiser? private val advertiser: BluetoothLeAdvertiser?
get() = BluetoothAdapter.getDefaultAdapter()?.bluetoothLeAdvertiser get() = BluetoothAdapter.getDefaultAdapter()?.bluetoothLeAdvertiser
private val alarmManager: AlarmManager
get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
private lateinit var database: ExposureDatabase private lateinit var database: ExposureDatabase
private val callback: AdvertiseCallback = object : AdvertiseCallback() {
override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
Log.d(TAG, "Advertising active for ${settingsInEffect?.timeout}ms")
}
override fun onStartFailure(errorCode: Int) {
Log.w(TAG, "Advertising failed: $errorCode")
stopOrRestartAdvertising()
}
}
@TargetApi(23)
private var setCallback: AdvertisingSetCallback? = null
private val trigger = object : BroadcastReceiver() { private val trigger = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "android.bluetooth.adapter.action.STATE_CHANGED") { if (intent?.action == "android.bluetooth.adapter.action.STATE_CHANGED") {
when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) { when (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)) {
BluetoothAdapter.STATE_TURNING_OFF, BluetoothAdapter.STATE_OFF -> stopAdvertising() BluetoothAdapter.STATE_TURNING_OFF, BluetoothAdapter.STATE_OFF -> stopOrRestartAdvertising()
BluetoothAdapter.STATE_ON -> { BluetoothAdapter.STATE_ON -> startAdvertisingIfNeeded()
if (looping) {
lifecycleScope.launchWhenStarted { restartAdvertising() }
} else {
loopAdvertising()
}
}
} }
} }
} }
} }
private val handler = Handler(Looper.getMainLooper())
private suspend fun BluetoothLeAdvertiser.startAdvertising(settings: AdvertiseSettings, advertiseData: AdvertiseData): AdvertiseCallback = suspendCoroutine { private val startLaterRunnable = Runnable { startAdvertisingIfNeeded() }
startAdvertising(settings, advertiseData, object : AdvertiseCallback() {
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
it.resume(this)
}
override fun onStartFailure(errorCode: Int) {
it.resumeWithException(RuntimeException("Error code: $errorCode"))
}
})
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -76,10 +71,10 @@ class AdvertiserService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
ForegroundServiceContext.completeForegroundService(this, intent, TAG) ForegroundServiceContext.completeForegroundService(this, intent, TAG)
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
if (ExposurePreferences(this).enabled) { if (intent?.action == ACTION_RESTART_ADVERTISING && advertising) {
loopAdvertising() stopOrRestartAdvertising()
} else { } else {
stopSelf() startAdvertisingIfNeeded()
} }
return START_STICKY return START_STICKY
} }
@ -87,87 +82,79 @@ class AdvertiserService : LifecycleService() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
unregisterReceiver(trigger) unregisterReceiver(trigger)
stopAdvertising() stopOrRestartAdvertising()
database.unref() database.unref()
} }
@Synchronized override fun onBind(intent: Intent?): IBinder? {
fun loopAdvertising() { return null
if (looping) return }
looping = true
lifecycleScope.launchWhenStarted { fun startAdvertisingIfNeeded() {
Log.d(TAG, "Looping advertising") if (ExposurePreferences(this).enabled) {
try { startAdvertising()
do { } else {
val aem = when (version) { stopSelf()
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)
val nextSend = (nextKeyMillis + Random.nextInt(-ADVERTISER_OFFSET, ADVERTISER_OFFSET)).coerceIn(0, 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() private var lastStartTime = System.currentTimeMillis()
var sendingBytes = ByteArray(0) private 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) { @Synchronized
stopAdvertising() fun startAdvertising() {
val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, bytes).build() if (advertising) return
val settings = Builder() val advertiser = advertiser ?: return
.setTimeout(nextSend) wantStartAdvertising = false
.setAdvertiseMode(ADVERTISE_MODE_LOW_POWER) val aemBytes = when (version) {
.setTxPowerLevel(ADVERTISE_TX_POWER_MEDIUM) VERSION_1_0 -> byteArrayOf(
.setConnectable(false) version, // Version and flags
.build() (currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
val (uuid, aem) = ByteBuffer.wrap(bytes).let { UUID(it.long, it.long) to it.int } 0x00, // Reserved
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") 0x00 // Reserved
callback = advertiser?.startAdvertising(settings, data) )
} VERSION_1_1 -> byteArrayOf(
(version + currentDeviceInfo.confidence * 4).toByte(), // Version and flags
suspend fun restartAdvertising() { (currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
val startTime = startTime 0x00, // Reserved
val bytes = sendingBytes 0x00 // Reserved
val next = sendingNext )
if (next == 0 || bytes.isEmpty()) return else -> return
val nextSend = (startTime - System.currentTimeMillis() + next).toInt() }
if (nextSend < 5000) return var nextSend = nextKeyMillis.coerceAtLeast(10000)
continueAdvertising(bytes, nextSend) val payload = database.generateCurrentPayload(aemBytes)
val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, payload).build()
val (uuid, _) = ByteBuffer.wrap(payload).let { UUID(it.long, it.long) to it.int }
Log.i(TAG, "Starting advertiser for RPI $uuid")
if (Build.VERSION.SDK_INT >= 26) {
setCallback = SetCallback()
val params = AdvertisingSetParameters.Builder()
.setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM)
.setLegacyMode(true)
.setTxPowerLevel(AdvertisingSetParameters.TX_POWER_LOW)
.setConnectable(false)
.build()
advertiser.startAdvertisingSet(params, data, null, null, null, setCallback)
} else {
nextSend = nextSend.coerceAtMost(180000)
val settings = Builder()
.setTimeout(nextSend.toInt())
.setAdvertiseMode(ADVERTISE_MODE_BALANCED)
.setTxPowerLevel(ADVERTISE_TX_POWER_LOW)
.setConnectable(false)
.build()
advertiser.startAdvertising(settings, data, callback)
}
advertising = true
sendingBytes = payload
lastStartTime = System.currentTimeMillis()
scheduleRestartAdvertising(nextSend)
} }
override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) { override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
writer?.println("Looping: $looping") writer?.println("Advertising: $advertising")
writer?.println("Active: ${callback != null}")
try { try {
val startTime = startTime val startTime = lastStartTime
val bytes = sendingBytes val bytes = sendingBytes
val (uuid, aem) = ByteBuffer.wrap(bytes).let { UUID(it.long, it.long) to it.int } val (uuid, aem) = ByteBuffer.wrap(bytes).let { UUID(it.long, it.long) to it.int }
writer?.println(""" writer?.println("""
@ -183,15 +170,53 @@ class AdvertiserService : LifecycleService() {
} }
} }
private fun scheduleRestartAdvertising(nextSend: Long) {
val intent = Intent(this, AdvertiserService::class.java).apply { action = ACTION_RESTART_ADVERTISING }
val pendingIntent = PendingIntent.getService(this, ACTION_RESTART_ADVERTISING.hashCode(), intent, FLAG_ONE_SHOT and FLAG_UPDATE_CURRENT)
when {
Build.VERSION.SDK_INT >= 23 ->
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextSend, pendingIntent)
else ->
alarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextSend, pendingIntent)
}
}
@Synchronized @Synchronized
fun stopAdvertising() { fun stopOrRestartAdvertising() {
callback?.let { advertiser?.stopAdvertising(it) } if (!advertising) return
callback = null val (uuid, _) = ByteBuffer.wrap(sendingBytes).let { UUID(it.long, it.long) to it.int }
Log.i(TAG, "Stopping advertiser for RPI $uuid")
advertising = false
if (Build.VERSION.SDK_INT >= 26) {
wantStartAdvertising = true
advertiser?.stopAdvertisingSet(setCallback)
} else {
advertiser?.stopAdvertising(callback)
}
handler.postDelayed(startLaterRunnable, 1000)
} }
companion object { companion object {
private const val ACTION_RESTART_ADVERTISING = "org.microg.gms.nearby.exposurenotification.RESTART_ADVERTISING"
fun isNeeded(context: Context): Boolean { fun isNeeded(context: Context): Boolean {
return ExposurePreferences(context).enabled return ExposurePreferences(context).enabled
} }
} }
@TargetApi(26)
inner class SetCallback : AdvertisingSetCallback() {
override fun onAdvertisingSetStarted(advertisingSet: AdvertisingSet?, txPower: Int, status: Int) {
Log.d(TAG, "Advertising active, status=$status")
}
override fun onAdvertisingSetStopped(advertisingSet: AdvertisingSet?) {
Log.d(TAG, "Advertising stopped")
if (wantStartAdvertising) {
startAdvertisingIfNeeded()
} else {
stopOrRestartAdvertising()
}
}
}
} }

View file

@ -39,7 +39,7 @@ val currentDeviceInfo: DeviceInfo
averageCurrentDeviceInfo(Build.MANUFACTURER, Build.MODEL, allDeviceInfos, CONFIDENCE_LOWEST) averageCurrentDeviceInfo(Build.MANUFACTURER, Build.MODEL, allDeviceInfos, CONFIDENCE_LOWEST)
} }
} }
Log.d(TAG, "Selected $deviceInfo") Log.i(TAG, "Selected $deviceInfo")
knownDeviceInfo = deviceInfo knownDeviceInfo = deviceInfo
} }
return deviceInfo return deviceInfo

View file

@ -5,7 +5,10 @@
package org.microg.gms.nearby.exposurenotification package org.microg.gms.nearby.exposurenotification
import android.annotation.SuppressLint
import android.annotation.TargetApi import android.annotation.TargetApi
import android.app.AlarmManager
import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.bluetooth.BluetoothAdapter.* import android.bluetooth.BluetoothAdapter.*
import android.bluetooth.le.* import android.bluetooth.le.*
@ -13,10 +16,7 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Build import android.os.*
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log import android.util.Log
import org.microg.gms.common.ForegroundServiceContext import org.microg.gms.common.ForegroundServiceContext
import java.io.FileDescriptor import java.io.FileDescriptor
@ -36,7 +36,6 @@ class ScannerService : Service() {
} }
override fun onBatchScanResults(results: MutableList<ScanResult>) { override fun onBatchScanResults(results: MutableList<ScanResult>) {
Log.d(TAG, "onBatchScanResults: ${results.size}")
for (result in results) { for (result in results) {
onScanResult(result) onScanResult(result)
} }
@ -59,10 +58,19 @@ class ScannerService : Service() {
} }
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val stopLaterRunnable = Runnable { stopScan() } private val stopLaterRunnable = Runnable { stopScan() }
private val startLaterRunnable = Runnable { startScan() }
// Wake lock for the duration of scan. Otherwise we might fall asleep while scanning
// resulting in potentially very long scan times
private val wakeLock: PowerManager.WakeLock by lazy {
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, ScannerService::class.java.canonicalName).apply { setReferenceCounted(false) }
}
private val scanner: BluetoothLeScanner? private val scanner: BluetoothLeScanner?
get() = getDefaultAdapter()?.bluetoothLeScanner get() = getDefaultAdapter()?.bluetoothLeScanner
private val alarmManager: AlarmManager
get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
private val powerManager: PowerManager
get() = getSystemService(Context.POWER_SERVICE) as PowerManager
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
ForegroundServiceContext.completeForegroundService(this, intent, TAG) ForegroundServiceContext.completeForegroundService(this, intent, TAG)
@ -104,21 +112,20 @@ class ScannerService : Service() {
return null return null
} }
@SuppressLint("WakelockTimeout")
@Synchronized @Synchronized
private fun startScan() { private fun startScan() {
if (scanning) return if (scanning) return
val scanner = scanner ?: return val scanner = scanner ?: return
Log.d(TAG, "Starting scanner for service $SERVICE_UUID") Log.i(TAG, "Starting scanner for service $SERVICE_UUID for ${SCANNING_TIME_MS}ms")
handler.removeCallbacks(startLaterRunnable)
seenAdvertisements = 0 seenAdvertisements = 0
wakeLock.acquire()
scanner.startScan( scanner.startScan(
listOf(ScanFilter.Builder() listOf(ScanFilter.Builder()
.setServiceUuid(SERVICE_UUID) .setServiceUuid(SERVICE_UUID)
.setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0)) .setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0))
.build()), .build()),
ScanSettings.Builder() ScanSettings.Builder().build(),
.let { if (Build.VERSION.SDK_INT >= 23) it.setMatchMode(ScanSettings.MATCH_MODE_STICKY) else it }
.build(),
callback callback
) )
scanning = true scanning = true
@ -129,12 +136,24 @@ class ScannerService : Service() {
@Synchronized @Synchronized
private fun stopScan() { private fun stopScan() {
if (!scanning) return if (!scanning) return
Log.d(TAG, "Stopping scanner for service $SERVICE_UUID") Log.i(TAG, "Stopping scanner for service $SERVICE_UUID, had seen $seenAdvertisements advertisements")
handler.removeCallbacks(stopLaterRunnable) handler.removeCallbacks(stopLaterRunnable)
scanning = false scanning = false
scanner?.stopScan(callback) scanner?.stopScan(callback)
if (ExposurePreferences(this).enabled) { if (ExposurePreferences(this).enabled) {
handler.postDelayed(startLaterRunnable, ((lastStartTime + SCANNING_INTERVAL_MS) - System.currentTimeMillis()).coerceIn(0, SCANNING_INTERVAL_MS)) scheduleStartScan(((lastStartTime + SCANNING_INTERVAL_MS) - System.currentTimeMillis()).coerceIn(0, SCANNING_INTERVAL_MS))
}
wakeLock.release()
}
private fun scheduleStartScan(nextScan: Long) {
val intent = Intent(this, ScannerService::class.java)
val pendingIntent = PendingIntent.getService(this, ScannerService::class.java.hashCode(), intent, PendingIntent.FLAG_ONE_SHOT and PendingIntent.FLAG_UPDATE_CURRENT)
if (Build.VERSION.SDK_INT >= 23) {
// Note: there is no setWindowAndAllowWhileIdle()
alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextScan, pendingIntent)
} else {
alarmManager.setWindow(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + nextScan - SCANNING_TIME_MS / 2, SCANNING_TIME_MS, pendingIntent)
} }
} }
@ -142,7 +161,6 @@ class ScannerService : Service() {
writer?.println("Scanning now: $scanning") writer?.println("Scanning now: $scanning")
writer?.println("Last scan start: ${Date(lastStartTime)}") writer?.println("Last scan start: ${Date(lastStartTime)}")
if (Build.VERSION.SDK_INT >= 29) { if (Build.VERSION.SDK_INT >= 29) {
writer?.println("Scan start pending: ${handler.hasCallbacks(startLaterRunnable)}")
writer?.println("Scan stop pending: ${handler.hasCallbacks(stopLaterRunnable)}") writer?.println("Scan stop pending: ${handler.hasCallbacks(stopLaterRunnable)}")
} }
writer?.println("Seen advertisements since last scan start: $seenAdvertisements") writer?.println("Seen advertisements since last scan start: $seenAdvertisements")