Do not use database.use (for pre-Q), ensure database stays open as needed

Related to #1115
This commit is contained in:
Marvin W 2020-08-04 11:42:15 +02:00
parent b898878f26
commit 761b6dfd47
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
3 changed files with 103 additions and 92 deletions

View File

@ -21,10 +21,25 @@ import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.roundToInt import kotlin.math.roundToInt
class ExposureDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { class ExposureDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
private val refCount = AtomicInteger(0)
fun ref(): ExposureDatabase {
refCount.incrementAndGet()
return this
}
fun unref() {
val nu = refCount.decrementAndGet()
if (nu == 0) {
close()
} else if (nu < 0) {
throw RuntimeException("ref/unref mismatch")
}
}
override fun onCreate(db: SQLiteDatabase) { override fun onCreate(db: SQLiteDatabase) {
onUpgrade(db, 0, DB_VERSION) onUpgrade(db, 0, DB_VERSION)
@ -149,7 +164,7 @@ class ExposureDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
} }
} }
fun applyDiagnosisKeySearchResult(packageName: String, token: String, key: TemporaryExposureKey, matched: Boolean) = writableDatabase.run { fun applyDiagnosisKeySearchResult(key: TemporaryExposureKey, matched: Boolean) = writableDatabase.run {
insert(TABLE_TEK_CHECK, "NULL", ContentValues().apply { insert(TABLE_TEK_CHECK, "NULL", ContentValues().apply {
put("keyData", key.keyData) put("keyData", key.keyData)
put("rollingStartNumber", key.rollingStartIntervalNumber) put("rollingStartNumber", key.rollingStartIntervalNumber)
@ -195,10 +210,10 @@ class ExposureDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
for (key in keys) { for (key in keys) {
if (oldestRpi == null || key.rollingStartIntervalNumber * ROLLING_WINDOW_LENGTH_MS - ALLOWED_KEY_OFFSET_MS < oldestRpi) { if (oldestRpi == null || key.rollingStartIntervalNumber * ROLLING_WINDOW_LENGTH_MS - ALLOWED_KEY_OFFSET_MS < oldestRpi) {
// Early ignore because key is older than since we started scanning. // Early ignore because key is older than since we started scanning.
applyDiagnosisKeySearchResult(packageName, token, key, false) applyDiagnosisKeySearchResult(key, false)
} else { } else {
futures.add(executor.submit { futures.add(executor.submit {
applyDiagnosisKeySearchResult(packageName, token, key, findMeasuredExposures(key).isNotEmpty()) applyDiagnosisKeySearchResult(key, findMeasuredExposures(key).isNotEmpty())
}) })
} }
} }
@ -207,7 +222,7 @@ class ExposureDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
} }
val time = (System.currentTimeMillis() - start).toDouble() / 1000.0 val time = (System.currentTimeMillis() - start).toDouble() / 1000.0
executor.shutdown() executor.shutdown()
Log.d(TAG, "Processed ${keys.size} keys in ${System.currentTimeMillis() - start}s -> ${(keys.size.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s") Log.d(TAG, "Processed ${keys.size} keys in ${time}s -> ${(keys.size.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s")
} }
fun findAllMeasuredExposures(packageName: String, token: String): List<MeasuredExposure> { fun findAllMeasuredExposures(packageName: String, token: String): List<MeasuredExposure> {

View File

@ -22,12 +22,15 @@ class ExposureNotificationService : BaseService(TAG, GmsService.NEARBY_EXPOSURE)
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
database.close() database.unref()
} }
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
database = ExposureDatabase(this) if (!this::database.isInitialized) {
database = ExposureDatabase(this)
}
database.ref()
} }
override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) {

View File

@ -59,11 +59,9 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
if (resultCode == SUCCESS) { if (resultCode == SUCCESS) {
ExposurePreferences(context).scannerEnabled = true ExposurePreferences(context).scannerEnabled = true
} }
database.use { database.noteAppAction(packageName, "start", JSONObject().apply {
it.noteAppAction(packageName, "start", JSONObject().apply { put("result", resultCode)
put("result", resultCode) }.toString())
}.toString())
}
try { try {
params.callback.onResult(Status(if (resultCode == SUCCESS) SUCCESS else FAILED_REJECTED_OPT_IN, resultData?.getString("message"))) params.callback.onResult(Status(if (resultCode == SUCCESS) SUCCESS else FAILED_REJECTED_OPT_IN, resultData?.getString("message")))
} catch (e: Exception) { } catch (e: Exception) {
@ -77,11 +75,9 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
if (resultCode == SUCCESS) { if (resultCode == SUCCESS) {
ExposurePreferences(context).scannerEnabled = false ExposurePreferences(context).scannerEnabled = false
} }
database.use { database.noteAppAction(packageName, "stop", JSONObject().apply {
it.noteAppAction(packageName, "stop", JSONObject().apply { put("result", resultCode)
put("result", resultCode) }.toString())
}.toString())
}
try { try {
params.callback.onResult(Status.SUCCESS) params.callback.onResult(Status.SUCCESS)
} catch (e: Exception) { } catch (e: Exception) {
@ -106,12 +102,10 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
FAILED_REJECTED_OPT_IN to emptyList() FAILED_REJECTED_OPT_IN to emptyList()
} }
database.use { database.noteAppAction(packageName, "getTemporaryExposureKeyHistory", JSONObject().apply {
it.noteAppAction(packageName, "getTemporaryExposureKeyHistory", JSONObject().apply { put("result", resultCode)
put("result", resultCode) put("response_keys_size", response.size)
put("response_keys_size", response.size) }.toString())
}.toString())
}
try { try {
params.callback.onResult(Status(status, resultData?.getString("message")), response) params.callback.onResult(Status(status, resultData?.getString("message")), response)
} catch (e: Exception) { } catch (e: Exception) {
@ -140,50 +134,51 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
} }
override fun provideDiagnosisKeys(params: ProvideDiagnosisKeysParams) { override fun provideDiagnosisKeys(params: ProvideDiagnosisKeysParams) {
database.ref()
Thread(Runnable { Thread(Runnable {
if (params.configuration != null) { try {
database.storeConfiguration(packageName, params.token, params.configuration) if (params.configuration != null) {
} database.storeConfiguration(packageName, params.token, params.configuration)
}
// keys // keys
for (key in params.keys.orEmpty()) { for (key in params.keys.orEmpty()) {
database.storeDiagnosisKey(packageName, params.token, key) database.storeDiagnosisKey(packageName, params.token, key)
} }
// Key files // Key files
var keys = params.keys?.size ?: 0 var keys = params.keys?.size ?: 0
for (file in params.keyFiles.orEmpty()) { for (file in params.keyFiles.orEmpty()) {
try { try {
ZipInputStream(ParcelFileDescriptor.AutoCloseInputStream(file)).use { stream -> ZipInputStream(ParcelFileDescriptor.AutoCloseInputStream(file)).use { stream ->
do { do {
val entry = stream.nextEntry ?: break val entry = stream.nextEntry ?: break
if (entry.name == "export.bin") { if (entry.name == "export.bin") {
val prefix = ByteArray(16) val prefix = ByteArray(16)
var totalBytesRead = 0 var totalBytesRead = 0
var bytesRead = 0 var bytesRead = 0
while (bytesRead != -1 && totalBytesRead < prefix.size) { while (bytesRead != -1 && totalBytesRead < prefix.size) {
bytesRead = stream.read(prefix, totalBytesRead, prefix.size - totalBytesRead) bytesRead = stream.read(prefix, totalBytesRead, prefix.size - totalBytesRead)
if (bytesRead > 0) { if (bytesRead > 0) {
totalBytesRead += bytesRead totalBytesRead += bytesRead
}
}
if (totalBytesRead == prefix.size && String(prefix).trim() == "EK Export v1") {
val fileKeys = storeDiagnosisKeyExport(params.token, TemporaryExposureKeyExport.ADAPTER.decode(stream))
keys + fileKeys
} else {
Log.d(TAG, "export.bin had invalid prefix")
} }
} }
if (totalBytesRead == prefix.size && String(prefix).trim() == "EK Export v1") { stream.closeEntry()
val fileKeys = storeDiagnosisKeyExport(params.token, TemporaryExposureKeyExport.ADAPTER.decode(stream)) } while (true);
keys + fileKeys }
} else { } catch (e: Exception) {
Log.d(TAG, "export.bin had invalid prefix") Log.w(TAG, "Failed parsing file", e)
}
}
stream.closeEntry()
} while (true);
} }
} catch (e: Exception) {
Log.w(TAG, "Failed parsing file", e)
} }
} Log.d(TAG, "$packageName/${params.token} provided $keys keys")
Log.d(TAG, "$packageName/${params.token} provided $keys keys")
Handler(Looper.getMainLooper()).post {
database.noteAppAction(packageName, "provideDiagnosisKeys", JSONObject().apply { database.noteAppAction(packageName, "provideDiagnosisKeys", JSONObject().apply {
put("request_token", params.token) put("request_token", params.token)
put("request_keys_size", params.keys?.size) put("request_keys_size", params.keys?.size)
@ -191,25 +186,27 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
put("request_keys_count", keys) put("request_keys_count", keys)
}.toString()) }.toString())
Handler(Looper.getMainLooper()).post {
try {
params.callback.onResult(Status.SUCCESS)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
}
database.finishMatching(packageName, params.token)
val match = database.findAllMeasuredExposures(packageName, params.token).isNotEmpty()
try { try {
params.callback.onResult(Status.SUCCESS) val intent = Intent(if (match) ACTION_EXPOSURE_STATE_UPDATED else ACTION_EXPOSURE_NOT_FOUND)
intent.`package` = packageName
Log.d(TAG, "Sending $intent")
context.sendOrderedBroadcast(intent, PERMISSION_EXPOSURE_CALLBACK)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Callback failed", e) Log.w(TAG, "Callback failed", e)
} }
} } finally {
database.unref()
val match = database.use {
it.finishMatching(packageName, params.token)
it.findAllMeasuredExposures(packageName, params.token).isNotEmpty()
}
try {
val intent = Intent(if (match) ACTION_EXPOSURE_STATE_UPDATED else ACTION_EXPOSURE_NOT_FOUND)
intent.`package` = packageName
Log.d(TAG, "Sending $intent")
context.sendOrderedBroadcast(intent, PERMISSION_EXPOSURE_CALLBACK)
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
} }
}).start() }).start()
} }
@ -237,18 +234,16 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
.setSummationRiskScore(exposures.map { it.getRiskScore(configuration) }.sum()) .setSummationRiskScore(exposures.map { it.getRiskScore(configuration) }.sum())
.build() .build()
database.use { database.noteAppAction(packageName, "getExposureSummary", JSONObject().apply {
it.noteAppAction(packageName, "getExposureSummary", JSONObject().apply { put("request_token", params.token)
put("request_token", params.token) put("response_days_since", response.daysSinceLastExposure)
put("response_days_since", response.daysSinceLastExposure) put("response_matched_keys", response.matchedKeyCount)
put("response_matched_keys", response.matchedKeyCount) put("response_max_risk", response.maximumRiskScore)
put("response_max_risk", response.maximumRiskScore) put("response_attenuation_durations", JSONArray().apply {
put("response_attenuation_durations", JSONArray().apply { response.attenuationDurationsInMinutes.forEach { put(it) }
response.attenuationDurationsInMinutes.forEach { put(it) } })
}) put("response_summation_risk", response.summationRiskScore)
put("response_summation_risk", response.summationRiskScore) }.toString())
}.toString())
}
try { try {
params.callback.onResult(Status.SUCCESS, response) params.callback.onResult(Status.SUCCESS, response)
} catch (e: Exception) { } catch (e: Exception) {
@ -271,12 +266,10 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
it.toExposureInformation(configuration) it.toExposureInformation(configuration)
} }
database.use { database.noteAppAction(packageName, "getExposureInformation", JSONObject().apply {
database.noteAppAction(packageName, "getExposureInformation", JSONObject().apply { put("request_token", params.token)
put("request_token", params.token) put("response_size", response.size)
put("response_size", response.size) }.toString())
}.toString())
}
try { try {
params.callback.onResult(Status.SUCCESS, response) params.callback.onResult(Status.SUCCESS, response)
} catch (e: Exception) { } catch (e: Exception) {