mirror of
https://github.com/TeamVanced/VancedMicroG
synced 2024-11-19 02:29:25 +01:00
EN: Make database access suspendable, add migration routine for oversized databases
This commit is contained in:
parent
c4b480c5a9
commit
1deeb45834
@ -8,6 +8,7 @@ package org.microg.gms.nearby.core.ui
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import org.json.JSONObject
|
||||
@ -48,26 +49,28 @@ class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
fun updateContent() {
|
||||
packageName?.let { packageName ->
|
||||
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))
|
||||
}
|
||||
val lastExposureSummaryTime = database.lastMethodCall(packageName, "getExposureSummary")
|
||||
val lastExposureSummary = database.lastMethodCallArgs(packageName, "getExposureSummary")
|
||||
if (lastExposureSummaryTime != null && lastExposureSummary != null && System.currentTimeMillis() - lastExposureSummaryTime <= TimeUnit.DAYS.toMillis(1)) {
|
||||
try {
|
||||
val json = JSONObject(lastExposureSummary)
|
||||
val matchedKeys = json.optInt("response_matched_keys")
|
||||
val daysSince = json.optInt("response_days_since", -1)
|
||||
if (matchedKeys > 0 && daysSince >= 0) {
|
||||
str += "\n" + resources.getQuantityString(R.plurals.pref_exposure_app_last_report_summary, matchedKeys, matchedKeys, daysSince)
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
lifecycleScope.launchWhenResumed {
|
||||
checks.summary = 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))
|
||||
}
|
||||
val lastExposureSummaryTime = database.lastMethodCall(packageName, "getExposureSummary")
|
||||
val lastExposureSummary = database.lastMethodCallArgs(packageName, "getExposureSummary")
|
||||
if (lastExposureSummaryTime != null && lastExposureSummary != null && System.currentTimeMillis() - lastExposureSummaryTime <= TimeUnit.DAYS.toMillis(1)) {
|
||||
try {
|
||||
val json = JSONObject(lastExposureSummary)
|
||||
val matchedKeys = json.optInt("response_matched_keys")
|
||||
val daysSince = json.optInt("response_days_since", -1)
|
||||
if (matchedKeys > 0 && daysSince >= 0) {
|
||||
str += "\n" + resources.getQuantityString(R.plurals.pref_exposure_app_last_report_summary, matchedKeys, matchedKeys, daysSince)
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
str
|
||||
}
|
||||
checks.summary = str
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,28 +74,26 @@ class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
|
||||
lifecycleScope.launchWhenResumed {
|
||||
handler.postDelayed(updateContentRunnable, UPDATE_CONTENT_INTERVAL)
|
||||
val context = requireContext()
|
||||
val (apps, lastHourKeys, currentId) = withContext(Dispatchers.IO) {
|
||||
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(requireContext(), R.id.openExposureAppDetails, bundleOf(
|
||||
"package" to applicationInfo.packageName
|
||||
))
|
||||
true
|
||||
}
|
||||
pref.key = "pref_exposure_app_" + applicationInfo.packageName
|
||||
pref
|
||||
val (apps, lastHourKeys, currentId) = 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(requireContext(), R.id.openExposureAppDetails, bundleOf(
|
||||
"package" to applicationInfo.packageName
|
||||
))
|
||||
true
|
||||
}
|
||||
val lastHourKeys = database.hourRpiCount
|
||||
val currentId = database.currentRpiId
|
||||
Triple(apps, lastHourKeys, currentId)
|
||||
pref.key = "pref_exposure_app_" + applicationInfo.packageName
|
||||
pref
|
||||
}
|
||||
val lastHourKeys = database.hourRpiCount
|
||||
val currentId = database.currentRpiId
|
||||
Triple(apps, lastHourKeys, currentId)
|
||||
}
|
||||
collectedRpis.summary = getString(R.string.pref_exposure_collected_rpis_summary, lastHourKeys)
|
||||
if (currentId != null) {
|
||||
|
@ -17,6 +17,7 @@ import kotlinx.coroutines.withContext
|
||||
import org.microg.gms.nearby.exposurenotification.ExposureDatabase
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
@TargetApi(21)
|
||||
class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() {
|
||||
@ -39,33 +40,31 @@ class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() {
|
||||
|
||||
fun updateChart() {
|
||||
lifecycleScope.launchWhenResumed {
|
||||
val (totalRpiCount, rpiHistogram) = withContext(Dispatchers.IO) {
|
||||
ExposureDatabase.with(requireContext()) { database ->
|
||||
val map = linkedMapOf<String, Float>()
|
||||
val lowestDate = Math.round((System.currentTimeMillis() / 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)
|
||||
val str = when (i) {
|
||||
0, 13 -> DateFormat.format(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMd"), lowestDate + i * 24 * 60 * 60 * 1000).toString()
|
||||
else -> IntArray(date).joinToString("").replace("0", "\u200B")
|
||||
}
|
||||
map[str] = 0f
|
||||
val (totalRpiCount, rpiHistogram) = ExposureDatabase.with(requireContext()) { database ->
|
||||
val map = linkedMapOf<String, Float>()
|
||||
val lowestDate = (System.currentTimeMillis() / 24 / 60 / 60 / 1000 - 13).toDouble().roundToLong() * 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)
|
||||
val str = when (i) {
|
||||
0, 13 -> DateFormat.format(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMd"), lowestDate + i * 24 * 60 * 60 * 1000).toString()
|
||||
else -> IntArray(date).joinToString("").replace("0", "\u200B")
|
||||
}
|
||||
val refDateLow = Calendar.getInstance().apply { this.time = Date(lowestDate) }.get(Calendar.DAY_OF_MONTH)
|
||||
val refDateHigh = Calendar.getInstance().apply { this.time = Date(lowestDate + 13 * 24 * 60 * 60 * 1000) }.get(Calendar.DAY_OF_MONTH)
|
||||
for (entry in database.rpiHistogram) {
|
||||
val time = Date(entry.key * 24 * 60 * 60 * 1000)
|
||||
if (time.time < lowestDate) continue // Ignore old data
|
||||
val date = Calendar.getInstance().apply { this.time = time }.get(Calendar.DAY_OF_MONTH)
|
||||
val str = when (date) {
|
||||
refDateLow, refDateHigh -> DateFormat.format(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMd"), entry.key * 24 * 60 * 60 * 1000).toString()
|
||||
else -> IntArray(date).joinToString("").replace("0", "\u200B")
|
||||
}
|
||||
map[str] = entry.value.toFloat()
|
||||
}
|
||||
val totalRpiCount = database.totalRpiCount
|
||||
totalRpiCount to map
|
||||
map[str] = 0f
|
||||
}
|
||||
val refDateLow = Calendar.getInstance().apply { this.time = Date(lowestDate) }.get(Calendar.DAY_OF_MONTH)
|
||||
val refDateHigh = Calendar.getInstance().apply { this.time = Date(lowestDate + 13 * 24 * 60 * 60 * 1000) }.get(Calendar.DAY_OF_MONTH)
|
||||
for (entry in database.rpiHistogram) {
|
||||
val time = Date(entry.key * 24 * 60 * 60 * 1000)
|
||||
if (time.time < lowestDate) continue // Ignore old data
|
||||
val date = Calendar.getInstance().apply { this.time = time }.get(Calendar.DAY_OF_MONTH)
|
||||
val str = when (date) {
|
||||
refDateLow, refDateHigh -> DateFormat.format(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMd"), entry.key * 24 * 60 * 60 * 1000).toString()
|
||||
else -> IntArray(date).joinToString("").replace("0", "\u200B")
|
||||
}
|
||||
map[str] = entry.value.toFloat()
|
||||
}
|
||||
val totalRpiCount = database.totalRpiCount
|
||||
totalRpiCount to map
|
||||
}
|
||||
histogramCategory.title = getString(R.string.prefcat_exposure_rpis_histogram_title, totalRpiCount)
|
||||
histogram.labelsFormatter = { it.roundToInt().toString() }
|
||||
|
@ -20,6 +20,10 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.microg.gms.common.ForegroundServiceContext
|
||||
import java.io.FileDescriptor
|
||||
import java.io.PrintWriter
|
||||
@ -27,7 +31,7 @@ import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
@TargetApi(21)
|
||||
class AdvertiserService : Service() {
|
||||
class AdvertiserService : LifecycleService() {
|
||||
private val version = VERSION_1_0
|
||||
private var advertising = false
|
||||
private var wantStartAdvertising = false
|
||||
@ -84,13 +88,13 @@ class AdvertiserService : Service() {
|
||||
handler.removeCallbacks(startLaterRunnable)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
fun startAdvertisingIfNeeded() {
|
||||
private fun startAdvertisingIfNeeded() {
|
||||
if (ExposurePreferences(this).enabled) {
|
||||
startAdvertising()
|
||||
lifecycleScope.launchWhenStarted {
|
||||
withContext(Dispatchers.IO) {
|
||||
startAdvertising()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopSelf()
|
||||
}
|
||||
@ -98,57 +102,65 @@ class AdvertiserService : Service() {
|
||||
|
||||
private var lastStartTime = System.currentTimeMillis()
|
||||
private var sendingBytes = ByteArray(0)
|
||||
private var starting = false
|
||||
|
||||
@Synchronized
|
||||
fun startAdvertising() {
|
||||
if (advertising) return
|
||||
val advertiser = advertiser ?: return
|
||||
wantStartAdvertising = false
|
||||
val aemBytes = when (version) {
|
||||
VERSION_1_0 -> byteArrayOf(
|
||||
version, // Version and flags
|
||||
(currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
|
||||
0x00, // Reserved
|
||||
0x00 // Reserved
|
||||
)
|
||||
VERSION_1_1 -> byteArrayOf(
|
||||
(version + currentDeviceInfo.confidence.toByte() * 4).toByte(), // Version and flags
|
||||
(currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
|
||||
0x00, // Reserved
|
||||
0x00 // Reserved
|
||||
)
|
||||
else -> return
|
||||
private suspend fun startAdvertising() {
|
||||
val advertiser = synchronized(this) {
|
||||
if (advertising || starting) return
|
||||
val advertiser = advertiser ?: return
|
||||
wantStartAdvertising = false
|
||||
starting = true
|
||||
advertiser
|
||||
}
|
||||
var nextSend = nextKeyMillis.coerceAtLeast(10000)
|
||||
val payload = ExposureDatabase.with(this@AdvertiserService) { database ->
|
||||
database.generateCurrentPayload(aemBytes)
|
||||
try {
|
||||
val aemBytes = when (version) {
|
||||
VERSION_1_0 -> byteArrayOf(
|
||||
version, // Version and flags
|
||||
(currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
|
||||
0x00, // Reserved
|
||||
0x00 // Reserved
|
||||
)
|
||||
VERSION_1_1 -> byteArrayOf(
|
||||
(version + currentDeviceInfo.confidence.toByte() * 4).toByte(), // Version and flags
|
||||
(currentDeviceInfo.txPowerCorrection + TX_POWER_LOW).toByte(), // TX power
|
||||
0x00, // Reserved
|
||||
0x00 // Reserved
|
||||
)
|
||||
else -> return
|
||||
}
|
||||
var nextSend = nextKeyMillis.coerceAtLeast(10000)
|
||||
val payload = ExposureDatabase.with(this@AdvertiserService) { database ->
|
||||
database.generateCurrentPayload(aemBytes)
|
||||
}
|
||||
val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, payload).build()
|
||||
val (uuid, _) = ByteBuffer.wrap(payload).let { UUID(it.long, it.long) to it.int }
|
||||
Log.i(TAG, "Starting advertiser for RPI $uuid")
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
setCallback = SetCallback()
|
||||
val params = AdvertisingSetParameters.Builder()
|
||||
.setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM)
|
||||
.setLegacyMode(true)
|
||||
.setTxPowerLevel(AdvertisingSetParameters.TX_POWER_LOW)
|
||||
.setConnectable(false)
|
||||
.build()
|
||||
advertiser.startAdvertisingSet(params, data, null, null, null, setCallback as AdvertisingSetCallback)
|
||||
} else {
|
||||
nextSend = nextSend.coerceAtMost(180000)
|
||||
val settings = Builder()
|
||||
.setTimeout(nextSend.toInt())
|
||||
.setAdvertiseMode(ADVERTISE_MODE_BALANCED)
|
||||
.setTxPowerLevel(ADVERTISE_TX_POWER_LOW)
|
||||
.setConnectable(false)
|
||||
.build()
|
||||
advertiser.startAdvertising(settings, data, callback)
|
||||
}
|
||||
synchronized(this) { advertising = true }
|
||||
sendingBytes = payload
|
||||
lastStartTime = System.currentTimeMillis()
|
||||
scheduleRestartAdvertising(nextSend)
|
||||
} finally {
|
||||
synchronized(this) { starting = false }
|
||||
}
|
||||
val data = AdvertiseData.Builder().addServiceUuid(SERVICE_UUID).addServiceData(SERVICE_UUID, payload).build()
|
||||
val (uuid, _) = ByteBuffer.wrap(payload).let { UUID(it.long, it.long) to it.int }
|
||||
Log.i(TAG, "Starting advertiser for RPI $uuid")
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
setCallback = SetCallback()
|
||||
val params = AdvertisingSetParameters.Builder()
|
||||
.setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM)
|
||||
.setLegacyMode(true)
|
||||
.setTxPowerLevel(AdvertisingSetParameters.TX_POWER_LOW)
|
||||
.setConnectable(false)
|
||||
.build()
|
||||
advertiser.startAdvertisingSet(params, data, null, null, null, setCallback as AdvertisingSetCallback)
|
||||
} else {
|
||||
nextSend = nextSend.coerceAtMost(180000)
|
||||
val settings = Builder()
|
||||
.setTimeout(nextSend.toInt())
|
||||
.setAdvertiseMode(ADVERTISE_MODE_BALANCED)
|
||||
.setTxPowerLevel(ADVERTISE_TX_POWER_LOW)
|
||||
.setConnectable(false)
|
||||
.build()
|
||||
advertiser.startAdvertising(settings, data, callback)
|
||||
}
|
||||
advertising = true
|
||||
sendingBytes = payload
|
||||
lastStartTime = System.currentTimeMillis()
|
||||
scheduleRestartAdvertising(nextSend)
|
||||
}
|
||||
|
||||
override fun dump(fd: FileDescriptor?, writer: PrintWriter?, args: Array<out String>?) {
|
||||
@ -182,7 +194,7 @@ class AdvertiserService : Service() {
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun stopOrRestartAdvertising() {
|
||||
private fun stopOrRestartAdvertising() {
|
||||
if (!advertising) return
|
||||
val (uuid, _) = ByteBuffer.wrap(sendingBytes).let { UUID(it.long, it.long) to it.int }
|
||||
Log.i(TAG, "Stopping advertiser for RPI $uuid")
|
||||
|
@ -14,6 +14,7 @@ import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.microg.gms.common.ForegroundServiceContext
|
||||
|
||||
class CleanupService : LifecycleService() {
|
||||
@ -22,7 +23,7 @@ class CleanupService : LifecycleService() {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
if (isNeeded(this)) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
var workPending = true
|
||||
while (workPending) {
|
||||
ExposureDatabase.with(this@CleanupService) {
|
||||
|
@ -17,16 +17,18 @@ import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
|
||||
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
|
||||
import kotlinx.coroutines.*
|
||||
import okio.ByteString
|
||||
import java.io.File
|
||||
import java.lang.Runnable
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@TargetApi(21)
|
||||
class ExposureDatabase private constructor(private val context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
||||
private var refCount = 0
|
||||
private val createdAt: Exception = Exception("Database ${hashCode()} created")
|
||||
private var refCount = 1
|
||||
|
||||
init {
|
||||
setWriteAheadLoggingEnabled(true)
|
||||
@ -675,27 +677,21 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
|
||||
fun generateCurrentPayload(metadata: ByteArray) = ensureTemporaryExposureKey().generatePayload(currentIntervalNumber.toInt(), metadata)
|
||||
|
||||
override fun getWritableDatabase(): SQLiteDatabase {
|
||||
if (this != instance) {
|
||||
throw IllegalStateException("Tried to open writable database from secondary instance. We are ${hashCode()} but primary is ${instance?.hashCode()}")
|
||||
}
|
||||
requirePrimary(this)
|
||||
return super.getWritableDatabase()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
synchronized(Companion) {
|
||||
super.close()
|
||||
instance = null
|
||||
}
|
||||
}
|
||||
|
||||
fun ref(): ExposureDatabase = synchronized(Companion) {
|
||||
@Synchronized
|
||||
fun ref(): ExposureDatabase {
|
||||
refCount++
|
||||
return this
|
||||
}
|
||||
|
||||
fun unref() = synchronized(Companion) {
|
||||
@Synchronized
|
||||
fun unref() {
|
||||
refCount--
|
||||
if (refCount == 0) {
|
||||
clearInstance(this)
|
||||
close()
|
||||
} else if (refCount < 0) {
|
||||
throw IllegalStateException("ref/unref mismatch")
|
||||
@ -705,6 +701,7 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
|
||||
companion object {
|
||||
private const val DB_NAME = "exposure.db"
|
||||
private const val DB_VERSION = 5
|
||||
private const val DB_SIZE_TOO_LARGE = 256L * 1024 * 1024
|
||||
private const val MAX_DELETE_TIME = 5000L
|
||||
private const val TABLE_ADVERTISEMENTS = "advertisements"
|
||||
private const val TABLE_APP_LOG = "app_log"
|
||||
@ -726,22 +723,137 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
|
||||
@Deprecated(message = "No longer supported")
|
||||
private const val TABLE_CONFIGURATIONS = "configurations"
|
||||
|
||||
private var deferredInstance: Deferred<ExposureDatabase>? = null
|
||||
private var deferredRefCount: Int = 0
|
||||
private var instance: ExposureDatabase? = null
|
||||
fun ref(context: Context): ExposureDatabase = synchronized(this) {
|
||||
if (instance == null) {
|
||||
instance = ExposureDatabase(context.applicationContext)
|
||||
Log.d(TAG, "Created instance ${instance?.hashCode()} of database for ${context.javaClass.name}")
|
||||
|
||||
@Synchronized
|
||||
private fun requirePrimary(database: ExposureDatabase) {
|
||||
if (database != instance) {
|
||||
throw IllegalStateException("Operation requires ${database.hashCode()} to be a primary database instance, but ${instance?.hashCode()} is primary", database.createdAt)
|
||||
}
|
||||
instance!!.ref()
|
||||
}
|
||||
|
||||
fun <T> with(context: Context, call: (ExposureDatabase) -> T): T {
|
||||
val it = ref(context)
|
||||
@Synchronized
|
||||
private fun clearInstance(database: ExposureDatabase) {
|
||||
if (database == instance) {
|
||||
if (deferredRefCount == 0) {
|
||||
deferredInstance = null
|
||||
instance = null
|
||||
}
|
||||
} else {
|
||||
throw IllegalStateException("Tried to remove database instance ${database.hashCode()}, but ${instance?.hashCode()} is primary", database.createdAt)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getDeferredInstance(): Pair<Deferred<ExposureDatabase>, Boolean> {
|
||||
val deferredInstance = deferredInstance
|
||||
deferredRefCount++
|
||||
return when {
|
||||
deferredInstance != null -> deferredInstance to false
|
||||
instance != null -> throw IllegalStateException("No deferred database instance, but instance ${instance?.hashCode()} is primary", instance?.createdAt)
|
||||
else -> {
|
||||
val newInstance = CompletableDeferred<ExposureDatabase>()
|
||||
this.deferredInstance = newInstance
|
||||
newInstance to true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun unrefDeferredInstance() {
|
||||
deferredRefCount--;
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun completeInstance(database: ExposureDatabase) {
|
||||
if (instance != null) {
|
||||
throw IllegalStateException("Tried to make ${database.hashCode()} the primary, but ${instance?.hashCode()} is currently primary", instance?.createdAt)
|
||||
}
|
||||
instance = database
|
||||
}
|
||||
|
||||
private fun prepareDatabaseMigration(context: Context): Pair<File, File> {
|
||||
val dbFile = context.getDatabasePath(DB_NAME)
|
||||
val dbWalFile = context.getDatabasePath("$DB_NAME-wal")
|
||||
val dbMigrateFile = context.getDatabasePath("$DB_NAME-migrate")
|
||||
val dbMigrateWalFile = context.getDatabasePath("$DB_NAME-migrate-wal")
|
||||
if (dbFile.length() + dbWalFile.length() > DB_SIZE_TOO_LARGE) {
|
||||
Log.d(TAG, "Database file is larger than $DB_SIZE_TOO_LARGE, force clean up")
|
||||
if (dbFile.exists()) dbFile.renameTo(dbMigrateFile)
|
||||
if (dbWalFile.exists()) dbWalFile.renameTo(dbMigrateWalFile)
|
||||
}
|
||||
return dbMigrateFile to dbMigrateWalFile
|
||||
}
|
||||
|
||||
private fun finishDatabaseMigration(database: ExposureDatabase, dbMigrateFile: File, dbMigrateWalFile: File) {
|
||||
if (dbMigrateFile.exists()) {
|
||||
val writableDatabase = database.writableDatabase
|
||||
writableDatabase.execSQL("ATTACH DATABASE '${dbMigrateFile.absolutePath}' AS old;")
|
||||
writableDatabase.beginTransaction()
|
||||
try {
|
||||
Log.d(TAG, "Migrating advertisements and TEKs from old database file")
|
||||
writableDatabase.execSQL("INSERT INTO $TABLE_ADVERTISEMENTS SELECT * FROM old.$TABLE_ADVERTISEMENTS;")
|
||||
writableDatabase.execSQL("INSERT INTO $TABLE_TEK SELECT * FROM old.$TABLE_TEK;")
|
||||
Log.d(TAG, "Migration finished successfully")
|
||||
writableDatabase.setTransactionSuccessful()
|
||||
} finally {
|
||||
writableDatabase.endTransaction()
|
||||
writableDatabase.execSQL("DETACH DATABASE old;")
|
||||
}
|
||||
}
|
||||
dbMigrateFile.delete()
|
||||
dbMigrateWalFile.delete()
|
||||
}
|
||||
|
||||
suspend fun ref(context: Context): ExposureDatabase {
|
||||
val (instance, new) = getDeferredInstance()
|
||||
try {
|
||||
if (new) {
|
||||
val newInstance = instance as CompletableDeferred
|
||||
try {
|
||||
val (dbMigrateFile, dbMigrateWalFile) = prepareDatabaseMigration(context)
|
||||
val database = ExposureDatabase(context.applicationContext)
|
||||
try {
|
||||
Log.d(TAG, "Created instance ${database.hashCode()} of database for ${context.javaClass.simpleName}")
|
||||
finishDatabaseMigration(database, dbMigrateFile, dbMigrateWalFile)
|
||||
completeInstance(database)
|
||||
newInstance.complete(database)
|
||||
return database
|
||||
} catch (e: Exception) {
|
||||
database.close()
|
||||
throw e
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
newInstance.completeExceptionally(e)
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
return instance.await().ref()
|
||||
}
|
||||
} finally {
|
||||
unrefDeferredInstance()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(message = "Sync database access is slow", replaceWith = ReplaceWith("with(context, call)"))
|
||||
fun <T> withSync(context: Context, call: (ExposureDatabase) -> T): T {
|
||||
val it = runBlocking { ref(context) }
|
||||
try {
|
||||
return call(it)
|
||||
} finally {
|
||||
it.unref()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> with(context: Context, call: suspend (ExposureDatabase) -> T): T = withContext(Dispatchers.IO) {
|
||||
val it = ref(context)
|
||||
try {
|
||||
call(it)
|
||||
} finally {
|
||||
it.unref()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class ExposureNotificationService : BaseService(TAG, GmsService.NEARBY_EXPOSURE)
|
||||
}
|
||||
|
||||
Log.d(TAG, "handleServiceRequest: " + request.packageName)
|
||||
callback.onPostInitCompleteWithConnectionInfo(SUCCESS, ExposureNotificationServiceImpl(this, request.packageName), ConnectionInfo().apply {
|
||||
callback.onPostInitCompleteWithConnectionInfo(SUCCESS, ExposureNotificationServiceImpl(this, lifecycle, request.packageName), ConnectionInfo().apply {
|
||||
features = arrayOf(
|
||||
Feature("nearby_exposure_notification", 3),
|
||||
Feature("nearby_exposure_notification_get_version", 1)
|
||||
|
@ -12,9 +12,11 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.gms.common.api.Status
|
||||
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
|
||||
import com.google.android.gms.nearby.exposurenotification.ExposureInformation
|
||||
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
|
||||
import com.google.android.gms.nearby.exposurenotification.ExposureSummary
|
||||
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
|
||||
@ -29,12 +31,14 @@ import org.microg.gms.nearby.exposurenotification.proto.TemporaryExposureKeyProt
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.zip.ZipFile
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.random.Random
|
||||
|
||||
class ExposureNotificationServiceImpl(private val context: Context, private val packageName: String) : INearbyExposureNotificationService.Stub() {
|
||||
class ExposureNotificationServiceImpl(private val context: Context, private val lifecycle: Lifecycle, private val packageName: String) : INearbyExposureNotificationService.Stub(), LifecycleOwner {
|
||||
|
||||
override fun getLifecycle(): Lifecycle = lifecycle
|
||||
|
||||
private fun pendingConfirm(permission: String): PendingIntent {
|
||||
val intent = Intent(ACTION_CONFIRM)
|
||||
intent.`package` = context.packageName
|
||||
@ -43,7 +47,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
|
||||
intent.putExtra(KEY_CONFIRM_RECEIVER, object : ResultReceiver(null) {
|
||||
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
ExposureDatabase.with(context) { database -> database.grantPermission(packageName, PackageUtils.firstSignatureDigest(context, packageName)!!, permission) }
|
||||
tempGrantedPermissions.add(packageName to permission)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -58,10 +62,14 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
|
||||
return pi
|
||||
}
|
||||
|
||||
private fun confirmPermission(permission: String): Status {
|
||||
private suspend fun confirmPermission(permission: String): Status {
|
||||
if (packageName == context.packageName) return Status.SUCCESS
|
||||
return ExposureDatabase.with(context) { database ->
|
||||
if (!database.hasPermission(packageName, PackageUtils.firstSignatureDigest(context, packageName)!!, permission)) {
|
||||
if (tempGrantedPermissions.contains(packageName to permission)) {
|
||||
database.grantPermission(packageName, PackageUtils.firstSignatureDigest(context, packageName)!!, permission)
|
||||
tempGrantedPermissions.remove(packageName to permission)
|
||||
Status.SUCCESS
|
||||
} else if (!database.hasPermission(packageName, PackageUtils.firstSignatureDigest(context, packageName)!!, permission)) {
|
||||
Status(RESOLUTION_REQUIRED, "Permission EN#$permission required.", pendingConfirm(permission))
|
||||
} else {
|
||||
Status.SUCCESS
|
||||
@ -79,29 +87,34 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
|
||||
|
||||
override fun start(params: StartParams) {
|
||||
if (ExposurePreferences(context).enabled) return
|
||||
val status = confirmPermission(CONFIRM_ACTION_START)
|
||||
if (status.isSuccess) {
|
||||
ExposurePreferences(context).enabled = true
|
||||
ExposureDatabase.with(context) { database -> database.noteAppAction(packageName, "start") }
|
||||
}
|
||||
try {
|
||||
params.callback.onResult(status)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Callback failed", e)
|
||||
lifecycleScope.launchWhenStarted {
|
||||
val status = confirmPermission(CONFIRM_ACTION_START)
|
||||
if (status.isSuccess) {
|
||||
ExposurePreferences(context).enabled = true
|
||||
ExposureDatabase.with(context) { database -> database.noteAppAction(packageName, "start") }
|
||||
}
|
||||
try {
|
||||
params.callback.onResult(status)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Callback failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop(params: StopParams) {
|
||||
ExposurePreferences(context).enabled = false
|
||||
ExposureDatabase.with(context) { database ->
|
||||
database.noteAppAction(packageName, "stop")
|
||||
}
|
||||
try {
|
||||
params.callback.onResult(Status.SUCCESS)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Callback failed", e)
|
||||
lifecycleScope.launchWhenStarted {
|
||||
ExposurePreferences(context).enabled = false
|
||||
ExposureDatabase.with(context) { database ->
|
||||
database.noteAppAction(packageName, "stop")
|
||||
}
|
||||
try {
|
||||
params.callback.onResult(Status.SUCCESS)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Callback failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isEnabled(params: IsEnabledParams) {
|
||||
try {
|
||||
params.callback.onResult(Status.SUCCESS, ExposurePreferences(context).enabled)
|
||||
@ -111,24 +124,26 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
|
||||
}
|
||||
|
||||
override fun getTemporaryExposureKeyHistory(params: GetTemporaryExposureKeyHistoryParams) {
|
||||
val status = confirmPermission(CONFIRM_ACTION_KEYS)
|
||||
val response = when {
|
||||
status.isSuccess -> ExposureDatabase.with(context) { database ->
|
||||
database.allKeys
|
||||
lifecycleScope.launchWhenStarted {
|
||||
val status = confirmPermission(CONFIRM_ACTION_KEYS)
|
||||
val response = when {
|
||||
status.isSuccess -> ExposureDatabase.with(context) { database ->
|
||||
database.allKeys
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,7 +172,7 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
|
||||
digest()
|
||||
}
|
||||
|
||||
private fun buildExposureSummary(token: String): ExposureSummary = ExposureDatabase.with(context) { database ->
|
||||
private suspend fun buildExposureSummary(token: String): ExposureSummary = ExposureDatabase.with(context) { database ->
|
||||
val pair = database.loadConfiguration(packageName, token)
|
||||
val (configuration, exposures) = if (pair != null) {
|
||||
pair.second to database.findAllMeasuredExposures(pair.first).merge()
|
||||
@ -180,23 +195,23 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
|
||||
|
||||
override fun provideDiagnosisKeys(params: ProvideDiagnosisKeysParams) {
|
||||
Log.w(TAG, "provideDiagnosisKeys() with $packageName/${params.token}")
|
||||
val tid = ExposureDatabase.with(context) { database ->
|
||||
if (params.configuration != null) {
|
||||
database.storeConfiguration(packageName, params.token, params.configuration)
|
||||
} else {
|
||||
database.getTokenId(packageName, params.token)
|
||||
lifecycleScope.launchWhenStarted {
|
||||
val tid = ExposureDatabase.with(context) { database ->
|
||||
if (params.configuration != null) {
|
||||
database.storeConfiguration(packageName, params.token, params.configuration)
|
||||
} else {
|
||||
database.getTokenId(packageName, params.token)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tid == null) {
|
||||
Log.w(TAG, "Unknown token without configuration: $packageName/${params.token}")
|
||||
try {
|
||||
params.callback.onResult(Status.INTERNAL_ERROR)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Callback failed", e)
|
||||
if (tid == null) {
|
||||
Log.w(TAG, "Unknown token without configuration: $packageName/${params.token}")
|
||||
try {
|
||||
params.callback.onResult(Status.INTERNAL_ERROR)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Callback failed", e)
|
||||
}
|
||||
return@launchWhenStarted
|
||||
}
|
||||
return
|
||||
}
|
||||
Thread(Runnable {
|
||||
ExposureDatabase.with(context) { database ->
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
@ -293,47 +308,55 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
|
||||
Log.w(TAG, "Callback failed", e)
|
||||
}
|
||||
}
|
||||
}).start()
|
||||
}
|
||||
|
||||
override fun getExposureSummary(params: GetExposureSummaryParams): Unit = ExposureDatabase.with(context) { database ->
|
||||
val response = buildExposureSummary(params.token)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getExposureInformation(params: GetExposureInformationParams): Unit = ExposureDatabase.with(context) { database ->
|
||||
val pair = database.loadConfiguration(packageName, params.token)
|
||||
val response = if (pair != null) {
|
||||
database.findAllMeasuredExposures(pair.first).merge().map {
|
||||
it.toExposureInformation(pair.second)
|
||||
override fun getExposureSummary(params: GetExposureSummaryParams) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
override fun getExposureInformation(params: GetExposureInformationParams) {
|
||||
lifecycleScope.launchWhenStarted {
|
||||
ExposureDatabase.with(context) { database ->
|
||||
val pair = database.loadConfiguration(packageName, params.token)
|
||||
val response = if (pair != null) {
|
||||
database.findAllMeasuredExposures(pair.first).merge().map {
|
||||
it.toExposureInformation(pair.second)
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -362,4 +385,8 @@ class ExposureNotificationServiceImpl(private val context: Context, private val
|
||||
Log.d(TAG, "onTransact [unknown]: $code, $data, $flags")
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val tempGrantedPermissions: MutableSet<Pair<String, String>> = hashSetOf()
|
||||
}
|
||||
}
|
||||
|
@ -18,13 +18,15 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import org.microg.gms.common.ForegroundServiceContext
|
||||
import java.io.FileDescriptor
|
||||
import java.io.PrintWriter
|
||||
import java.util.*
|
||||
|
||||
@TargetApi(21)
|
||||
class ScannerService : Service() {
|
||||
class ScannerService : LifecycleService() {
|
||||
private var scanning = false
|
||||
private var lastStartTime = 0L
|
||||
private var seenAdvertisements = 0L
|
||||
@ -81,11 +83,13 @@ class ScannerService : Service() {
|
||||
fun onScanResult(result: ScanResult) {
|
||||
val data = result.scanRecord?.serviceData?.get(SERVICE_UUID) ?: return
|
||||
if (data.size < 16) return // Ignore invalid advertisements
|
||||
ExposureDatabase.with(this) { database ->
|
||||
database.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi)
|
||||
}
|
||||
seenAdvertisements++
|
||||
lastAdvertisement = System.currentTimeMillis()
|
||||
lifecycleScope.launchWhenStarted {
|
||||
ExposureDatabase.with(this@ScannerService) { database ->
|
||||
database.noteAdvertisement(data.sliceArray(0..15), data.drop(16).toByteArray(), result.rssi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startScanIfNeeded() {
|
||||
@ -107,10 +111,6 @@ class ScannerService : Service() {
|
||||
stopScan()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
@SuppressLint("WakelockTimeout")
|
||||
@Synchronized
|
||||
private fun startScan() {
|
||||
|
Loading…
Reference in New Issue
Block a user