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

View File

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

View File

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

View File

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

View File

@ -24,22 +24,8 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.roundToInt
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")
}
}
class ExposureDatabase private constructor(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
private var refCount = 0
override fun onCreate(db: SQLiteDatabase) {
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)
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 {
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_DIAGNOSIS = "diagnosis"
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
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) {
PackageUtils.getAndCheckCallingPackage(this, request.packageName)
@ -53,7 +38,7 @@ class ExposureNotificationService : BaseService(TAG, GmsService.NEARBY_EXPOSURE)
}
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))
})
}

View File

@ -26,7 +26,7 @@ import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyProt
import java.util.*
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) {
val intent = Intent(ACTION_CONFIRM)
intent.`package` = context.packageName
@ -59,9 +59,11 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
if (resultCode == SUCCESS) {
ExposurePreferences(context).scannerEnabled = true
}
database.noteAppAction(packageName, "start", JSONObject().apply {
put("result", resultCode)
}.toString())
ExposureDatabase.with(context) { database ->
database.noteAppAction(packageName, "start", JSONObject().apply {
put("result", resultCode)
}.toString())
}
try {
params.callback.onResult(Status(if (resultCode == SUCCESS) SUCCESS else FAILED_REJECTED_OPT_IN, resultData?.getString("message")))
} catch (e: Exception) {
@ -75,9 +77,11 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
if (resultCode == SUCCESS) {
ExposurePreferences(context).scannerEnabled = false
}
database.noteAppAction(packageName, "stop", JSONObject().apply {
put("result", resultCode)
}.toString())
ExposureDatabase.with(context) { database ->
database.noteAppAction(packageName, "stop", JSONObject().apply {
put("result", resultCode)
}.toString())
}
try {
params.callback.onResult(Status.SUCCESS)
} 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 ->
val (status, response) = if (resultCode == SUCCESS) {
SUCCESS to database.allKeys
@ -122,7 +126,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
.setTransmissionRiskLevel(transmission_risk_level ?: 0)
.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) }}")
for (key in export.keys) {
database.storeDiagnosisKey(packageName, token, key.toKey())
@ -130,13 +134,12 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
for (key in export.revised_keys) {
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) {
database.ref()
Thread(Runnable {
try {
ExposureDatabase.with(context) { database ->
if (params.configuration != null) {
database.storeConfiguration(packageName, params.token, params.configuration)
}
@ -205,13 +208,11 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
} finally {
database.unref()
}
}).start()
}
override fun getExposureSummary(params: GetExposureSummaryParams) {
override fun getExposureSummary(params: GetExposureSummaryParams): Unit = ExposureDatabase.with(context) { database ->
val configuration = database.loadConfiguration(packageName, params.token)
if (configuration == null) {
try {
@ -219,7 +220,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
return
return@with
}
val exposures = database.findAllMeasuredExposures(packageName, params.token)
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?
val configuration = database.loadConfiguration(packageName, params.token)
if (configuration == null) {
@ -260,7 +261,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
} catch (e: Exception) {
Log.w(TAG, "Callback failed", e)
}
return
return@with
}
val response = database.findAllMeasuredExposures(packageName, params.token).map {
it.toExposureInformation(configuration)

View File

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