mirror of https://github.com/YTVanced/VancedMicroG
191 lines
7.2 KiB
Kotlin
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
|
|
}
|
|
}
|
|
}
|
|
}
|