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

package org.microg.gms.nearby.exposurenotification

8
9
import android.app.Activity
import android.app.PendingIntent
10
11
import android.bluetooth.BluetoothAdapter
import android.content.*
12
13
import android.os.*
import android.util.Log
14
import androidx.lifecycle.Lifecycle
15
import androidx.lifecycle.LifecycleCoroutineScope
16
17
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
18
import com.google.android.gms.common.api.Status
Marvin W.'s avatar
Marvin W. committed
19
import com.google.android.gms.nearby.exposurenotification.*
20
21
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
import com.google.android.gms.nearby.exposurenotification.internal.*
22
import kotlinx.coroutines.*
23
24
import org.json.JSONArray
import org.json.JSONObject
25
import org.microg.gms.common.Constants
26
import org.microg.gms.common.PackageUtils
27
import org.microg.gms.nearby.exposurenotification.Constants.*
28
29
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyExport
import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyProto
30
31
32
33
import java.io.File
import java.io.InputStream
import java.security.MessageDigest
import java.util.zip.ZipFile
Marvin W.'s avatar
Marvin W. committed
34
import kotlin.math.max
Marvin W.'s avatar
Marvin W. committed
35
import kotlin.math.roundToInt
36
import kotlin.random.Random
37

38
39
class ExposureNotificationServiceImpl(private val context: Context, private val lifecycle: Lifecycle, private val packageName: String) : INearbyExposureNotificationService.Stub(), LifecycleOwner {

40
41
    private fun LifecycleCoroutineScope.launchSafely(block: suspend CoroutineScope.() -> Unit): Job = launchWhenStarted { try { block() } catch (e: Exception) { Log.w(TAG, "Error in coroutine", e) } }

42
43
    override fun getLifecycle(): Lifecycle = lifecycle

44
    private fun pendingConfirm(permission: String): PendingIntent {
45
46
47
        val intent = Intent(ACTION_CONFIRM)
        intent.`package` = context.packageName
        intent.putExtra(KEY_CONFIRM_PACKAGE, packageName)
48
49
        intent.putExtra(KEY_CONFIRM_ACTION, permission)
        intent.putExtra(KEY_CONFIRM_RECEIVER, object : ResultReceiver(null) {
50
            override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
51
                if (resultCode == Activity.RESULT_OK) {
52
                    tempGrantedPermissions.add(packageName to permission)
53
                }
54
55
56
57
58
59
            }
        })
        try {
            intent.component = ComponentName(context, context.packageManager.resolveActivity(intent, 0)?.activityInfo?.name!!)
        } catch (e: Exception) {
            Log.w(TAG, e)
60
61
62
63
64
65
66
        }
        Log.d(TAG, "Pending: $intent")
        val pi = PendingIntent.getActivity(context, permission.hashCode(), intent, PendingIntent.FLAG_ONE_SHOT)
        Log.d(TAG, "Pending: $pi")
        return pi
    }

67
68
69
70
71
72
73
74
75
76
    private fun hasConfirmActivity(): Boolean {
        val intent = Intent(ACTION_CONFIRM)
        intent.`package` = context.packageName
        return try {
            context.packageManager.resolveActivity(intent, 0) != null
        } catch (e: Exception) {
            false
        }
    }

77
    private suspend fun confirmPermission(permission: String, force: Boolean = false): Status {
78
        return ExposureDatabase.with(context) { database ->
79
80
81
82
83
84
            when {
                tempGrantedPermissions.contains(packageName to permission) -> {
                    database.grantPermission(packageName, PackageUtils.firstSignatureDigest(context, packageName)!!, permission)
                    tempGrantedPermissions.remove(packageName to permission)
                    Status.SUCCESS
                }
85
                !force && database.hasPermission(packageName, PackageUtils.firstSignatureDigest(context, packageName)!!, permission) -> {
86
87
88
89
90
91
92
93
                    Status.SUCCESS
                }
                !hasConfirmActivity() -> {
                    Status.SUCCESS
                }
                else -> {
                    Status(RESOLUTION_REQUIRED, "Permission EN#$permission required.", pendingConfirm(permission))
                }
94
            }
95
96
97
        }
    }

98
    override fun getVersion(params: GetVersionParams) {
Marvin W.'s avatar
Marvin W. committed
99
        params.callback.onResult(Status.SUCCESS, Constants.GMS_VERSION_CODE.toLong())
100
101
102
103
104
105
    }

    override fun getCalibrationConfidence(params: GetCalibrationConfidenceParams) {
        params.callback.onResult(Status.SUCCESS, currentDeviceInfo.confidence)
    }

106
    override fun start(params: StartParams) {
107
        lifecycleScope.launchSafely {
108
            val isAuthorized = ExposureDatabase.with(context) { it.isAppAuthorized(packageName) }
109
            val adapter = BluetoothAdapter.getDefaultAdapter()
110
111
            val status = if (isAuthorized && ExposurePreferences(context).enabled) {
                Status.SUCCESS
112
113
            } else if (adapter == null) {
                Status(FAILED_NOT_SUPPORTED, "No Bluetooth Adapter available.")
114
            } else {
115
                val status = confirmPermission(CONFIRM_ACTION_START, !adapter.isEnabled)
116
                if (status.isSuccess) {
117
118
                    val context = context
                    adapter.enableAsync(context)
119
120
121
122
123
                    ExposurePreferences(context).enabled = true
                    ExposureDatabase.with(context) { database ->
                        database.authorizeApp(packageName)
                        database.noteAppAction(packageName, "start")
                    }
124
                }
125
                status
126
127
128
129
130
131
            }
            try {
                params.callback.onResult(status)
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
            }
132
133
134
135
        }
    }

    override fun stop(params: StopParams) {
136
        lifecycleScope.launchSafely {
137
138
139
140
141
142
143
            val isAuthorized = ExposureDatabase.with(context) { database ->
                database.isAppAuthorized(packageName).also {
                    if (it) database.noteAppAction(packageName, "stop")
                }
            }
            if (isAuthorized) {
                ExposurePreferences(context).enabled = false
144
145
146
147
148
149
            }
            try {
                params.callback.onResult(Status.SUCCESS)
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
            }
150
151
        }
    }
152

153
    override fun isEnabled(params: IsEnabledParams) {
154
        lifecycleScope.launchSafely {
155
            val isAuthorized = ExposureDatabase.with(context) { database ->
Marvin W.'s avatar
Marvin W. committed
156
                database.isAppAuthorized(packageName)
157
158
159
160
161
162
            }
            try {
                params.callback.onResult(Status.SUCCESS, isAuthorized && ExposurePreferences(context).enabled)
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
            }
163
164
165
        }
    }

166
    override fun getTemporaryExposureKeyHistory(params: GetTemporaryExposureKeyHistoryParams) {
167
        lifecycleScope.launchSafely {
168
169
170
            val status = confirmPermission(CONFIRM_ACTION_KEYS)
            val response = when {
                status.isSuccess -> ExposureDatabase.with(context) { database ->
171
                    database.authorizeApp(packageName)
172
                    database.exportKeys()
173
174
                }
                else -> emptyList()
175
176
            }

177
178
179
180
181
182
183
184
185
186
187
            ExposureDatabase.with(context) { database ->
                database.noteAppAction(packageName, "getTemporaryExposureKeyHistory", JSONObject().apply {
                    put("result", status.statusCode)
                    put("response_keys_size", response.size)
                }.toString())
            }
            try {
                params.callback.onResult(status, response)
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
            }
188
189
190
191
192
193
194
195
196
197
198
        }
    }

    private fun TemporaryExposureKeyProto.toKey(): TemporaryExposureKey = TemporaryExposureKey.TemporaryExposureKeyBuilder()
            .setKeyData(key_data?.toByteArray() ?: throw IllegalArgumentException("key data missing"))
            .setRollingStartIntervalNumber(rolling_start_interval_number
                    ?: throw IllegalArgumentException("rolling start interval number missing"))
            .setRollingPeriod(rolling_period ?: throw IllegalArgumentException("rolling period missing"))
            .setTransmissionRiskLevel(transmission_risk_level ?: 0)
            .build()

199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
    private fun InputStream.copyToFile(outputFile: File) {
        outputFile.outputStream().use { output ->
            copyTo(output)
            output.flush()
        }
    }

    private fun MessageDigest.digest(file: File): ByteArray = file.inputStream().use { input ->
        val buf = ByteArray(4096)
        var bytes = input.read(buf)
        while (bytes != -1) {
            update(buf, 0, bytes)
            bytes = input.read(buf)
        }
        digest()
214
215
    }

Marvin W.'s avatar
Marvin W. committed
216
217
218
    private fun ExposureConfiguration?.orDefault() = this
            ?: ExposureConfiguration.ExposureConfigurationBuilder().build()

219
    private suspend fun buildExposureSummary(token: String): ExposureSummary = ExposureDatabase.with(context) { database ->
220
221
222
223
224
        if (!database.isAppAuthorized(packageName)) {
            // Not providing summary if app not authorized
            Log.d(TAG, "$packageName not yet authorized")
            return@with ExposureSummary.ExposureSummaryBuilder().build()
        }
225
226
        val pair = database.loadConfiguration(packageName, token)
        val (configuration, exposures) = if (pair != null) {
Marvin W.'s avatar
Marvin W. committed
227
            pair.second.orDefault() to database.findAllMeasuredExposures(pair.first).merge()
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
        } else {
            ExposureConfiguration.ExposureConfigurationBuilder().build() to emptyList()
        }

        ExposureSummary.ExposureSummaryBuilder()
                .setDaysSinceLastExposure(exposures.map { it.daysSinceExposure }.min()?.toInt() ?: 0)
                .setMatchedKeyCount(exposures.map { it.key }.distinct().size)
                .setMaximumRiskScore(exposures.map { it.getRiskScore(configuration) }.max()?.toInt() ?: 0)
                .setAttenuationDurations(intArrayOf(
                        exposures.map { it.getAttenuationDurations(configuration)[0] }.sum(),
                        exposures.map { it.getAttenuationDurations(configuration)[1] }.sum(),
                        exposures.map { it.getAttenuationDurations(configuration)[2] }.sum()
                ))
                .setSummationRiskScore(exposures.map { it.getRiskScore(configuration) }.sum())
                .build()
    }

245
    override fun provideDiagnosisKeys(params: ProvideDiagnosisKeysParams) {
246
247
        val token = params.token ?: TOKEN_A
        Log.w(TAG, "provideDiagnosisKeys() with $packageName/$token")
248
        lifecycleScope.launchSafely {
249
            val tid = ExposureDatabase.with(context) { database ->
250
251
252
                val configuration = params.configuration
                if (configuration != null) {
                    database.storeConfiguration(packageName, token, configuration)
253
                } else {
254
                    database.getOrCreateTokenId(packageName, token)
255
                }
256
            }
257
            if (tid == null) {
258
                Log.w(TAG, "Unknown token without configuration: $packageName/$token")
259
260
261
262
263
                try {
                    params.callback.onResult(Status.INTERNAL_ERROR)
                } catch (e: Exception) {
                    Log.w(TAG, "Callback failed", e)
                }
264
                return@launchSafely
265
            }
266
            ExposureDatabase.with(context) { database ->
Marvin W.'s avatar
Marvin W. committed
267
268
                val start = System.currentTimeMillis()

269
                // keys
270
                params.keys?.let { database.batchStoreSingleDiagnosisKey(tid, it) }
271

272
                var keys = params.keys?.size ?: 0
273
274
275

                // Key files
                val todoKeyFiles = arrayListOf<Pair<File, ByteArray>>()
276
277
                for (file in params.keyFiles.orEmpty()) {
                    try {
278
279
280
281
282
283
284
285
286
                        val cacheFile = File(context.cacheDir, "en-keyfile-${System.currentTimeMillis()}-${Random.nextInt()}.zip")
                        ParcelFileDescriptor.AutoCloseInputStream(file).use { it.copyToFile(cacheFile) }
                        val hash = MessageDigest.getInstance("SHA-256").digest(cacheFile)
                        val storedKeys = database.storeDiagnosisFileUsed(tid, hash)
                        if (storedKeys != null) {
                            keys += storedKeys.toInt()
                            cacheFile.delete()
                        } else {
                            todoKeyFiles.add(cacheFile to hash)
287
288
289
                        }
                    } catch (e: Exception) {
                        Log.w(TAG, "Failed parsing file", e)
290
291
                    }
                }
Marvin W.'s avatar
Marvin W. committed
292
293
                params.keyFileSupplier?.let { keyFileSupplier ->
                    Log.d(TAG, "Using key file supplier")
294
295
                    try {
                        while (keyFileSupplier.isAvailable && keyFileSupplier.hasNext()) {
296
297
298
299
300
301
302
303
304
305
306
307
308
309
                            withContext(Dispatchers.IO) {
                                try {
                                    val cacheFile = File(context.cacheDir, "en-keyfile-${System.currentTimeMillis()}-${Random.nextLong()}.zip")
                                    ParcelFileDescriptor.AutoCloseInputStream(keyFileSupplier.next()).use { it.copyToFile(cacheFile) }
                                    val hash = MessageDigest.getInstance("SHA-256").digest(cacheFile)
                                    val storedKeys = database.storeDiagnosisFileUsed(tid, hash)
                                    if (storedKeys != null) {
                                        keys += storedKeys.toInt()
                                        cacheFile.delete()
                                    } else {
                                        todoKeyFiles.add(cacheFile to hash)
                                    }
                                } catch (e: Exception) {
                                    Log.w(TAG, "Failed parsing file", e)
310
                                }
Marvin W.'s avatar
Marvin W. committed
311
312
                            }
                        }
313
314
                    } catch (e: Exception) {
                        Log.w(TAG, "Disconnected from key file supplier", e)
Marvin W.'s avatar
Marvin W. committed
315
316
                    }
                }
317

318
                if (todoKeyFiles.size > 0) {
Christian Grigis's avatar
Christian Grigis committed
319
                    val time = (System.currentTimeMillis() - start).coerceAtLeast(1).toDouble() / 1000.0
320
                    Log.d(TAG, "$packageName/$token processed $keys keys (${todoKeyFiles.size} files pending) in ${time}s -> ${(keys.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s")
321
                }
322

323
324
325
326
327
328
329
330
                Handler(Looper.getMainLooper()).post {
                    try {
                        params.callback.onResult(Status.SUCCESS)
                    } catch (e: Exception) {
                        Log.w(TAG, "Callback failed", e)
                    }
                }

331
332
                var newKeys = if (params.keys != null) database.finishSingleMatching(tid) else 0
                for ((cacheFile, hash) in todoKeyFiles) {
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
                    withContext(Dispatchers.IO) {
                        try {
                            ZipFile(cacheFile).use { zip ->
                                for (entry in zip.entries()) {
                                    if (entry.name == "export.bin") {
                                        val stream = zip.getInputStream(entry)
                                        val prefix = ByteArray(16)
                                        var totalBytesRead = 0
                                        var bytesRead = 0
                                        while (bytesRead != -1 && totalBytesRead < prefix.size) {
                                            bytesRead = stream.read(prefix, totalBytesRead, prefix.size - totalBytesRead)
                                            if (bytesRead > 0) {
                                                totalBytesRead += bytesRead
                                            }
                                        }
                                        if (totalBytesRead == prefix.size && String(prefix).trim() == "EK Export v1") {
                                            val export = TemporaryExposureKeyExport.ADAPTER.decode(stream)
                                            database.finishFileMatching(tid, hash, export.end_timestamp?.let { it * 1000 }
                                                    ?: System.currentTimeMillis(), export.keys.map { it.toKey() }, export.revised_keys.map { it.toKey() })
                                            keys += export.keys.size + export.revised_keys.size
                                            newKeys += export.keys.size
                                        } else {
                                            Log.d(TAG, "export.bin had invalid prefix")
                                        }
357
358
359
                                    }
                                }
                            }
360
361
362
                            cacheFile.delete()
                        } catch (e: Exception) {
                            Log.w(TAG, "Failed parsing file", e)
363
364
365
366
                        }
                    }
                }

Marvin W.'s avatar
Marvin W. committed
367
                val time = (System.currentTimeMillis() - start).coerceAtLeast(1).toDouble() / 1000.0
368
                Log.d(TAG, "$packageName/$token processed $keys keys ($newKeys new) in ${time}s -> ${(keys.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s")
369
370

                database.noteAppAction(packageName, "provideDiagnosisKeys", JSONObject().apply {
371
                    put("request_token", token)
372
373
374
375
376
                    put("request_keys_size", params.keys?.size)
                    put("request_keyFiles_size", params.keyFiles?.size)
                    put("request_keys_count", keys)
                }.toString())

377
378
379
380
381
382
                if (!database.isAppAuthorized(packageName)) {
                    // Not sending results via broadcast if app not authorized
                    Log.d(TAG, "$packageName not yet authorized")
                    return@with
                }

383
                val exposureSummary = buildExposureSummary(token)
384

385
                try {
386
                    val intent = if (exposureSummary.matchedKeyCount > 0) {
387
                        Intent(ACTION_EXPOSURE_STATE_UPDATED)
388
389
390
                    } else {
                        Intent(ACTION_EXPOSURE_NOT_FOUND)
                    }
391
392
393
                    if (token != TOKEN_A) {
                        intent.putExtra(EXTRA_EXPOSURE_SUMMARY, exposureSummary)
                    }
394
                    intent.putExtra(EXTRA_TOKEN, token)
395
396
                    intent.`package` = packageName
                    Log.d(TAG, "Sending $intent")
397
                    context.sendOrderedBroadcast(intent, null)
398
399
400
401
402
403
404
                } catch (e: Exception) {
                    Log.w(TAG, "Callback failed", e)
                }
            }
        }
    }

405
    override fun getExposureSummary(params: GetExposureSummaryParams) {
406
        lifecycleScope.launchSafely {
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
            val response = buildExposureSummary(params.token)

            ExposureDatabase.with(context) { database ->
                database.noteAppAction(packageName, "getExposureSummary", JSONObject().apply {
                    put("request_token", params.token)
                    put("response_days_since", response.daysSinceLastExposure)
                    put("response_matched_keys", response.matchedKeyCount)
                    put("response_max_risk", response.maximumRiskScore)
                    put("response_attenuation_durations", JSONArray().apply {
                        response.attenuationDurationsInMinutes.forEach { put(it) }
                    })
                    put("response_summation_risk", response.summationRiskScore)
                }.toString())
            }
            try {
                params.callback.onResult(Status.SUCCESS, response)
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
425
426
            }
        }
427
    }
428

429
    override fun getExposureInformation(params: GetExposureInformationParams) {
430
        lifecycleScope.launchSafely {
431
432
            ExposureDatabase.with(context) { database ->
                val pair = database.loadConfiguration(packageName, params.token)
433
                val response = if (pair != null && database.isAppAuthorized(packageName)) {
434
                    database.findAllMeasuredExposures(pair.first).merge().map {
Marvin W.'s avatar
Marvin W. committed
435
                        it.toExposureInformation(pair.second.orDefault())
436
437
                    }
                } else {
438
439
                    // Not providing information if app not authorized
                    Log.d(TAG, "$packageName not yet authorized")
440
441
442
443
444
445
446
447
448
449
450
451
452
                    emptyList()
                }

                database.noteAppAction(packageName, "getExposureInformation", JSONObject().apply {
                    put("request_token", params.token)
                    put("response_size", response.size)
                }.toString())
                try {
                    params.callback.onResult(Status.SUCCESS, response)
                } catch (e: Exception) {
                    Log.w(TAG, "Callback failed", e)
                }
            }
453
454
        }
    }
Marvin W.'s avatar
Marvin W. committed
455

Marvin W.'s avatar
Marvin W. committed
456
457
    private fun ScanInstance.Builder.apply(subExposure: MergedSubExposure): ScanInstance.Builder {
        return this
458
                .setSecondsSinceLastScan(subExposure.duration.coerceAtMost(5 * 60).toInt())
Marvin W.'s avatar
Marvin W. committed
459
460
461
462
463
464
465
466
467
                .setMinAttenuationDb(subExposure.attenuation) // FIXME: We use the average for both, because we don't store the minimum attenuation yet
                .setTypicalAttenuationDb(subExposure.attenuation)
    }

    private fun List<MergedSubExposure>.toScanInstances(): List<ScanInstance> {
        val res = arrayListOf<ScanInstance>()
        for (subExposure in this) {
            res.add(ScanInstance.Builder().apply(subExposure).build())
            if (subExposure.duration > 5 * 60 * 1000L) {
468
                res.add(ScanInstance.Builder().apply(subExposure).setSecondsSinceLastScan((subExposure.duration - 5 * 60).coerceAtMost(5 * 60).toInt()).build())
Marvin W.'s avatar
Marvin W. committed
469
470
471
472
473
474
475
476
477
478
            }
        }
        return res
    }

    private fun DiagnosisKeysDataMapping?.orDefault() = this ?: DiagnosisKeysDataMapping()

    private suspend fun getExposureWindowsInternal(token: String = TOKEN_A): List<ExposureWindow> {
        val (exposures, mapping) = ExposureDatabase.with(context) { database ->
            val triple = database.loadConfiguration(packageName, token)
479
            if (triple != null && database.isAppAuthorized(packageName)) {
Marvin W.'s avatar
Marvin W. committed
480
481
                database.findAllMeasuredExposures(triple.first).merge() to triple.third.orDefault()
            } else {
482
483
                // Not providing windows if app not authorized
                Log.d(TAG, "$packageName not yet authorized")
Marvin W.'s avatar
Marvin W. committed
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
                emptyList<MergedExposure>() to DiagnosisKeysDataMapping()
            }
        }
        return exposures.map {
            val infectiousness =
                    if (it.key.daysSinceOnsetOfSymptoms == DAYS_SINCE_ONSET_OF_SYMPTOMS_UNKNOWN)
                        mapping.infectiousnessWhenDaysSinceOnsetMissing
                    else
                        mapping.daysSinceOnsetToInfectiousness[it.key.daysSinceOnsetOfSymptoms]
                                ?: Infectiousness.NONE
            val reportType =
                    if (it.key.reportType == ReportType.UNKNOWN)
                        mapping.reportTypeWhenMissing
                    else
                        it.key.reportType

            ExposureWindow.Builder()
                    .setCalibrationConfidence(it.confidence)
                    .setDateMillisSinceEpoch(it.key.rollingStartIntervalNumber.toLong() * ROLLING_WINDOW_LENGTH_MS)
                    .setInfectiousness(infectiousness)
                    .setReportType(reportType)
                    .setScanInstances(it.subs.toScanInstances())
                    .build()
        }
    }

510
    override fun getExposureWindows(params: GetExposureWindowsParams) {
511
        lifecycleScope.launchSafely {
Marvin W.'s avatar
Marvin W. committed
512
513
514
515
516
517
518
519
520
            val response = getExposureWindowsInternal(params.token ?: TOKEN_A)

            ExposureDatabase.with(context) { database ->
                database.noteAppAction(packageName, "getExposureWindows", JSONObject().apply {
                    put("request_token", params.token)
                    put("response_size", response.size)
                }.toString())
            }

521
522
523
524
525
            try {
                params.callback.onResult(Status.SUCCESS, response)
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
            }
Marvin W.'s avatar
Marvin W. committed
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
        }
    }

    private fun DailySummariesConfig.bucketFor(attenuation: Int): Int {
        if (attenuation < attenuationBucketThresholdDb[0]) return 0
        if (attenuation < attenuationBucketThresholdDb[1]) return 1
        if (attenuation < attenuationBucketThresholdDb[2]) return 2
        return 3
    }

    private fun DailySummariesConfig.weightedDurationFor(attenuation: Int, seconds: Int): Double {
        return attenuationBucketWeights[bucketFor(attenuation)] * seconds
    }

    private fun Collection<DailySummary.ExposureSummaryData>.sum(): DailySummary.ExposureSummaryData {
        return DailySummary.ExposureSummaryData(map { it.maximumScore }.maxOrNull()
                ?: 0.0, sumByDouble { it.scoreSum }, sumByDouble { it.weightedDurationSum })
543
544
545
    }

    override fun getDailySummaries(params: GetDailySummariesParams) {
546
        lifecycleScope.launchSafely {
Marvin W.'s avatar
Marvin W. committed
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
            val response = getExposureWindowsInternal().groupBy { it.dateMillisSinceEpoch }.map {
                val map = arrayListOf<DailySummary.ExposureSummaryData>()
                for (i in 0 until ReportType.VALUES) {
                    map[i] = DailySummary.ExposureSummaryData(0.0, 0.0, 0.0)
                }
                for (entry in it.value.groupBy { it.reportType }) {
                    for (window in entry.value) {
                        val weightedDuration = window.scanInstances.map { params.config.weightedDurationFor(it.typicalAttenuationDb, it.secondsSinceLastScan) }.sum()
                        val score = (params.config.reportTypeWeights[window.reportType] ?: 1.0) *
                                (params.config.infectiousnessWeights[window.infectiousness] ?: 1.0) *
                                weightedDuration
                        if (score >= params.config.minimumWindowScore) {
                            map[entry.key] = DailySummary.ExposureSummaryData(max(map[entry.key].maximumScore, score), map[entry.key].scoreSum + score, map[entry.key].weightedDurationSum + weightedDuration)
                        }
                    }
                }
                DailySummary((it.key / (1000L * 60 * 60 * 24)).toInt(), map, map.sum())
            }

            ExposureDatabase.with(context) { database ->
                database.noteAppAction(packageName, "getDailySummaries", JSONObject().apply {
                    put("response_size", response.size)
                }.toString())
            }

572
573
574
575
576
            try {
                params.callback.onResult(Status.SUCCESS, response)
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
            }
Marvin W.'s avatar
Marvin W. committed
577
        }
578
579
580
    }

    override fun setDiagnosisKeysDataMapping(params: SetDiagnosisKeysDataMappingParams) {
581
        lifecycleScope.launchSafely {
Marvin W.'s avatar
Marvin W. committed
582
583
584
585
            ExposureDatabase.with(context) { database ->
                database.storeConfiguration(packageName, TOKEN_A, params.mapping)
                database.noteAppAction(packageName, "setDiagnosisKeysDataMapping")
            }
586
587
588
589
590
            try {
                params.callback.onResult(Status.SUCCESS)
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
            }
Marvin W.'s avatar
Marvin W. committed
591
        }
592
593
594
    }

    override fun getDiagnosisKeysDataMapping(params: GetDiagnosisKeysDataMappingParams) {
595
        lifecycleScope.launchSafely {
Marvin W.'s avatar
Marvin W. committed
596
597
598
599
600
            val mapping = ExposureDatabase.with(context) { database ->
                val triple = database.loadConfiguration(packageName, TOKEN_A)
                database.noteAppAction(packageName, "getDiagnosisKeysDataMapping")
                triple?.third
            }
601
602
603
604
605
            try {
                params.callback.onResult(Status.SUCCESS, mapping.orDefault())
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
            }
Marvin W.'s avatar
Marvin W. committed
606
607
608
609
610
        }
    }

    override fun getPackageConfiguration(params: GetPackageConfigurationParams) {
        Log.w(TAG, "Not yet implemented: getPackageConfiguration")
611
        lifecycleScope.launchSafely {
Marvin W.'s avatar
Marvin W. committed
612
613
614
            ExposureDatabase.with(context) { database ->
                database.noteAppAction(packageName, "getPackageConfiguration")
            }
615
616
617
618
619
            try {
                params.callback.onResult(Status.SUCCESS, PackageConfiguration.PackageConfigurationBuilder().setValues(Bundle.EMPTY).build())
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
            }
Marvin W.'s avatar
Marvin W. committed
620
621
622
623
624
        }
    }

    override fun getStatus(params: GetStatusParams) {
        Log.w(TAG, "Not yet implemented: getStatus")
625
        lifecycleScope.launchSafely {
Marvin W.'s avatar
Marvin W. committed
626
627
628
            ExposureDatabase.with(context) { database ->
                database.noteAppAction(packageName, "getStatus")
            }
629
630
631
632
633
            try {
                params.callback.onResult(Status.SUCCESS, ExposureNotificationStatus.setToFlags(setOf(ExposureNotificationStatus.UNKNOWN)))
            } catch (e: Exception) {
                Log.w(TAG, "Callback failed", e)
            }
Marvin W.'s avatar
Marvin W. committed
634
        }
635
636
    }

Marvin W.'s avatar
Marvin W. committed
637
638
639
640
641
    override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
        if (super.onTransact(code, data, reply, flags)) return true
        Log.d(TAG, "onTransact [unknown]: $code, $data, $flags")
        return false
    }
642
643
644
645

    companion object {
        private val tempGrantedPermissions: MutableSet<Pair<String, String>> = hashSetOf()
    }
646
}