2020-08-03 16:07:06 +00:00
/ *
* SPDX - FileCopyrightText : 2020 , microG Project Team
* SPDX - License - Identifier : Apache - 2.0
* /
package org.microg.gms.nearby.exposurenotification
2020-08-24 08:12:49 +00:00
import android.annotation.TargetApi
2020-08-03 16:07:06 +00:00
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteCursor
import android.database.sqlite.SQLiteDatabase
2020-08-24 08:12:49 +00:00
import android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE
2020-08-03 16:07:06 +00:00
import android.database.sqlite.SQLiteOpenHelper
import android.os.Parcel
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 java.nio.ByteBuffer
import java.util.*
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
2020-08-24 08:12:49 +00:00
@TargetApi ( 21 )
class ExposureDatabase private constructor ( private val context : Context ) : SQLiteOpenHelper ( context , DB _NAME , null , DB _VERSION ) {
2020-08-05 12:17:29 +00:00
private var refCount = 0
2020-08-03 16:07:06 +00:00
2020-08-24 08:12:49 +00:00
init {
setWriteAheadLoggingEnabled ( true )
}
2020-09-06 12:59:41 +00:00
override fun onConfigure ( db : SQLiteDatabase ) {
super . onConfigure ( db )
db . setForeignKeyConstraintsEnabled ( true )
}
2020-08-03 16:07:06 +00:00
override fun onCreate ( db : SQLiteDatabase ) {
onUpgrade ( db , 0 , DB _VERSION )
}
override fun onUpgrade ( db : SQLiteDatabase , oldVersion : Int , newVersion : Int ) {
if ( oldVersion < 1 ) {
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _ADVERTISEMENTS(rpi BLOB NOT NULL, aem BLOB NOT NULL, timestamp INTEGER NOT NULL, rssi INTEGER NOT NULL, duration INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(rpi, timestamp)); " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS index_ ${TABLE_ADVERTISEMENTS} _rpi ON $TABLE _ADVERTISEMENTS(rpi); " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS index_ ${TABLE_ADVERTISEMENTS} _timestamp ON $TABLE _ADVERTISEMENTS(timestamp); " )
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _APP_LOG(package TEXT NOT NULL, timestamp INTEGER NOT NULL, method TEXT NOT NULL, args TEXT); " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS index_ ${TABLE_APP_LOG} _package_timestamp ON $TABLE _APP_LOG(package, timestamp); " )
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _TEK(keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL); " )
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _CONFIGURATIONS(package TEXT NOT NULL, token TEXT NOT NULL, configuration BLOB, PRIMARY KEY(package, token)) " )
}
2020-08-24 08:12:49 +00:00
if ( oldVersion < 2 ) {
db . execSQL ( " DROP TABLE IF EXISTS $TABLE _TEK_CHECK; " )
db . execSQL ( " DROP TABLE IF EXISTS $TABLE _DIAGNOSIS; " )
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _TEK_CHECK(tcid INTEGER PRIMARY KEY, keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, matched INTEGER, UNIQUE(keyData, rollingStartNumber, rollingPeriod)); " )
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _DIAGNOSIS(package TEXT NOT NULL, token TEXT NOT NULL, tcid INTEGER REFERENCES $TABLE _TEK_CHECK(tcid) ON DELETE CASCADE, transmissionRiskLevel INTEGER NOT NULL); " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS index_ ${TABLE_DIAGNOSIS} _package_token ON $TABLE _DIAGNOSIS(package, token); " )
}
2020-09-05 21:51:00 +00:00
if ( oldVersion < 3 ) {
db . execSQL ( " CREATE TABLE $TABLE _APP_PERMS(package TEXT NOT NULL, sig TEXT NOT NULL, perm TEXT NOT NULL, timestamp INTEGER NOT NULL); " )
}
2020-08-03 16:07:06 +00:00
}
2020-08-24 08:12:49 +00:00
fun SQLiteDatabase . delete ( table : String , whereClause : String , args : LongArray ) : Int =
compileStatement ( " DELETE FROM $table WHERE $whereClause " ) . use {
args . forEachIndexed { idx , l -> it . bindLong ( idx + 1 , l ) }
it . executeUpdateDelete ( )
}
2020-08-03 16:07:06 +00:00
fun dailyCleanup ( ) = writableDatabase . run {
2020-09-06 12:59:41 +00:00
beginTransaction ( )
try {
val rollingStartTime = currentRollingStartNumber * ROLLING _WINDOW _LENGTH * 1000 - TimeUnit . DAYS . toMillis ( KEEP_DAYS . toLong ( ) )
val advertisements = delete ( TABLE _ADVERTISEMENTS , " timestamp < ? " , longArrayOf ( rollingStartTime ) )
val appLogEntries = delete ( TABLE _APP _LOG , " timestamp < ? " , longArrayOf ( rollingStartTime ) )
val temporaryExposureKeys = delete ( TABLE _TEK , " (rollingStartNumber + rollingPeriod) < ? " , longArrayOf ( rollingStartTime / ROLLING _WINDOW _LENGTH _MS ) )
val checkedTemporaryExposureKeys = delete ( TABLE _TEK _CHECK , " (rollingStartNumber + rollingPeriod) < ? " , longArrayOf ( rollingStartTime / ROLLING _WINDOW _LENGTH _MS ) )
val appPerms = delete ( TABLE _APP _PERMS , " timestamp < ? " , longArrayOf ( System . currentTimeMillis ( ) - CONFIRM _PERMISSION _VALIDITY ) )
Log . d ( TAG , " Deleted on daily cleanup: $advertisements adv, $appLogEntries applogs, $temporaryExposureKeys teks, $checkedTemporaryExposureKeys cteks, $appPerms perms " )
setTransactionSuccessful ( )
} finally {
endTransaction ( )
}
2020-09-05 21:51:00 +00:00
}
fun grantPermission ( packageName : String , signatureDigest : String , permission : String , timestamp : Long = System . currentTimeMillis ( ) ) = writableDatabase . run {
insert ( TABLE _APP _PERMS , " NULL " , ContentValues ( ) . apply {
put ( " package " , packageName )
put ( " sig " , signatureDigest )
put ( " perm " , permission )
put ( " timestamp " , timestamp )
} )
}
fun hasPermission ( packageName : String , signatureDigest : String , permission : String , maxAge : Long = CONFIRM _PERMISSION _VALIDITY ) = readableDatabase . run {
query ( TABLE _APP _PERMS , arrayOf ( " MAX(timestamp) " ) , " package = ? AND sig = ? and perm = ? " , arrayOf ( packageName , signatureDigest , permission ) , null , null , null ) . use { cursor ->
cursor . moveToNext ( ) && cursor . getLong ( 0 ) + maxAge > System . currentTimeMillis ( )
}
2020-08-03 16:07:06 +00:00
}
fun noteAdvertisement ( rpi : ByteArray , aem : ByteArray , rssi : Int , timestamp : Long = Date ( ) . time ) = writableDatabase . run {
val update = compileStatement ( " UPDATE $TABLE _ADVERTISEMENTS SET rssi = ((rssi * duration) + (? * (? - timestamp - duration)) / (? - timestamp)), duration = (? - timestamp) WHERE rpi = ? AND timestamp > ? AND timestamp < ? " ) . run {
bindLong ( 1 , rssi . toLong ( ) )
bindLong ( 2 , timestamp )
bindLong ( 3 , timestamp )
bindLong ( 4 , timestamp )
bindBlob ( 5 , rpi )
bindLong ( 6 , timestamp - ALLOWED _KEY _OFFSET _MS )
bindLong ( 7 , timestamp + ALLOWED _KEY _OFFSET _MS )
executeUpdateDelete ( )
}
if ( update <= 0 ) {
insert ( TABLE _ADVERTISEMENTS , " NULL " , ContentValues ( ) . apply {
put ( " rpi " , rpi )
put ( " aem " , aem )
put ( " timestamp " , timestamp )
put ( " rssi " , rssi )
put ( " duration " , MINIMUM _EXPOSURE _DURATION _MS )
} )
}
}
fun deleteAllCollectedAdvertisements ( ) = writableDatabase . run {
delete ( TABLE _ADVERTISEMENTS , null , null )
2020-08-24 08:12:49 +00:00
update ( TABLE _TEK _CHECK , ContentValues ( ) . apply {
2020-08-03 16:07:06 +00:00
put ( " matched " , 0 )
} , null , null )
}
fun noteAppAction ( packageName : String , method : String , args : String ? = null , timestamp : Long = Date ( ) . time ) = writableDatabase . run {
insert ( TABLE _APP _LOG , " NULL " , ContentValues ( ) . apply {
put ( " package " , packageName )
put ( " timestamp " , timestamp )
put ( " method " , method )
put ( " args " , args )
} )
}
2020-09-06 12:59:41 +00:00
private fun storeOwnKey ( key : TemporaryExposureKey , database : SQLiteDatabase = writableDatabase ) = database . run {
2020-08-03 16:07:06 +00:00
insert ( TABLE _TEK , " NULL " , ContentValues ( ) . apply {
put ( " keyData " , key . keyData )
put ( " rollingStartNumber " , key . rollingStartIntervalNumber )
put ( " rollingPeriod " , key . rollingPeriod )
} )
}
2020-09-06 12:59:41 +00:00
private fun getTekCheckId ( key : TemporaryExposureKey , mayInsert : Boolean = false , database : SQLiteDatabase = if ( mayInsert ) writableDatabase else readableDatabase ) : Long ? = database . run {
2020-08-24 08:12:49 +00:00
if ( mayInsert ) {
insertWithOnConflict ( TABLE _TEK _CHECK , " NULL " , ContentValues ( ) . apply {
put ( " keyData " , key . keyData )
put ( " rollingStartNumber " , key . rollingStartIntervalNumber )
put ( " rollingPeriod " , key . rollingPeriod )
} , CONFLICT _IGNORE )
}
compileStatement ( " SELECT tcid FROM $TABLE _TEK_CHECK WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ? " ) . use {
it . bindBlob ( 1 , key . keyData )
it . bindLong ( 2 , key . rollingStartIntervalNumber . toLong ( ) )
it . bindLong ( 3 , key . rollingPeriod . toLong ( ) )
it . simpleQueryForLong ( )
}
}
2020-09-06 12:59:41 +00:00
fun storeDiagnosisKey ( packageName : String , token : String , key : TemporaryExposureKey , database : SQLiteDatabase = writableDatabase ) = database . run {
val tcid = getTekCheckId ( key , true , database )
2020-08-03 16:07:06 +00:00
insert ( TABLE _DIAGNOSIS , " NULL " , ContentValues ( ) . apply {
put ( " package " , packageName )
put ( " token " , token )
2020-08-24 08:12:49 +00:00
put ( " tcid " , tcid )
2020-08-03 16:07:06 +00:00
put ( " transmissionRiskLevel " , key . transmissionRiskLevel )
} )
}
2020-09-06 12:59:41 +00:00
fun batchStoreDiagnosisKey ( packageName : String , token : String , keys : List < TemporaryExposureKey > , database : SQLiteDatabase = writableDatabase ) = database . run {
beginTransaction ( )
try {
keys . forEach { storeDiagnosisKey ( packageName , token , it , database ) }
setTransactionSuccessful ( )
} finally {
endTransaction ( )
}
}
fun updateDiagnosisKey ( packageName : String , token : String , key : TemporaryExposureKey , database : SQLiteDatabase = writableDatabase ) = database . run {
val tcid = getTekCheckId ( key , false , database ) ?: return 0
2020-08-24 08:12:49 +00:00
compileStatement ( " UPDATE $TABLE _DIAGNOSIS SET transmissionRiskLevel = ? WHERE package = ? AND token = ? AND tcid = ?; " ) . use {
it . bindLong ( 1 , key . transmissionRiskLevel . toLong ( ) )
it . bindString ( 2 , packageName )
it . bindString ( 3 , token )
it . bindLong ( 4 , tcid )
2020-08-03 16:07:06 +00:00
it . executeUpdateDelete ( )
}
}
2020-09-06 12:59:41 +00:00
fun batchUpdateDiagnosisKey ( packageName : String , token : String , keys : List < TemporaryExposureKey > , database : SQLiteDatabase = writableDatabase ) = database . run {
beginTransaction ( )
try {
keys . forEach { updateDiagnosisKey ( packageName , token , it , database ) }
setTransactionSuccessful ( )
} finally {
endTransaction ( )
}
}
private fun listDiagnosisKeysPendingSearch ( packageName : String , token : String , database : SQLiteDatabase = readableDatabase ) = database . run {
2020-08-03 16:07:06 +00:00
rawQuery ( """
2020-08-24 08:12:49 +00:00
SELECT $ TABLE_TEK_CHECK . keyData , $ TABLE_TEK_CHECK . rollingStartNumber , $ TABLE_TEK_CHECK . rollingPeriod
2020-08-03 16:07:06 +00:00
FROM $ TABLE _DIAGNOSIS
2020-08-24 08:12:49 +00:00
LEFT JOIN $ TABLE _TEK _CHECK ON $ TABLE_DIAGNOSIS . tcid = $ TABLE_TEK_CHECK . tcid
2020-08-03 16:07:06 +00:00
WHERE
$ TABLE_DIAGNOSIS . package = ? AND
$ TABLE_DIAGNOSIS . token = ? AND
$ TABLE_TEK_CHECK . matched IS NULL
""" , arrayOf(packageName, token)).use { cursor ->
val list = arrayListOf < TemporaryExposureKey > ( )
while ( cursor . moveToNext ( ) ) {
list . add ( TemporaryExposureKey . TemporaryExposureKeyBuilder ( )
. setKeyData ( cursor . getBlob ( 0 ) )
. setRollingStartIntervalNumber ( cursor . getLong ( 1 ) . toInt ( ) )
. setRollingPeriod ( cursor . getLong ( 2 ) . toInt ( ) )
. build ( ) )
}
list
}
}
2020-09-06 12:59:41 +00:00
private fun applyDiagnosisKeySearchResult ( key : TemporaryExposureKey , matched : Boolean , database : SQLiteDatabase = writableDatabase ) = database . run {
2020-08-24 08:12:49 +00:00
compileStatement ( " UPDATE $TABLE _TEK_CHECK SET matched = ? WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ?; " ) . use {
it . bindLong ( 1 , if ( matched ) 1 else 0 )
it . bindBlob ( 2 , key . keyData )
it . bindLong ( 3 , key . rollingStartIntervalNumber . toLong ( ) )
it . bindLong ( 4 , key . rollingPeriod . toLong ( ) )
it . executeUpdateDelete ( )
}
2020-08-03 16:07:06 +00:00
}
2020-09-06 12:59:41 +00:00
private fun listMatchedDiagnosisKeys ( packageName : String , token : String , database : SQLiteDatabase = readableDatabase ) = database . run {
2020-08-03 16:07:06 +00:00
rawQuery ( """
2020-08-24 08:12:49 +00:00
SELECT $ TABLE_TEK_CHECK . keyData , $ TABLE_TEK_CHECK . rollingStartNumber , $ TABLE_TEK_CHECK . rollingPeriod , $ TABLE_DIAGNOSIS . transmissionRiskLevel
2020-08-03 16:07:06 +00:00
FROM $ TABLE _DIAGNOSIS
2020-08-24 08:12:49 +00:00
LEFT JOIN $ TABLE _TEK _CHECK ON $ TABLE_DIAGNOSIS . tcid = $ TABLE_TEK_CHECK . tcid
2020-08-03 16:07:06 +00:00
WHERE
$ TABLE_DIAGNOSIS . package = ? AND
$ TABLE_DIAGNOSIS . token = ? AND
$ TABLE_TEK_CHECK . matched = 1
""" , arrayOf(packageName, token)).use { cursor ->
val list = arrayListOf < TemporaryExposureKey > ( )
while ( cursor . moveToNext ( ) ) {
list . add ( TemporaryExposureKey . TemporaryExposureKeyBuilder ( )
. setKeyData ( cursor . getBlob ( 0 ) )
. setRollingStartIntervalNumber ( cursor . getLong ( 1 ) . toInt ( ) )
. setRollingPeriod ( cursor . getLong ( 2 ) . toInt ( ) )
. setTransmissionRiskLevel ( cursor . getLong ( 3 ) . toInt ( ) )
. build ( ) )
}
list
}
}
2020-09-06 12:59:41 +00:00
fun finishMatching ( packageName : String , token : String , database : SQLiteDatabase = writableDatabase ) {
2020-08-03 16:07:06 +00:00
val start = System . currentTimeMillis ( )
val workQueue = LinkedBlockingQueue < Runnable > ( )
val poolSize = Runtime . getRuntime ( ) . availableProcessors ( )
val executor = ThreadPoolExecutor ( poolSize , poolSize , 1 , TimeUnit . SECONDS , workQueue )
val futures = arrayListOf < Future < * > > ( )
2020-09-06 12:59:41 +00:00
val keys = listDiagnosisKeysPendingSearch ( packageName , token , database )
2020-08-03 16:07:06 +00:00
val oldestRpi = oldestRpi
for ( key in keys ) {
2020-09-03 22:13:11 +00:00
if ( oldestRpi == null || ( key . rollingStartIntervalNumber + key . rollingPeriod ) * ROLLING _WINDOW _LENGTH _MS + ALLOWED _KEY _OFFSET _MS < oldestRpi ) {
2020-08-03 16:07:06 +00:00
// Early ignore because key is older than since we started scanning.
2020-09-06 12:59:41 +00:00
applyDiagnosisKeySearchResult ( key , false , database )
2020-08-03 16:07:06 +00:00
} else {
futures . add ( executor . submit {
2020-09-06 12:59:41 +00:00
applyDiagnosisKeySearchResult ( key , findMeasuredExposures ( key ) . isNotEmpty ( ) , database )
2020-08-03 16:07:06 +00:00
} )
}
}
for ( future in futures ) {
future . get ( )
}
val time = ( System . currentTimeMillis ( ) - start ) . toDouble ( ) / 1000.0
executor . shutdown ( )
2020-08-24 08:12:49 +00:00
Log . d ( TAG , " Processed ${keys.size} new keys in ${time} s -> ${(keys.size.toDouble() / time * 1000).roundToInt().toDouble() / 1000.0} keys/s " )
2020-08-03 16:07:06 +00:00
}
2020-09-06 12:59:41 +00:00
fun findAllMeasuredExposures ( packageName : String , token : String , database : SQLiteDatabase = readableDatabase ) : List < MeasuredExposure > {
return listMatchedDiagnosisKeys ( packageName , token , database ) . flatMap { findMeasuredExposures ( it , database ) }
2020-08-03 16:07:06 +00:00
}
2020-09-06 12:59:41 +00:00
private fun findMeasuredExposures ( key : TemporaryExposureKey , database : SQLiteDatabase = readableDatabase ) : List < MeasuredExposure > {
2020-08-03 16:07:06 +00:00
val allRpis = key . generateAllRpiIds ( )
val rpis = ( 0 until key . rollingPeriod ) . map { i ->
val pos = i * 16
allRpis . sliceArray ( pos until ( pos + 16 ) )
}
2020-09-06 12:59:41 +00:00
val measures = findExposures ( rpis , key . rollingStartIntervalNumber . toLong ( ) * ROLLING _WINDOW _LENGTH _MS - ALLOWED _KEY _OFFSET _MS , ( key . rollingStartIntervalNumber . toLong ( ) + key . rollingPeriod ) * ROLLING _WINDOW _LENGTH _MS + ALLOWED _KEY _OFFSET _MS , database )
2020-09-03 22:13:11 +00:00
return measures . filter {
val index = rpis . indexOfFirst { rpi -> rpi . contentEquals ( it . rpi ) }
2020-08-03 16:07:06 +00:00
val targetTimestamp = ( key . rollingStartIntervalNumber + index ) . toLong ( ) * ROLLING _WINDOW _LENGTH _MS
2020-09-03 22:13:11 +00:00
it . timestamp >= targetTimestamp - ALLOWED _KEY _OFFSET _MS && it . timestamp <= targetTimestamp + ROLLING _WINDOW _LENGTH _MS + ALLOWED _KEY _OFFSET _MS
2020-08-03 16:07:06 +00:00
} . mapNotNull {
val decrypted = key . cryptAem ( it . rpi , it . aem )
if ( decrypted [ 0 ] == 0x40 . toByte ( ) || decrypted [ 0 ] == 0x50 . toByte ( ) ) {
val txPower = decrypted [ 1 ]
2020-09-03 22:13:11 +00:00
MeasuredExposure ( it . timestamp , it . duration , it . rssi , txPower . toInt ( ) , key )
2020-08-03 16:07:06 +00:00
} else {
Log . w ( TAG , " Unknown AEM version ${decrypted[0]} , ignoring " )
null
}
}
}
2020-09-06 12:59:41 +00:00
private fun findExposures ( rpis : List < ByteArray > , minTime : Long , maxTime : Long , database : SQLiteDatabase = readableDatabase ) : List < PlainExposure > = database . run {
2020-08-03 16:07:06 +00:00
if ( rpis . isEmpty ( ) ) return emptyList ( )
val qs = rpis . map { " ? " } . joinToString ( " , " )
queryWithFactory ( { _ , cursorDriver , editTable , query ->
query . bindLong ( 1 , minTime )
query . bindLong ( 2 , maxTime )
2020-09-03 22:13:11 +00:00
rpis . forEachIndexed { index , rpi ->
query . bindBlob ( index + 3 , rpi )
2020-08-03 16:07:06 +00:00
}
SQLiteCursor ( cursorDriver , editTable , query )
} , false , TABLE _ADVERTISEMENTS , arrayOf ( " rpi " , " aem " , " timestamp " , " duration " , " rssi " ) , " timestamp > ? AND timestamp < ? AND rpi IN ( $qs ) " , null , null , null , null , null ) . use { cursor ->
2020-09-03 22:13:11 +00:00
val list = arrayListOf < PlainExposure > ( )
2020-08-03 16:07:06 +00:00
while ( cursor . moveToNext ( ) ) {
2020-09-03 22:13:11 +00:00
list . add ( PlainExposure ( cursor . getBlob ( 0 ) , cursor . getBlob ( 1 ) , cursor . getLong ( 2 ) , cursor . getLong ( 3 ) , cursor . getInt ( 4 ) ) )
2020-08-03 16:07:06 +00:00
}
list
}
}
2020-09-03 22:13:11 +00:00
fun findExposure ( rpi : ByteArray , minTime : Long , maxTime : Long ) : PlainExposure ? = readableDatabase . run {
2020-08-03 16:07:06 +00:00
queryWithFactory ( { _ , cursorDriver , editTable , query ->
query . bindBlob ( 1 , rpi )
query . bindLong ( 2 , minTime )
query . bindLong ( 3 , maxTime )
SQLiteCursor ( cursorDriver , editTable , query )
} , false , TABLE _ADVERTISEMENTS , arrayOf ( " aem " , " timestamp " , " duration " , " rssi " ) , " rpi = ? AND timestamp > ? AND timestamp < ? " , null , null , null , null , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
2020-09-03 22:13:11 +00:00
PlainExposure ( rpi , cursor . getBlob ( 0 ) , cursor . getLong ( 1 ) , cursor . getLong ( 2 ) , cursor . getInt ( 3 ) )
2020-08-03 16:07:06 +00:00
} else {
null
}
}
}
2020-09-06 12:59:41 +00:00
private fun findOwnKeyAt ( rollingStartNumber : Int , database : SQLiteDatabase = readableDatabase ) : TemporaryExposureKey ? = database . run {
2020-08-03 16:07:06 +00:00
query ( TABLE _TEK , arrayOf ( " keyData " , " rollingStartNumber " , " rollingPeriod " ) , " rollingStartNumber = ? " , arrayOf ( rollingStartNumber . toString ( ) ) , null , null , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
TemporaryExposureKey . TemporaryExposureKeyBuilder ( )
. setKeyData ( cursor . getBlob ( 0 ) )
. setRollingStartIntervalNumber ( cursor . getLong ( 1 ) . toInt ( ) )
. setRollingPeriod ( cursor . getLong ( 2 ) . toInt ( ) )
. build ( )
} else {
null
}
}
}
fun Parcelable . marshall ( ) : ByteArray {
val parcel = Parcel . obtain ( )
writeToParcel ( parcel , 0 )
val bytes = parcel . marshall ( )
parcel . recycle ( )
return bytes
}
fun < T > Parcelable . Creator < T > . unmarshall ( data : ByteArray ) : T {
val parcel = Parcel . obtain ( )
parcel . unmarshall ( data , 0 , data . size )
parcel . setDataPosition ( 0 )
val res = createFromParcel ( parcel )
parcel . recycle ( )
return res
}
fun storeConfiguration ( packageName : String , token : String , configuration : ExposureConfiguration ) = writableDatabase . run {
val update = update ( TABLE _CONFIGURATIONS , ContentValues ( ) . apply { put ( " configuration " , configuration . marshall ( ) ) } , " package = ? AND token = ? " , arrayOf ( packageName , token ) )
if ( update <= 0 ) {
insert ( TABLE _CONFIGURATIONS , " NULL " , ContentValues ( ) . apply {
put ( " package " , packageName )
put ( " token " , token )
put ( " configuration " , configuration . marshall ( ) )
} )
}
}
fun loadConfiguration ( packageName : String , token : String ) : ExposureConfiguration ? = readableDatabase . run {
query ( TABLE _CONFIGURATIONS , arrayOf ( " configuration " ) , " package = ? AND token = ? " , arrayOf ( packageName , token ) , null , null , null , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
ExposureConfiguration . CREATOR . unmarshall ( cursor . getBlob ( 0 ) )
} else {
null
}
}
}
val allKeys : List < TemporaryExposureKey > = readableDatabase . run {
val startRollingNumber = ( currentRollingStartNumber - 14 * ROLLING _PERIOD )
query ( TABLE _TEK , arrayOf ( " keyData " , " rollingStartNumber " , " rollingPeriod " ) , " rollingStartNumber >= ? AND rollingStartNumber < ? " , arrayOf ( startRollingNumber . toString ( ) , currentIntervalNumber . toString ( ) ) , null , null , null ) . use { cursor ->
val list = arrayListOf < TemporaryExposureKey > ( )
while ( cursor . moveToNext ( ) ) {
list . add ( TemporaryExposureKey . TemporaryExposureKeyBuilder ( )
. setKeyData ( cursor . getBlob ( 0 ) )
. setRollingStartIntervalNumber ( cursor . getLong ( 1 ) . toInt ( ) )
. setRollingPeriod ( cursor . getLong ( 2 ) . toInt ( ) )
. build ( ) )
}
list
}
}
val rpiHistogram : Map < Long , Long >
get ( ) = readableDatabase . run {
rawQuery ( " SELECT round(timestamp/(24*60*60*1000)), COUNT(*) FROM $TABLE _ADVERTISEMENTS WHERE timestamp > ? GROUP BY round(timestamp/(24*60*60*1000)) ORDER BY timestamp ASC; " , arrayOf ( ( Date ( ) . time - ( 14 * 24 * 60 * 60 * 1000 ) ) . toString ( ) ) ) . use { cursor ->
val map = linkedMapOf < Long , Long > ( )
while ( cursor . moveToNext ( ) ) {
map [ cursor . getLong ( 0 ) ] = cursor . getLong ( 1 )
}
map
}
}
val totalRpiCount : Long
get ( ) = readableDatabase . run {
rawQuery ( " SELECT COUNT(*) FROM $TABLE _ADVERTISEMENTS WHERE timestamp > ?; " , arrayOf ( ( Date ( ) . time - ( 14 * 24 * 60 * 60 * 1000 ) ) . toString ( ) ) ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
cursor . getLong ( 0 )
} else {
0L
}
}
}
val hourRpiCount : Long
get ( ) = readableDatabase . run {
rawQuery ( " SELECT COUNT(*) FROM $TABLE _ADVERTISEMENTS WHERE timestamp > ?; " , arrayOf ( ( Date ( ) . time - ( 60 * 60 * 1000 ) ) . toString ( ) ) ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
cursor . getLong ( 0 )
} else {
0L
}
}
}
val oldestRpi : Long ?
get ( ) = readableDatabase . run {
query ( TABLE _ADVERTISEMENTS , arrayOf ( " MIN(timestamp) " ) , null , null , null , null , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
cursor . getLong ( 0 )
} else {
null
}
}
}
val appList : List < String >
get ( ) = readableDatabase . run {
query ( true , TABLE _APP _LOG , arrayOf ( " package " ) , null , null , null , null , " timestamp DESC " , null ) . use { cursor ->
val list = arrayListOf < String > ( )
while ( cursor . moveToNext ( ) ) {
list . add ( cursor . getString ( 0 ) )
}
list
}
}
fun countMethodCalls ( packageName : String , method : String ) : Int = readableDatabase . run {
query ( TABLE _APP _LOG , arrayOf ( " COUNT(*) " ) , " package = ? AND method = ? AND timestamp > ? " , arrayOf ( packageName , method , ( System . currentTimeMillis ( ) - TimeUnit . DAYS . toMillis ( KEEP_DAYS . toLong ( ) ) ) . toString ( ) ) , null , null , null , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
cursor . getInt ( 0 )
} else {
0
}
}
}
fun lastMethodCall ( packageName : String , method : String ) : Long ? = readableDatabase . run {
query ( TABLE _APP _LOG , arrayOf ( " MAX(timestamp) " ) , " package = ? AND method = ? " , arrayOf ( packageName , method ) , null , null , null , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
cursor . getLong ( 0 )
} else {
null
}
}
}
2020-09-04 08:37:02 +00:00
fun lastMethodCallArgs ( packageName : String , method : String ) : String ? = readableDatabase . run {
query ( TABLE _APP _LOG , arrayOf ( " args " ) , " package = ? AND method = ? " , arrayOf ( packageName , method ) , null , null , " timestamp DESC " , " 1 " ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
cursor . getString ( 0 )
} else {
null
}
}
}
2020-08-03 16:07:06 +00:00
private val currentTemporaryExposureKey : TemporaryExposureKey
2020-09-06 12:59:41 +00:00
get ( ) = writableDatabase . let { database ->
database . beginTransaction ( )
try {
var key = findOwnKeyAt ( currentRollingStartNumber . toInt ( ) , database )
if ( key == null ) {
key = generateCurrentTemporaryExposureKey ( )
storeOwnKey ( key , database )
}
database . setTransactionSuccessful ( )
key
} finally {
database . endTransaction ( )
}
}
2020-08-03 16:07:06 +00:00
val currentRpiId : UUID
get ( ) {
val buffer = ByteBuffer . wrap ( currentTemporaryExposureKey . generateRpiId ( currentIntervalNumber . toInt ( ) ) )
return UUID ( buffer . long , buffer . long )
}
fun generateCurrentPayload ( metadata : ByteArray ) = currentTemporaryExposureKey . generatePayload ( currentIntervalNumber . toInt ( ) , metadata )
2020-08-05 12:17:29 +00:00
override fun getWritableDatabase ( ) : SQLiteDatabase {
if ( this != instance ) {
throw IllegalStateException ( " Tried to open writable database from secondary instance " )
}
2020-08-24 08:12:49 +00:00
return super . getWritableDatabase ( )
2020-08-05 12:17:29 +00:00
}
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 " )
}
}
2020-08-03 16:07:06 +00:00
companion object {
private const val DB _NAME = " exposure.db "
2020-09-05 21:51:00 +00:00
private const val DB _VERSION = 3
2020-08-03 16:07:06 +00:00
private const val TABLE _ADVERTISEMENTS = " advertisements "
private const val TABLE _APP _LOG = " app_log "
private const val TABLE _TEK = " tek "
private const val TABLE _TEK _CHECK = " tek_check "
private const val TABLE _DIAGNOSIS = " diagnosis "
private const val TABLE _CONFIGURATIONS = " configurations "
2020-09-05 21:51:00 +00:00
private const val TABLE _APP _PERMS = " app_perms "
2020-08-05 12:17:29 +00:00
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 ( )
}
}
2020-08-03 16:07:06 +00:00
}
}