EN API: Only use a single writable database instance

Should fix some issues with older Android versions, #1115
This commit is contained in:
Marvin W 2020-08-05 14:17:29 +02:00
parent ee176c42cc
commit f30605b145
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
9 changed files with 121 additions and 113 deletions

View File

@ -47,14 +47,14 @@ class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
fun updateContent() { fun updateContent() {
packageName?.let { packageName -> packageName?.let { packageName ->
val database = ExposureDatabase(requireContext()) ExposureDatabase.with(requireContext()) { database ->
var str = getString(R.string.pref_exposure_app_checks_summary, database.countMethodCalls(packageName, "provideDiagnosisKeys")) var str = getString(R.string.pref_exposure_app_checks_summary, database.countMethodCalls(packageName, "provideDiagnosisKeys"))
val lastCheckTime = database.lastMethodCall(packageName, "provideDiagnosisKeys") val lastCheckTime = database.lastMethodCall(packageName, "provideDiagnosisKeys")
if (lastCheckTime != null && lastCheckTime != 0L) { if (lastCheckTime != null && lastCheckTime != 0L) {
str += "\n" + getString(R.string.pref_exposure_app_last_check_summary, DateUtils.getRelativeDateTimeString(context, lastCheckTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME)) str += "\n" + getString(R.string.pref_exposure_app_last_check_summary, DateUtils.getRelativeDateTimeString(context, lastCheckTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME))
}
checks.summary = str
} }
checks.summary = str
database.close()
} }
} }
} }

View File

@ -25,16 +25,10 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
private lateinit var exposureAppsNone: Preference private lateinit var exposureAppsNone: Preference
private lateinit var collectedRpis: Preference private lateinit var collectedRpis: Preference
private lateinit var advertisingId: Preference private lateinit var advertisingId: Preference
private lateinit var database: ExposureDatabase
private val handler = Handler() private val handler = Handler()
private val updateStatusRunnable = Runnable { updateStatus() } private val updateStatusRunnable = Runnable { updateStatus() }
private val updateContentRunnable = Runnable { updateContent() } private val updateContentRunnable = Runnable { updateContent() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
database = ExposureDatabase(requireContext())
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_exposure_notifications) addPreferencesFromResource(R.xml.preferences_exposure_notifications)
} }
@ -53,13 +47,14 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
updateStatus() updateStatus()
updateContent() updateContent()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
database.close()
handler.removeCallbacks(updateStatusRunnable) handler.removeCallbacks(updateStatusRunnable)
handler.removeCallbacks(updateContentRunnable) handler.removeCallbacks(updateContentRunnable)
} }
@ -78,26 +73,27 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
handler.postDelayed(updateContentRunnable, UPDATE_CONTENT_INTERVAL) handler.postDelayed(updateContentRunnable, UPDATE_CONTENT_INTERVAL)
val context = requireContext() val context = requireContext()
val (apps, lastHourKeys, currentId) = withContext(Dispatchers.IO) { val (apps, lastHourKeys, currentId) = withContext(Dispatchers.IO) {
val apps = database.appList.map { packageName -> ExposureDatabase.with(context) { database ->
context.packageManager.getApplicationInfoIfExists(packageName) val apps = database.appList.map { packageName ->
}.filterNotNull().mapIndexed { idx, applicationInfo -> context.packageManager.getApplicationInfoIfExists(packageName)
val pref = AppIconPreference(context) }.filterNotNull().mapIndexed { idx, applicationInfo ->
pref.order = idx val pref = AppIconPreference(context)
pref.title = applicationInfo.loadLabel(context.packageManager) pref.order = idx
pref.icon = applicationInfo.loadIcon(context.packageManager) pref.title = applicationInfo.loadLabel(context.packageManager)
pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { pref.icon = applicationInfo.loadIcon(context.packageManager)
findNavController().navigate(R.id.openExposureAppDetails, bundleOf( pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
"package" to applicationInfo.packageName findNavController().navigate(R.id.openExposureAppDetails, bundleOf(
)) "package" to applicationInfo.packageName
true ))
true
}
pref.key = "pref_exposure_app_" + applicationInfo.packageName
pref
} }
pref.key = "pref_exposure_app_" + applicationInfo.packageName val lastHourKeys = database.hourRpiCount
pref val currentId = database.currentRpiId
Triple(apps, lastHourKeys, currentId)
} }
val lastHourKeys = database.hourRpiCount
val currentId = database.currentRpiId
database.close()
Triple(apps, lastHourKeys, currentId)
} }
collectedRpis.summary = getString(R.string.pref_exposure_collected_rpis_summary, lastHourKeys) collectedRpis.summary = getString(R.string.pref_exposure_collected_rpis_summary, lastHourKeys)
advertisingId.summary = currentId.toString() advertisingId.summary = currentId.toString()

View File

@ -22,12 +22,6 @@ import kotlin.math.roundToInt
class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() { class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() {
private lateinit var histogramCategory: PreferenceCategory private lateinit var histogramCategory: PreferenceCategory
private lateinit var histogram: BarChartPreference private lateinit var histogram: BarChartPreference
private lateinit var database: ExposureDatabase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
database = ExposureDatabase(requireContext())
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_exposure_notifications_rpis) addPreferencesFromResource(R.xml.preferences_exposure_notifications_rpis)
@ -43,28 +37,24 @@ class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() {
updateChart() updateChart()
} }
override fun onPause() {
super.onPause()
database.close()
}
fun updateChart() { fun updateChart() {
lifecycleScope.launchWhenResumed { lifecycleScope.launchWhenResumed {
val (totalRpiCount, rpiHistogram) = withContext(Dispatchers.IO) { val (totalRpiCount, rpiHistogram) = withContext(Dispatchers.IO) {
val map = linkedMapOf<String, Float>() ExposureDatabase.with(requireContext()) { database ->
val lowestDate = Math.round((Date().time / 24 / 60 / 60 / 1000 - 13).toDouble()) * 24 * 60 * 60 * 1000 val map = linkedMapOf<String, Float>()
for (i in 0..13) { val lowestDate = Math.round((Date().time / 24 / 60 / 60 / 1000 - 13).toDouble()) * 24 * 60 * 60 * 1000
val date = Calendar.getInstance().apply { this.time = Date(lowestDate + i * 24 * 60 * 60 * 1000) }.get(Calendar.DAY_OF_MONTH) for (i in 0..13) {
map[date.toString()] = 0f val date = Calendar.getInstance().apply { this.time = Date(lowestDate + i * 24 * 60 * 60 * 1000) }.get(Calendar.DAY_OF_MONTH)
map[date.toString()] = 0f
}
for (entry in database.rpiHistogram) {
val time = Date(entry.key * 24 * 60 * 60 * 1000)
val date = Calendar.getInstance().apply { this.time = time }.get(Calendar.DAY_OF_MONTH)
map[date.toString()] = entry.value.toFloat()
}
val totalRpiCount = database.totalRpiCount
totalRpiCount to map
} }
for (entry in database.rpiHistogram) {
val time = Date(entry.key * 24 * 60 * 60 * 1000)
val date = Calendar.getInstance().apply { this.time = time }.get(Calendar.DAY_OF_MONTH)
map[date.toString()] = entry.value.toFloat()
}
val totalRpiCount = database.totalRpiCount
database.close()
totalRpiCount to map
} }
histogramCategory.title = getString(R.string.prefcat_exposure_rpis_histogram_title, totalRpiCount) histogramCategory.title = getString(R.string.prefcat_exposure_rpis_histogram_title, totalRpiCount)
histogram.labelsFormatter = { it.roundToInt().toString() } histogram.labelsFormatter = { it.roundToInt().toString() }

View File

@ -13,7 +13,7 @@ fun PackageManager.getApplicationInfoIfExists(packageName: String?, flags: Int =
try { try {
getApplicationInfo(it, flags) getApplicationInfo(it, flags)
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Package does not exist", e) Log.w(TAG, "Package $packageName not installed.")
null null
} }
} }

View File

@ -40,7 +40,7 @@ class AdvertiserService : LifecycleService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
database = ExposureDatabase(this) database = ExposureDatabase.ref(this)
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@ -56,7 +56,7 @@ class AdvertiserService : LifecycleService() {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
stopAdvertising() stopAdvertising()
database.close() database.unref()
} }
fun startAdvertising() { fun startAdvertising() {

View File

@ -24,22 +24,8 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger 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 private constructor(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
private val refCount = AtomicInteger(0) private var refCount = 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)
@ -447,6 +433,35 @@ class ExposureDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
fun generateCurrentPayload(metadata: ByteArray) = currentTemporaryExposureKey.generatePayload(currentIntervalNumber.toInt(), metadata) fun generateCurrentPayload(metadata: ByteArray) = currentTemporaryExposureKey.generatePayload(currentIntervalNumber.toInt(), metadata)
override fun getWritableDatabase(): SQLiteDatabase {
if (this != instance) {
throw IllegalStateException("Tried to open writable database from secondary instance")
}
val db = super.getWritableDatabase()
db.enableWriteAheadLogging()
return db
}
override fun close() {
synchronized(Companion) {
super.close()
instance = null
}
}
fun ref(): ExposureDatabase = synchronized(Companion) {
refCount++
return this
}
fun unref() = synchronized(Companion) {
refCount--
if (refCount == 0) {
close()
} else if (refCount < 0) {
throw IllegalStateException("ref/unref mismatch")
}
}
companion object { companion object {
private const val DB_NAME = "exposure.db" private const val DB_NAME = "exposure.db"
@ -457,6 +472,23 @@ class ExposureDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, nu
private const val TABLE_TEK_CHECK = "tek_check" private const val TABLE_TEK_CHECK = "tek_check"
private const val TABLE_DIAGNOSIS = "diagnosis" private const val TABLE_DIAGNOSIS = "diagnosis"
private const val TABLE_CONFIGURATIONS = "configurations" private const val TABLE_CONFIGURATIONS = "configurations"
private var instance: ExposureDatabase? = null
fun ref(context: Context): ExposureDatabase = synchronized(this) {
if (instance == null) {
instance = ExposureDatabase(context.applicationContext)
}
instance!!.ref()
}
fun <T> with(context: Context, call: (ExposureDatabase) -> T): T {
val it = ref(context)
try {
return call(it)
} finally {
it.unref()
}
}
} }
} }

View File

@ -18,21 +18,6 @@ import org.microg.gms.common.GmsService
import org.microg.gms.common.PackageUtils import org.microg.gms.common.PackageUtils
class ExposureNotificationService : BaseService(TAG, GmsService.NEARBY_EXPOSURE) { class ExposureNotificationService : BaseService(TAG, GmsService.NEARBY_EXPOSURE) {
lateinit var database: ExposureDatabase
override fun onDestroy() {
super.onDestroy()
database.unref()
}
override fun onCreate() {
super.onCreate()
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) {
PackageUtils.getAndCheckCallingPackage(this, request.packageName) PackageUtils.getAndCheckCallingPackage(this, request.packageName)
@ -53,7 +38,7 @@ class ExposureNotificationService : BaseService(TAG, GmsService.NEARBY_EXPOSURE)
} }
Log.d(TAG, "handleServiceRequest: " + request.packageName) Log.d(TAG, "handleServiceRequest: " + request.packageName)
callback.onPostInitCompleteWithConnectionInfo(SUCCESS, ExposureNotificationServiceImpl(this, request.packageName, database), ConnectionInfo().apply { callback.onPostInitCompleteWithConnectionInfo(SUCCESS, ExposureNotificationServiceImpl(this, request.packageName), ConnectionInfo().apply {
features = arrayOf(Feature("nearby_exposure_notification", 2)) features = arrayOf(Feature("nearby_exposure_notification", 2))
}) })
} }

View File

@ -26,7 +26,7 @@ import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyProt
import java.util.* import java.util.*
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
class ExposureNotificationServiceImpl(private val context: Context, private val packageName: String, private val database: ExposureDatabase) : INearbyExposureNotificationService.Stub() { class ExposureNotificationServiceImpl(private val context: Context, private val packageName: String) : INearbyExposureNotificationService.Stub() {
private fun confirm(action: String, callback: (resultCode: Int, resultData: Bundle?) -> Unit) { private fun confirm(action: String, callback: (resultCode: Int, resultData: Bundle?) -> Unit) {
val intent = Intent(ACTION_CONFIRM) val intent = Intent(ACTION_CONFIRM)
intent.`package` = context.packageName intent.`package` = context.packageName
@ -59,9 +59,11 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
if (resultCode == SUCCESS) { if (resultCode == SUCCESS) {
ExposurePreferences(context).scannerEnabled = true ExposurePreferences(context).scannerEnabled = true
} }
database.noteAppAction(packageName, "start", JSONObject().apply { ExposureDatabase.with(context) { database ->
put("result", resultCode) database.noteAppAction(packageName, "start", JSONObject().apply {
}.toString()) put("result", resultCode)
}.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) {
@ -75,9 +77,11 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
if (resultCode == SUCCESS) { if (resultCode == SUCCESS) {
ExposurePreferences(context).scannerEnabled = false ExposurePreferences(context).scannerEnabled = false
} }
database.noteAppAction(packageName, "stop", JSONObject().apply { ExposureDatabase.with(context) { database ->
put("result", resultCode) database.noteAppAction(packageName, "stop", JSONObject().apply {
}.toString()) put("result", resultCode)
}.toString())
}
try { try {
params.callback.onResult(Status.SUCCESS) params.callback.onResult(Status.SUCCESS)
} catch (e: Exception) { } catch (e: Exception) {
@ -94,7 +98,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
} }
} }
override fun getTemporaryExposureKeyHistory(params: GetTemporaryExposureKeyHistoryParams) { override fun getTemporaryExposureKeyHistory(params: GetTemporaryExposureKeyHistoryParams): Unit = ExposureDatabase.with(context) { database ->
confirm(CONFIRM_ACTION_START) { resultCode, resultData -> confirm(CONFIRM_ACTION_START) { resultCode, resultData ->
val (status, response) = if (resultCode == SUCCESS) { val (status, response) = if (resultCode == SUCCESS) {
SUCCESS to database.allKeys SUCCESS to database.allKeys
@ -122,7 +126,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
.setTransmissionRiskLevel(transmission_risk_level ?: 0) .setTransmissionRiskLevel(transmission_risk_level ?: 0)
.build() .build()
private fun storeDiagnosisKeyExport(token: String, export: TemporaryExposureKeyExport): Int { private fun storeDiagnosisKeyExport(token: String, export: TemporaryExposureKeyExport): Int = ExposureDatabase.with(context) { database ->
Log.d(TAG, "Importing keys from file ${export.start_timestamp?.let { Date(it * 1000) }} to ${export.end_timestamp?.let { Date(it * 1000) }}") Log.d(TAG, "Importing keys from file ${export.start_timestamp?.let { Date(it * 1000) }} to ${export.end_timestamp?.let { Date(it * 1000) }}")
for (key in export.keys) { for (key in export.keys) {
database.storeDiagnosisKey(packageName, token, key.toKey()) database.storeDiagnosisKey(packageName, token, key.toKey())
@ -130,13 +134,12 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
for (key in export.revised_keys) { for (key in export.revised_keys) {
database.updateDiagnosisKey(packageName, token, key.toKey()) database.updateDiagnosisKey(packageName, token, key.toKey())
} }
return export.keys.size + export.revised_keys.size export.keys.size + export.revised_keys.size
} }
override fun provideDiagnosisKeys(params: ProvideDiagnosisKeysParams) { override fun provideDiagnosisKeys(params: ProvideDiagnosisKeysParams) {
database.ref()
Thread(Runnable { Thread(Runnable {
try { ExposureDatabase.with(context) { database ->
if (params.configuration != null) { if (params.configuration != null) {
database.storeConfiguration(packageName, params.token, params.configuration) database.storeConfiguration(packageName, params.token, params.configuration)
} }
@ -205,13 +208,11 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Callback failed", e) Log.w(TAG, "Callback failed", e)
} }
} finally {
database.unref()
} }
}).start() }).start()
} }
override fun getExposureSummary(params: GetExposureSummaryParams) { override fun getExposureSummary(params: GetExposureSummaryParams): Unit = ExposureDatabase.with(context) { database ->
val configuration = database.loadConfiguration(packageName, params.token) val configuration = database.loadConfiguration(packageName, params.token)
if (configuration == null) { if (configuration == null) {
try { try {
@ -219,7 +220,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Callback failed", e) Log.w(TAG, "Callback failed", e)
} }
return return@with
} }
val exposures = database.findAllMeasuredExposures(packageName, params.token) val exposures = database.findAllMeasuredExposures(packageName, params.token)
val response = ExposureSummary.ExposureSummaryBuilder() val response = ExposureSummary.ExposureSummaryBuilder()
@ -251,7 +252,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
} }
} }
override fun getExposureInformation(params: GetExposureInformationParams) { override fun getExposureInformation(params: GetExposureInformationParams): Unit = ExposureDatabase.with(context) { database ->
// TODO: Notify user? // TODO: Notify user?
val configuration = database.loadConfiguration(packageName, params.token) val configuration = database.loadConfiguration(packageName, params.token)
if (configuration == null) { if (configuration == null) {
@ -260,7 +261,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Callback failed", e) Log.w(TAG, "Callback failed", e)
} }
return return@with
} }
val response = database.findAllMeasuredExposures(packageName, params.token).map { val response = database.findAllMeasuredExposures(packageName, params.token).map {
it.toExposureInformation(configuration) it.toExposureInformation(configuration)

View File

@ -15,12 +15,12 @@ import android.os.IBinder
@TargetApi(21) @TargetApi(21)
class ScannerService : Service() { class ScannerService : Service() {
private var started = false private var started = false
private lateinit var db: ExposureDatabase private lateinit var database: ExposureDatabase
private val callback = object : ScanCallback() { private val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) { override fun onScanResult(callbackType: Int, result: ScanResult?) {
val data = result?.scanRecord?.serviceData?.get(SERVICE_UUID) ?: return val data = result?.scanRecord?.serviceData?.get(SERVICE_UUID) ?: return
if (data.size < 16) return // Ignore invalid advertisements if (data.size < 16) return // Ignore invalid advertisements
db.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi) database.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi)
} }
} }
private val scanner: BluetoothLeScanner private val scanner: BluetoothLeScanner
@ -36,9 +36,15 @@ class ScannerService : Service() {
return START_STICKY return START_STICKY
} }
override fun onCreate() {
super.onCreate()
database = ExposureDatabase.ref(this)
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
stopScan() stopScan()
database.unref()
} }
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
@ -48,7 +54,6 @@ class ScannerService : Service() {
@Synchronized @Synchronized
private fun startScan() { private fun startScan() {
if (started) return if (started) return
db = ExposureDatabase(this)
scanner.startScan( scanner.startScan(
listOf(ScanFilter.Builder().setServiceUuid(SERVICE_UUID).setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0)).build()), listOf(ScanFilter.Builder().setServiceUuid(SERVICE_UUID).setServiceData(SERVICE_UUID, byteArrayOf(0), byteArrayOf(0)).build()),
ScanSettings.Builder().build(), ScanSettings.Builder().build(),
@ -61,7 +66,6 @@ class ScannerService : Service() {
private fun stopScan() { private fun stopScan() {
if (!started) return if (!started) return
scanner.stopScan(callback) scanner.stopScan(callback)
db.close()
started = false started = false
} }
} }