AdvertiserService.kt 9.67 KB
Newer Older
1
2
3
4
5
6
7
8
/*
 * SPDX-FileCopyrightText: 2020, microG Project Team
 * SPDX-License-Identifier: Apache-2.0
 */

package org.microg.gms.nearby.exposurenotification

import android.annotation.TargetApi
9
10
11
12
import android.app.AlarmManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_ONE_SHOT
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
13
import android.bluetooth.BluetoothAdapter.*
14
import android.bluetooth.le.*
15
import android.bluetooth.le.AdvertiseSettings.*
Marvin W.'s avatar
Marvin W. committed
16
17
import android.content.BroadcastReceiver
import android.content.Context
18
import android.content.Intent
Marvin W.'s avatar
Marvin W. committed
19
import android.content.IntentFilter
20
21
22
23
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
Marvin W.'s avatar
Marvin W. committed
24
import android.util.Log
25
26
27
28
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
29
import org.microg.gms.common.ForegroundServiceContext
30
import org.microg.gms.common.ForegroundServiceInfo
31
32
import java.io.FileDescriptor
import java.io.PrintWriter
Marvin W.'s avatar
Marvin W. committed
33
34
import java.nio.ByteBuffer
import java.util.*
35
36

@TargetApi(21)
37
@ForegroundServiceInfo("Exposure Notification")
38
class AdvertiserService : LifecycleService() {
Marvin W.'s avatar
Marvin W. committed
39
    private val version = VERSION_1_0
40
41
    private var advertising = false
    private var wantStartAdvertising = false
Marvin W.'s avatar
Marvin W. committed
42
    private val advertiser: BluetoothLeAdvertiser?
43
        get() = getDefaultAdapter()?.bluetoothLeAdvertiser
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    private val alarmManager: AlarmManager
        get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
    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)
58
    private var setCallback: Any? = null
Marvin W.'s avatar
Marvin W. committed
59
60
    private val trigger = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
61
62
63
64
            if (intent?.action == ACTION_STATE_CHANGED) {
                when (intent.getIntExtra(EXTRA_STATE, -1)) {
                    STATE_TURNING_OFF, STATE_OFF -> stopOrRestartAdvertising()
                    STATE_ON -> startAdvertisingIfNeeded()
Marvin W.'s avatar
Marvin W. committed
65
66
67
68
                }
            }
        }
    }
69
70
    private val handler = Handler(Looper.getMainLooper())
    private val startLaterRunnable = Runnable { startAdvertisingIfNeeded() }
71
72
73

    override fun onCreate() {
        super.onCreate()
74
        registerReceiver(trigger, IntentFilter().apply { addAction(ACTION_STATE_CHANGED) })
75
76
77
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
78
        ForegroundServiceContext.completeForegroundService(this, intent, TAG)
Marvin W.'s avatar
Marvin W. committed
79
        Log.d(TAG, "AdvertisingService.start: $intent")
80
        super.onStartCommand(intent, flags, startId)
81
82
        if (intent?.action == ACTION_RESTART_ADVERTISING && advertising) {
            stopOrRestartAdvertising()
83
        } else {
84
            startAdvertisingIfNeeded()
85
86
87
88
89
90
        }
        return START_STICKY
    }

    override fun onDestroy() {
        super.onDestroy()
Marvin W.'s avatar
Marvin W. committed
91
        unregisterReceiver(trigger)
92
        stopOrRestartAdvertising()
93
        handler.removeCallbacks(startLaterRunnable)
94
95
    }

96
    private fun startAdvertisingIfNeeded() {
97
        if (ExposurePreferences(this).enabled) {
98
99
100
101
102
            lifecycleScope.launchWhenStarted {
                withContext(Dispatchers.IO) {
                    startAdvertising()
                }
            }
103
104
105
        } else {
            stopSelf()
        }
Marvin W.'s avatar
Marvin W. committed
106
107
    }

108
109
    private var lastStartTime = System.currentTimeMillis()
    private var sendingBytes = ByteArray(0)
110
    private var starting = false
Marvin W.'s avatar
Marvin W. committed
111

112
113
114
115
116
117
118
    private suspend fun startAdvertising() {
        val advertiser = synchronized(this) {
            if (advertising || starting) return
            val advertiser = advertiser ?: return
            wantStartAdvertising = false
            starting = true
            advertiser
119
        }
120
121
122
123
        try {
            val aemBytes = when (version) {
                VERSION_1_0 -> byteArrayOf(
                        version, // Version and flags
124
                        currentDeviceInfo.txPowerCorrection, // TX power
125
126
127
128
129
                        0x00, // Reserved
                        0x00  // Reserved
                )
                VERSION_1_1 -> byteArrayOf(
                        (version + currentDeviceInfo.confidence.toByte() * 4).toByte(), // Version and flags
130
                        currentDeviceInfo.txPowerCorrection, // TX power
131
132
133
134
135
136
137
138
139
140
                        0x00, // Reserved
                        0x00  // Reserved
                )
                else -> return
            }
            var nextSend = nextKeyMillis.coerceAtLeast(10000)
            val payload = ExposureDatabase.with(this@AdvertiserService) { database ->
                database.generateCurrentPayload(aemBytes)
            }
            val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, payload).build()
Fynn Godau's avatar
Fynn Godau committed
141
            Log.i(TAG, "Starting advertiser")
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
            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 as AdvertisingSetCallback)
            } 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)
            }
            synchronized(this) { advertising = true }
            sendingBytes = payload
            lastStartTime = System.currentTimeMillis()
            scheduleRestartAdvertising(nextSend)
        } finally {
            synchronized(this) { starting = false }
167
        }
168
169
170
    }

    override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
171
        writer?.println("Advertising: $advertising")
Marvin W.'s avatar
Marvin W. committed
172
        try {
173
            val startTime = lastStartTime
Marvin W.'s avatar
Marvin W. committed
174
175
176
177
178
179
180
            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)}
181
                    TX Power: ${currentDeviceInfo.txPowerCorrection}
Marvin W.'s avatar
Marvin W. committed
182
183
184
185
186
                    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()}")
        }
187
188
    }

189
190
191
192
193
194
195
196
197
198
199
    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)
        }
    }

200
    @Synchronized
201
    private fun stopOrRestartAdvertising() {
202
        if (!advertising) return
Fynn Godau's avatar
Fynn Godau committed
203
        Log.i(TAG, "Stopping advertiser")
204
205
206
        advertising = false
        if (Build.VERSION.SDK_INT >= 26) {
            wantStartAdvertising = true
207
            advertiser?.stopAdvertisingSet(setCallback as AdvertisingSetCallback)
208
209
210
211
        } else {
            advertiser?.stopAdvertising(callback)
        }
        handler.postDelayed(startLaterRunnable, 1000)
212
    }
213

214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
    @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()
            }
        }
    }
229
230
231
232
233
234
235
236


    companion object {
        private const val ACTION_RESTART_ADVERTISING = "org.microg.gms.nearby.exposurenotification.RESTART_ADVERTISING"

        fun isNeeded(context: Context): Boolean {
            return ExposurePreferences(context).enabled
        }
237
238

        fun isSupported(context: Context): Boolean? {
239
            val adapter = getDefaultAdapter()
240
            return when {
241
242
                adapter == null -> false
                Build.VERSION.SDK_INT >= 26 && (adapter.isLeExtendedAdvertisingSupported || adapter.isLePeriodicAdvertisingSupported) -> true
243
                adapter.state != STATE_ON -> null
244
245
                adapter.bluetoothLeAdvertiser != null -> true
                else -> false
246
247
            }
        }
248
    }
249
}