diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsAppPreferencesFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsAppPreferencesFragment.kt index 2b1c7c55..bfdfbe1a 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsAppPreferencesFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsAppPreferencesFragment.kt @@ -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() } } } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsPreferencesFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsPreferencesFragment.kt index d5b1b351..6d27fa22 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsPreferencesFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsPreferencesFragment.kt @@ -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() diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsRpisFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsRpisFragment.kt index 7fd1d14e..70ff7b54 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsRpisFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/ExposureNotificationsRpisFragment.kt @@ -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() - 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() + 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() } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/ui/Utils.kt b/play-services-core/src/main/kotlin/org/microg/gms/ui/Utils.kt index 59af6756..aac8b144 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/ui/Utils.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/ui/Utils.kt @@ -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 } } diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt index b058f4d9..7f2858da 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/AdvertiserService.kt @@ -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() { diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt index 2c409148..077be955 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt @@ -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 with(context: Context, call: (ExposureDatabase) -> T): T { + val it = ref(context) + try { + return call(it) + } finally { + it.unref() + } + } } } diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt index c2f0e06f..045fb70c 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationService.kt @@ -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)) }) } diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt index 0826a180..738553ff 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureNotificationServiceImpl.kt @@ -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) diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt index a33503b9..3bb000c5 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ScannerService.kt @@ -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 } }