EN: Cleanup data after 14 days, improve storage efficiency, add randomness for changing RPI

This commit is contained in:
Marvin W 2020-08-24 10:12:49 +02:00
parent c88832213c
commit cfc1c314d4
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
9 changed files with 155 additions and 57 deletions

View File

@ -14,12 +14,9 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<application>
<service
android:name="org.microg.gms.nearby.exposurenotification.ScannerService"
android:exported="true" />
<service
android:name="org.microg.gms.nearby.exposurenotification.AdvertiserService"
android:exported="true" />
<service android:name="org.microg.gms.nearby.exposurenotification.ScannerService" />
<service android:name="org.microg.gms.nearby.exposurenotification.AdvertiserService" />
<service android:name="org.microg.gms.nearby.exposurenotification.CleanupService" />
<service android:name="org.microg.gms.nearby.exposurenotification.ExposureNotificationService">
<intent-filter>

View File

@ -7,8 +7,11 @@ package org.microg.gms.nearby.exposurenotification
import android.annotation.TargetApi
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.*
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.bluetooth.le.AdvertiseSettings.*
import android.bluetooth.le.BluetoothLeAdvertiser
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@ -17,6 +20,7 @@ import android.util.Log
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import org.microg.gms.common.ForegroundServiceContext
import java.io.FileDescriptor
import java.io.PrintWriter
import java.nio.ByteBuffer
@ -24,6 +28,7 @@ import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
@TargetApi(21)
class AdvertiserService : LifecycleService() {
@ -69,6 +74,7 @@ class AdvertiserService : LifecycleService() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
super.onStartCommand(intent, flags, startId)
if (ExposurePreferences(this).advertiserEnabled) {
loopAdvertising()
@ -109,7 +115,7 @@ class AdvertiserService : LifecycleService() {
else -> return@launchWhenStarted
}
val payload = database.generateCurrentPayload(aem)
var nextSend = nextKeyMillis.coerceAtMost(180000)
val nextSend = (nextKeyMillis + Random.nextInt(-ADVERTISER_OFFSET, ADVERTISER_OFFSET)).coerceIn(0, 180000)
startAdvertising(payload, nextSend.toInt())
if (callback != null) delay(nextSend)
} while (callback != null)
@ -182,4 +188,10 @@ class AdvertiserService : LifecycleService() {
callback?.let { advertiser?.stopAdvertising(it) }
callback = null
}
companion object {
fun isNeeded(context: Context): Boolean {
return ExposurePreferences(context).scannerEnabled
}
}
}

View File

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.nearby.exposurenotification
import android.content.Context
import android.content.Intent
import androidx.lifecycle.LifecycleService
import org.microg.gms.common.ForegroundServiceContext
class CleanupService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
super.onStartCommand(intent, flags, startId)
if (isNeeded(this)) {
ExposureDatabase.with(this@CleanupService) {
it.dailyCleanup()
}
ExposurePreferences(this).lastCleanup = System.currentTimeMillis()
}
stopSelf()
return START_NOT_STICKY
}
companion object {
fun isNeeded(context: Context): Boolean {
return ExposurePreferences(context).let {
it.scannerEnabled && it.lastCleanup < System.currentTimeMillis() - CLEANUP_INTERVAL
}
}
}
}

View File

@ -30,6 +30,9 @@ const val PERMISSION_EXPOSURE_CALLBACK = "com.google.android.gms.nearby.exposure
const val TX_POWER_LOW = -15
const val ADVERTISER_OFFSET = 60 * 1000
const val CLEANUP_INTERVAL = 24 * 60 * 60 * 1000
const val VERSION_1_0: Byte = 0x40
const val VERSION_1_1: Byte = 0x50

View File

@ -5,28 +5,38 @@
package org.microg.gms.nearby.exposurenotification
import android.annotation.TargetApi
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteCursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE
import android.database.sqlite.SQLiteOpenHelper
import android.database.sqlite.SQLiteStatement
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.text.TextUtils
import android.util.Log
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import org.microg.gms.common.PackageUtils
import java.nio.ByteBuffer
import java.util.*
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.roundToInt
class ExposureDatabase private constructor(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
@TargetApi(21)
class ExposureDatabase private constructor(private val context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
private var refCount = 0
init {
setWriteAheadLoggingEnabled(true)
}
override fun onCreate(db: SQLiteDatabase) {
onUpgrade(db, 0, DB_VERSION)
}
@ -39,19 +49,30 @@ class ExposureDatabase private constructor(context: Context) : SQLiteOpenHelper(
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_APP_LOG(package TEXT NOT NULL, timestamp INTEGER NOT NULL, method TEXT NOT NULL, args TEXT);")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_APP_LOG}_package_timestamp ON $TABLE_APP_LOG(package, timestamp);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK(keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL);")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK_CHECK(keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, matched INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(keyData, rollingStartNumber, rollingPeriod));")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_DIAGNOSIS(package TEXT NOT NULL, token TEXT NOT NULL, keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, transmissionRiskLevel INTEGER NOT NULL, PRIMARY KEY(package, token, keyData));")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_CONFIGURATIONS(package TEXT NOT NULL, token TEXT NOT NULL, configuration BLOB, PRIMARY KEY(package, token))")
}
if (oldVersion < 2) {
db.execSQL("DROP TABLE IF EXISTS $TABLE_TEK_CHECK;")
db.execSQL("DROP TABLE IF EXISTS $TABLE_DIAGNOSIS;")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_TEK_CHECK(tcid INTEGER PRIMARY KEY, keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, matched INTEGER, UNIQUE(keyData, rollingStartNumber, rollingPeriod));")
db.execSQL("CREATE TABLE IF NOT EXISTS $TABLE_DIAGNOSIS(package TEXT NOT NULL, token TEXT NOT NULL, tcid INTEGER REFERENCES $TABLE_TEK_CHECK(tcid) ON DELETE CASCADE, transmissionRiskLevel INTEGER NOT NULL);")
db.execSQL("CREATE INDEX IF NOT EXISTS index_${TABLE_DIAGNOSIS}_package_token ON $TABLE_DIAGNOSIS(package, token);")
}
}
fun SQLiteDatabase.delete(table: String, whereClause: String, args: LongArray): Int =
compileStatement("DELETE FROM $table WHERE $whereClause").use {
args.forEachIndexed { idx, l -> it.bindLong(idx + 1, l) }
it.executeUpdateDelete()
}
fun dailyCleanup() = writableDatabase.run {
val rollingStartTime = currentRollingStartNumber * ROLLING_WINDOW_LENGTH * 1000 - TimeUnit.DAYS.toMillis(KEEP_DAYS.toLong())
delete(TABLE_ADVERTISEMENTS, "timestamp < ?", arrayOf(rollingStartTime.toString()))
delete(TABLE_APP_LOG, "timestamp < ?", arrayOf(rollingStartTime.toString()))
delete(TABLE_TEK, "rollingStartNumber + rollingPeriod < ?", arrayOf((rollingStartTime / ROLLING_WINDOW_LENGTH_MS).toString()))
delete(TABLE_TEK_CHECK, "rollingStartNumber + rollingPeriod < ?", arrayOf((rollingStartTime / ROLLING_WINDOW_LENGTH_MS).toString()))
delete(TABLE_DIAGNOSIS, "rollingStartNumber + rollingPeriod < ?", arrayOf((rollingStartTime / ROLLING_WINDOW_LENGTH_MS).toString()))
val advertisements = delete(TABLE_ADVERTISEMENTS, "timestamp < ?", longArrayOf(rollingStartTime))
val appLogEntries = delete(TABLE_APP_LOG, "timestamp < ?", longArrayOf(rollingStartTime))
val temporaryExposureKeys = delete(TABLE_TEK, "(rollingStartNumber + rollingPeriod) < ?", longArrayOf(rollingStartTime / ROLLING_WINDOW_LENGTH_MS))
val checkedTemporaryExposureKeys = delete(TABLE_TEK_CHECK, "(rollingStartNumber + rollingPeriod) < ?", longArrayOf(rollingStartTime / ROLLING_WINDOW_LENGTH_MS))
Log.d(TAG, "Deleted on daily cleanup: $advertisements adv, $appLogEntries applogs, $temporaryExposureKeys teks, $checkedTemporaryExposureKeys cteks")
}
fun noteAdvertisement(rpi: ByteArray, aem: ByteArray, rssi: Int, timestamp: Long = Date().time) = writableDatabase.run {
@ -78,7 +99,7 @@ class ExposureDatabase private constructor(context: Context) : SQLiteOpenHelper(
fun deleteAllCollectedAdvertisements() = writableDatabase.run {
delete(TABLE_ADVERTISEMENTS, null, null)
update(TABLE_DIAGNOSIS, ContentValues().apply {
update(TABLE_TEK_CHECK, ContentValues().apply {
put("matched", 0)
}, null, null)
}
@ -102,37 +123,48 @@ class ExposureDatabase private constructor(context: Context) : SQLiteOpenHelper(
key
}
fun getTekCheckId(key: TemporaryExposureKey, mayInsert: Boolean = false): Long? = (if (mayInsert) writableDatabase else readableDatabase).run {
if (mayInsert) {
insertWithOnConflict(TABLE_TEK_CHECK, "NULL", ContentValues().apply {
put("keyData", key.keyData)
put("rollingStartNumber", key.rollingStartIntervalNumber)
put("rollingPeriod", key.rollingPeriod)
}, CONFLICT_IGNORE)
}
compileStatement("SELECT tcid FROM $TABLE_TEK_CHECK WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ?").use {
it.bindBlob(1, key.keyData)
it.bindLong(2, key.rollingStartIntervalNumber.toLong())
it.bindLong(3, key.rollingPeriod.toLong())
it.simpleQueryForLong()
}
}
fun storeDiagnosisKey(packageName: String, token: String, key: TemporaryExposureKey) = writableDatabase.run {
val tcid = getTekCheckId(key, true)
insert(TABLE_DIAGNOSIS, "NULL", ContentValues().apply {
put("package", packageName)
put("token", token)
put("keyData", key.keyData)
put("rollingStartNumber", key.rollingStartIntervalNumber)
put("rollingPeriod", key.rollingPeriod)
put("tcid", tcid)
put("transmissionRiskLevel", key.transmissionRiskLevel)
})
}
fun updateDiagnosisKey(packageName: String, token: String, key: TemporaryExposureKey) = writableDatabase.run {
compileStatement("UPDATE $TABLE_DIAGNOSIS SET rollingStartNumber = ?, rollingPeriod = ?, transmissionRiskLevel = ? WHERE package = ? AND token = ? AND keyData = ?;").use {
it.bindLong(1, key.rollingStartIntervalNumber.toLong())
it.bindLong(2, key.rollingPeriod.toLong())
it.bindLong(3, key.transmissionRiskLevel.toLong())
it.bindString(4, packageName)
it.bindString(5, token)
it.bindBlob(6, key.keyData)
val tcid = getTekCheckId(key) ?: return 0
compileStatement("UPDATE $TABLE_DIAGNOSIS SET transmissionRiskLevel = ? WHERE package = ? AND token = ? AND tcid = ?;").use {
it.bindLong(1, key.transmissionRiskLevel.toLong())
it.bindString(2, packageName)
it.bindString(3, token)
it.bindLong(4, tcid)
it.executeUpdateDelete()
}
}
fun listDiagnosisKeysPendingSearch(packageName: String, token: String) = readableDatabase.run {
rawQuery("""
SELECT $TABLE_DIAGNOSIS.keyData, $TABLE_DIAGNOSIS.rollingStartNumber, $TABLE_DIAGNOSIS.rollingPeriod
SELECT $TABLE_TEK_CHECK.keyData, $TABLE_TEK_CHECK.rollingStartNumber, $TABLE_TEK_CHECK.rollingPeriod
FROM $TABLE_DIAGNOSIS
LEFT JOIN $TABLE_TEK_CHECK ON
$TABLE_DIAGNOSIS.keyData = $TABLE_TEK_CHECK.keyData AND
$TABLE_DIAGNOSIS.rollingStartNumber = $TABLE_TEK_CHECK.rollingStartNumber AND
$TABLE_DIAGNOSIS.rollingPeriod = $TABLE_TEK_CHECK.rollingPeriod
LEFT JOIN $TABLE_TEK_CHECK ON $TABLE_DIAGNOSIS.tcid = $TABLE_TEK_CHECK.tcid
WHERE
$TABLE_DIAGNOSIS.package = ? AND
$TABLE_DIAGNOSIS.token = ? AND
@ -151,22 +183,20 @@ class ExposureDatabase private constructor(context: Context) : SQLiteOpenHelper(
}
fun applyDiagnosisKeySearchResult(key: TemporaryExposureKey, matched: Boolean) = writableDatabase.run {
insert(TABLE_TEK_CHECK, "NULL", ContentValues().apply {
put("keyData", key.keyData)
put("rollingStartNumber", key.rollingStartIntervalNumber)
put("rollingPeriod", key.rollingPeriod)
put("matched", if (matched) 1 else 0)
})
compileStatement("UPDATE $TABLE_TEK_CHECK SET matched = ? WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ?;").use {
it.bindLong(1, if (matched) 1 else 0)
it.bindBlob(2, key.keyData)
it.bindLong(3, key.rollingStartIntervalNumber.toLong())
it.bindLong(4, key.rollingPeriod.toLong())
it.executeUpdateDelete()
}
}
fun listMatchedDiagnosisKeys(packageName: String, token: String) = readableDatabase.run {
rawQuery("""
SELECT $TABLE_DIAGNOSIS.keyData, $TABLE_DIAGNOSIS.rollingStartNumber, $TABLE_DIAGNOSIS.rollingPeriod, $TABLE_DIAGNOSIS.transmissionRiskLevel
SELECT $TABLE_TEK_CHECK.keyData, $TABLE_TEK_CHECK.rollingStartNumber, $TABLE_TEK_CHECK.rollingPeriod, $TABLE_DIAGNOSIS.transmissionRiskLevel
FROM $TABLE_DIAGNOSIS
LEFT JOIN $TABLE_TEK_CHECK ON
$TABLE_DIAGNOSIS.keyData = $TABLE_TEK_CHECK.keyData AND
$TABLE_DIAGNOSIS.rollingStartNumber = $TABLE_TEK_CHECK.rollingStartNumber AND
$TABLE_DIAGNOSIS.rollingPeriod = $TABLE_TEK_CHECK.rollingPeriod
LEFT JOIN $TABLE_TEK_CHECK ON $TABLE_DIAGNOSIS.tcid = $TABLE_TEK_CHECK.tcid
WHERE
$TABLE_DIAGNOSIS.package = ? AND
$TABLE_DIAGNOSIS.token = ? AND
@ -208,7 +238,7 @@ class ExposureDatabase private constructor(context: Context) : SQLiteOpenHelper(
}
val time = (System.currentTimeMillis() - start).toDouble() / 1000.0
executor.shutdown()
Log.d(TAG, "Processed ${keys.size} keys in ${time}s -> ${(keys.size.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s")
Log.d(TAG, "Processed ${keys.size} new keys in ${time}s -> ${(keys.size.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s")
}
fun findAllMeasuredExposures(packageName: String, token: String): List<MeasuredExposure> {
@ -226,7 +256,6 @@ class ExposureDatabase private constructor(context: Context) : SQLiteOpenHelper(
val pos = i * 16
allRpis.sliceArray(pos until (pos + 16))
}
val start = System.currentTimeMillis()
val measures = findMeasuredExposures(rpis, key.rollingStartIntervalNumber.toLong() * ROLLING_WINDOW_LENGTH_MS - ALLOWED_KEY_OFFSET_MS, (key.rollingStartIntervalNumber.toLong() + key.rollingPeriod) * ROLLING_WINDOW_LENGTH_MS + ALLOWED_KEY_OFFSET_MS)
measures.filter {
val index = rpis.indexOf(it.rpi)
@ -437,9 +466,7 @@ class ExposureDatabase private constructor(context: Context) : SQLiteOpenHelper(
if (this != instance) {
throw IllegalStateException("Tried to open writable database from secondary instance")
}
val db = super.getWritableDatabase()
db.enableWriteAheadLogging()
return db
return super.getWritableDatabase()
}
override fun close() {
@ -465,7 +492,7 @@ class ExposureDatabase private constructor(context: Context) : SQLiteOpenHelper(
companion object {
private const val DB_NAME = "exposure.db"
private const val DB_VERSION = 1
private const val DB_VERSION = 2
private const val TABLE_ADVERTISEMENTS = "advertisements"
private const val TABLE_APP_LOG = "app_log"
private const val TABLE_TEK = "tek"

View File

@ -19,8 +19,7 @@ import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import com.google.android.gms.nearby.exposurenotification.internal.*
import org.json.JSONArray
import org.json.JSONObject
import org.microg.gms.nearby.exposurenotification.Constants.ACTION_EXPOSURE_NOT_FOUND
import org.microg.gms.nearby.exposurenotification.Constants.ACTION_EXPOSURE_STATE_UPDATED
import org.microg.gms.nearby.exposurenotification.Constants.*
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyExport
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyProto
import java.util.*
@ -73,6 +72,10 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
override fun stop(params: StopParams) {
if (!ExposurePreferences(context).scannerEnabled) {
params.callback.onResult(Status.SUCCESS)
return
}
confirm(CONFIRM_ACTION_STOP) { resultCode, _ ->
if (resultCode == SUCCESS) {
ExposurePreferences(context).scannerEnabled = false
@ -189,6 +192,8 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
put("request_keys_count", keys)
}.toString())
database.finishMatching(packageName, params.token)
Handler(Looper.getMainLooper()).post {
try {
params.callback.onResult(Status.SUCCESS)
@ -197,11 +202,11 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
}
}
database.finishMatching(packageName, params.token)
val match = database.findAllMeasuredExposures(packageName, params.token).isNotEmpty()
try {
val intent = Intent(if (match) ACTION_EXPOSURE_STATE_UPDATED else ACTION_EXPOSURE_NOT_FOUND)
intent.putExtra(EXTRA_TOKEN, params.token)
intent.`package` = packageName
Log.d(TAG, "Sending $intent")
context.sendOrderedBroadcast(intent, PERMISSION_EXPOSURE_CALLBACK)

View File

@ -29,7 +29,12 @@ class ExposurePreferences(private val context: Context) {
val advertiserEnabled
get() = scannerEnabled
var lastCleanup
get() = preferences.getLong(PREF_LAST_CLEANUP, 0)
set(value) = preferences.edit().putLong(PREF_LAST_CLEANUP, value).apply()
companion object {
private const val PREF_SCANNER_ENABLED = "exposure_scanner_enabled"
private const val PREF_LAST_CLEANUP = "exposure_last_cleanup"
}
}

View File

@ -17,6 +17,7 @@ import android.content.IntentFilter
import android.os.Build
import android.os.IBinder
import android.util.Log
import org.microg.gms.common.ForegroundServiceContext
import java.io.FileDescriptor
import java.io.PrintWriter
import java.util.*
@ -26,6 +27,7 @@ class ScannerService : Service() {
private var started = false
private var startTime = 0L
private var seenAdvertisements = 0L
private var lastAdvertisement = 0L
private lateinit var database: ExposureDatabase
private val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
@ -59,6 +61,7 @@ class ScannerService : Service() {
get() = getDefaultAdapter().bluetoothLeScanner
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
ForegroundServiceContext.completeForegroundService(this, intent, TAG)
super.onStartCommand(intent, flags, startId)
startScanIfNeeded()
return START_STICKY
@ -69,6 +72,7 @@ class ScannerService : Service() {
if (data.size < 16) return // Ignore invalid advertisements
database.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi)
seenAdvertisements++
lastAdvertisement = System.currentTimeMillis()
}
fun startScanIfNeeded() {
@ -128,6 +132,13 @@ class ScannerService : Service() {
if (started) {
writer?.println("Since ${Date(startTime)}")
writer?.println("Seen advertisements: $seenAdvertisements")
writer?.println("Last advertisement: ${Date(lastAdvertisement)}")
}
}
companion object {
fun isNeeded(context: Context): Boolean {
return ExposurePreferences(context).scannerEnabled
}
}
}

View File

@ -16,11 +16,15 @@ class ServiceTrigger : BroadcastReceiver() {
@SuppressLint("UnsafeProtectedBroadcastReceiver")
override fun onReceive(context: Context, intent: Intent?) {
Log.d(TAG, "ServiceTrigger: $intent")
if (ExposurePreferences(context).scannerEnabled) {
ForegroundServiceContext(context).startService(Intent(context, ScannerService::class.java))
val serviceContext = ForegroundServiceContext(context)
if (ScannerService.isNeeded(context)) {
serviceContext.startService(Intent(context, ScannerService::class.java))
}
if (ExposurePreferences(context).advertiserEnabled) {
ForegroundServiceContext(context).startService(Intent(context, AdvertiserService::class.java))
if (AdvertiserService.isNeeded(context)) {
serviceContext.startService(Intent(context, AdvertiserService::class.java))
}
if (CleanupService.isNeeded(context)) {
serviceContext.startService(Intent(context, CleanupService::class.java))
}
}
}