/* * 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) { 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?) { 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 } } } }