VancedMicroG/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt

191 lines
7.2 KiB
Kotlin

/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.AlarmManager
import android.app.PendingIntent
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.*
import android.util.Log
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import org.microg.gms.common.ForegroundServiceContext
import org.microg.gms.common.ForegroundServiceInfo
import java.io.FileDescriptor
import java.io.PrintWriter
import java.util.*
@TargetApi(21)
@ForegroundServiceInfo("Exposure Notification")
class ScannerService : LifecycleService() {
private var scanning = false
private var lastStartTime = 0L
private var seenAdvertisements = 0L
private var lastAdvertisement = 0L
private val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let { onScanResult(it) }
}
override fun onBatchScanResults(results: MutableList<ScanResult>) {
for (result in results) {
onScanResult(result)
}
}
override fun onScanFailed(errorCode: Int) {
Log.w(TAG, "onScanFailed: $errorCode")
stopScan()
}
}
private val trigger = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == ACTION_STATE_CHANGED) {
when (intent.getIntExtra(EXTRA_STATE, -1)) {
STATE_TURNING_OFF, STATE_OFF -> stopScan()
STATE_ON -> startScanIfNeeded()
}
}
}
}
private val handler = Handler(Looper.getMainLooper())
private val stopLaterRunnable = Runnable { stopScan() }
// 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?
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 {
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
Log.d(TAG, "ScannerService.start: $intent")
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
seenAdvertisements++
lastAdvertisement = System.currentTimeMillis()
lifecycleScope.launchWhenStarted {
ExposureDatabase.with(this@ScannerService) { database ->
database.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi)
}
}
}
fun startScanIfNeeded() {
if (ExposurePreferences(this).enabled) {
startScan()
} else {
stopSelf()
}
}
override fun onCreate() {
super.onCreate()
registerReceiver(trigger, IntentFilter().apply { addAction(ACTION_STATE_CHANGED) })
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(trigger)
stopScan()
}
@SuppressLint("WakelockTimeout")
@Synchronized
private fun startScan() {
if (scanning) return
val scanner = scanner ?: return
Log.i(TAG, "Starting scanner for service $SERVICE_UUID for ${SCANNING_TIME_MS}ms")
seenAdvertisements = 0
wakeLock.acquire()
try {
scanner.startScan(
listOf(ScanFilter.Builder()
.setServiceUuid(SERVICE_UUID)
.setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0))
.build()),
ScanSettings.Builder().build(),
callback
)
} catch (e: SecurityException) {
Log.e(TAG, "Couldn't start ScannerService, need android.permission.BLUETOOTH_SCAN permission.")
}
scanning = true
lastStartTime = System.currentTimeMillis()
handler.postDelayed(stopLaterRunnable, SCANNING_TIME_MS)
}
@Synchronized
private fun stopScan() {
if (!scanning) return
Log.i(TAG, "Stopping scanner for service $SERVICE_UUID, had seen $seenAdvertisements advertisements")
handler.removeCallbacks(stopLaterRunnable)
scanning = false
scanner?.stopScan(callback)
if (ExposurePreferences(this).enabled) {
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)
}
}
override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
writer?.println("Scanning now: $scanning")
writer?.println("Last scan start: ${Date(lastStartTime)}")
if (Build.VERSION.SDK_INT >= 29) {
writer?.println("Scan stop pending: ${handler.hasCallbacks(stopLaterRunnable)}")
}
writer?.println("Seen advertisements since last scan start: $seenAdvertisements")
writer?.println("Last advertisement seen: ${Date(lastAdvertisement)}")
}
companion object {
fun isNeeded(context: Context): Boolean {
return ExposurePreferences(context).enabled
}
fun isSupported(context: Context): Boolean? {
val adapter = getDefaultAdapter()
return when {
adapter == null -> false
adapter.state != STATE_ON -> null
adapter.bluetoothLeScanner != null -> true
else -> false
}
}
}
}