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
2020-11-30 17:12:21 +00:00
import android.content.Intent
2020-08-03 16:07:06 +00:00
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
2020-11-30 17:12:21 +00:00
import android.net.Uri
2020-08-03 16:07:06 +00:00
import android.os.Parcel
import android.os.Parcelable
import android.util.Log
2020-11-30 17:12:21 +00:00
import androidx.core.content.FileProvider
2022-01-16 22:08:51 +00:00
import com.google.android.gms.nearby.exposurenotification.*
2020-10-12 19:25:34 +00:00
import kotlinx.coroutines.*
2020-09-27 09:33:02 +00:00
import okio.ByteString
2020-12-12 11:40:48 +00:00
import org.microg.gms.common.PackageUtils
2020-09-27 09:33:02 +00:00
import java.io.File
2020-10-12 19:25:34 +00:00
import java.lang.Runnable
2020-08-03 16:07:06 +00:00
import java.nio.ByteBuffer
import java.util.*
2020-09-27 09:33:02 +00:00
import java.util.concurrent.*
2020-11-18 16:06:29 +00:00
import kotlin.experimental.and
2020-08-03 16:07:06 +00:00
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-10-12 19:25:34 +00:00
private val createdAt : Exception = Exception ( " Database ${hashCode()} created " )
private var refCount = 1
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 ) {
2020-09-27 09:33:02 +00:00
Log . d ( TAG , " Upgrading database from $oldVersion to $newVersion " )
2020-08-03 16:07:06 +00:00
if ( oldVersion < 1 ) {
2020-09-27 09:33:02 +00:00
Log . d ( TAG , " Creating tables for version >= 1 " )
2020-08-03 16:07:06 +00:00
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); " )
2020-08-24 08:12:49 +00:00
}
2020-09-05 21:51:00 +00:00
if ( oldVersion < 3 ) {
2020-09-27 09:33:02 +00:00
Log . d ( TAG , " Creating tables for version >= 3 " )
2020-09-05 21:51:00 +00:00
db . execSQL ( " CREATE TABLE $TABLE _APP_PERMS(package TEXT NOT NULL, sig TEXT NOT NULL, perm TEXT NOT NULL, timestamp INTEGER NOT NULL); " )
}
2020-09-27 09:33:02 +00:00
if ( oldVersion < 5 ) {
2020-12-12 11:40:48 +00:00
Log . d ( TAG , " Creating tables for version >= 5 " )
2020-11-18 16:06:29 +00:00
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _TOKENS(tid INTEGER PRIMARY KEY, package TEXT NOT NULL, token TEXT NOT NULL, timestamp INTEGER NOT NULL, configuration BLOB, diagnosisKeysDataMap BLOB); " )
2020-09-27 09:33:02 +00:00
db . execSQL ( " CREATE UNIQUE INDEX IF NOT EXISTS index_ ${TABLE_TOKENS} _package_token ON $TABLE _TOKENS(package, token); " )
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _TEK_CHECK_SINGLE(tcsid INTEGER PRIMARY KEY, keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, matched INTEGER); " )
db . execSQL ( " CREATE UNIQUE INDEX IF NOT EXISTS index_ ${TABLE_TEK_CHECK_SINGLE} _key ON $TABLE _TEK_CHECK_SINGLE(keyData, rollingStartNumber, rollingPeriod); " )
2022-01-10 14:55:10 +00:00
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _TEK_CHECK_SINGLE_TOKEN(tcsid INTEGER REFERENCES $TABLE _TEK_CHECK_SINGLE(tcsid) ON DELETE CASCADE, tid INTEGER REFERENCES $TABLE _TOKENS(tid) ON DELETE CASCADE, transmissionRiskLevel INTEGER NOT NULL, reportType INTEGER NOT NULL, daysSinceOnsetOfSymptoms INTEGER NOT NULL, UNIQUE(tcsid, tid)); " )
2020-09-27 09:33:02 +00:00
db . execSQL ( " CREATE INDEX IF NOT EXISTS index_ ${TABLE_TEK_CHECK_SINGLE_TOKEN} _tid ON $TABLE _TEK_CHECK_SINGLE_TOKEN(tid); " )
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _TEK_CHECK_FILE(tcfid INTEGER PRIMARY KEY, hash TEXT NOT NULL, endTimestamp INTEGER NOT NULL, keys INTEGER NOT NULL); " )
db . execSQL ( " CREATE UNIQUE INDEX IF NOT EXISTS index_ ${TABLE_TEK_CHECK_FILE} _hash ON $TABLE _TEK_CHECK_FILE(hash); " )
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _TEK_CHECK_FILE_TOKEN(tcfid INTEGER REFERENCES $TABLE _TEK_CHECK_FILE(tcfid) ON DELETE CASCADE, tid INTEGER REFERENCES $TABLE _TOKENS(tid) ON DELETE CASCADE, UNIQUE(tcfid, tid)); " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS index_ ${TABLE_TEK_CHECK_FILE_TOKEN} _tid ON $TABLE _TEK_CHECK_FILE_TOKEN(tid); " )
2022-01-10 14:55:10 +00:00
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _TEK_CHECK_FILE_MATCH(tcfid INTEGER REFERENCES $TABLE _TEK_CHECK_FILE(tcfid) ON DELETE CASCADE, keyData BLOB NOT NULL, rollingStartNumber INTEGER NOT NULL, rollingPeriod INTEGER NOT NULL, transmissionRiskLevel INTEGER NOT NULL, reportType INTEGER NOT NULL, daysSinceOnsetOfSymptoms INTEGER NOT NULL, UNIQUE(tcfid, keyData, rollingStartNumber, rollingPeriod)); " )
2020-09-27 09:33:02 +00:00
db . execSQL ( " CREATE INDEX IF NOT EXISTS index_ ${TABLE_TEK_CHECK_FILE_MATCH} _tcfid ON $TABLE _TEK_CHECK_FILE_MATCH(tcfid); " )
db . execSQL ( " CREATE INDEX IF NOT EXISTS index_ ${TABLE_TEK_CHECK_FILE_MATCH} _key ON $TABLE _TEK_CHECK_FILE_MATCH(keyData, rollingStartNumber, rollingPeriod); " )
}
2020-12-12 11:40:48 +00:00
if ( oldVersion < 9 ) {
Log . d ( TAG , " Creating tables for version >= 9 " )
db . execSQL ( " CREATE TABLE IF NOT EXISTS $TABLE _APP(package TEXT NOT NULL, sig TEXT NOT NULL, PRIMARY KEY (package, sig)); " )
}
2020-11-24 15:36:08 +00:00
if ( oldVersion in 5 until 7 ) {
Log . d ( TAG , " Altering tables for version >= 7 " )
db . execSQL ( " ALTER TABLE $TABLE _TOKENS ADD COLUMN diagnosisKeysDataMap BLOB; " )
}
2022-01-10 15:05:01 +00:00
if ( oldVersion in 5 until 11 ) {
Log . d ( TAG , " Altering tables for version >= 11 " )
2022-01-16 22:08:51 +00:00
db . execSQL ( " ALTER TABLE $TABLE _TEK_CHECK_SINGLE_TOKEN ADD COLUMN reportType INTEGER NOT NULL DEFAULT ${ReportType.UNKNOWN} ; " )
db . execSQL ( " ALTER TABLE $TABLE _TEK_CHECK_SINGLE_TOKEN ADD COLUMN daysSinceOnsetOfSymptoms INTEGER NOT NULL DEFAULT ${TemporaryExposureKey.DAYS_SINCE_ONSET_OF_SYMPTOMS_UNKNOWN} ; " )
db . execSQL ( " ALTER TABLE $TABLE _TEK_CHECK_FILE_MATCH ADD COLUMN reportType INTEGER NOT NULL DEFAULT ${ReportType.UNKNOWN} ; " )
db . execSQL ( " ALTER TABLE $TABLE _TEK_CHECK_FILE_MATCH ADD COLUMN daysSinceOnsetOfSymptoms INTEGER NOT NULL DEFAULT ${TemporaryExposureKey.DAYS_SINCE_ONSET_OF_SYMPTOMS_UNKNOWN} ; " )
2022-01-10 14:55:10 +00:00
}
2020-11-24 15:36:08 +00:00
if ( oldVersion in 1 until 5 ) {
Log . d ( TAG , " Dropping legacy tables from version < 5 " )
db . execSQL ( " DROP TABLE IF EXISTS $TABLE _CONFIGURATIONS; " )
db . execSQL ( " DROP TABLE IF EXISTS $TABLE _DIAGNOSIS; " )
db . execSQL ( " DROP TABLE IF EXISTS $TABLE _TEK_CHECK; " )
}
if ( oldVersion in 1 until 6 ) {
Log . d ( TAG , " Fixing invalid rssi values from version < 6 " )
2020-10-17 13:30:03 +00:00
// There's no bluetooth chip with a sensitivity that would result in rssi -200, so this would be invalid.
// RSSI of -100 is already extremely low and thus is a good "default" value
db . execSQL ( " UPDATE $TABLE _ADVERTISEMENTS SET rssi = -100 WHERE rssi < -200; " )
}
2020-11-18 16:06:29 +00:00
if ( oldVersion in 5 until 7 ) {
2020-11-24 15:36:08 +00:00
Log . d ( TAG , " Clearing non-matching tek cache from version < 7 " )
// Entries might be invalid due to previously missing support for new bluetooth AEM format
db . execSQL ( " DELETE FROM $TABLE _TEK_CHECK_FILE WHERE tcfid NOT IN (SELECT tcfid FROM $TABLE _TEK_CHECK_FILE_MATCH); " )
2020-11-18 16:06:29 +00:00
}
2020-12-12 11:40:48 +00:00
if ( oldVersion in 1 until 9 ) {
Log . d ( TAG , " Migrating authorized apps from version < 9 " )
val pm = context . packageManager
db . query ( true , TABLE _APP _LOG , arrayOf ( " package " ) , null , null , null , null , null , null ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
val packageName = cursor . getString ( 0 )
val signatureDigest = PackageUtils . firstSignatureDigest ( pm , packageName )
if ( signatureDigest != null ) {
db . insertWithOnConflict ( TABLE _APP , " NULL " , ContentValues ( ) . apply {
put ( " package " , packageName )
put ( " sig " , signatureDigest )
} , CONFLICT _IGNORE )
}
}
}
}
2020-12-20 11:43:38 +00:00
if ( oldVersion == 9 ) {
Log . d ( TAG , " Get rid of isEnabled log entries " )
db . delete ( TABLE _APP _LOG , " method = ? " , arrayOf ( " isEnabled " ) ) ;
}
2020-09-27 09:33:02 +00:00
Log . d ( TAG , " Finished database upgrade " )
2020-08-03 16:07:06 +00:00
}
2020-10-09 14:24:43 +00:00
fun SQLiteDatabase . delete ( table : String , whereClause : String , args : LongArray ) : Int =
compileStatement ( " DELETE FROM $table WHERE $whereClause " ) . use {
2020-08-24 08:12:49 +00:00
args . forEachIndexed { idx , l -> it . bindLong ( idx + 1 , l ) }
it . executeUpdateDelete ( )
}
2020-10-07 21:16:53 +00:00
fun dailyCleanup ( ) : Boolean = writableDatabase . run {
2020-10-09 14:24:43 +00:00
val start = System . currentTimeMillis ( )
2020-12-13 14:50:54 +00:00
val rollingStartTime = currentDayRollingStartNumber . toLong ( ) * ROLLING _WINDOW _LENGTH _MS - TimeUnit . DAYS . toMillis ( KEEP_DAYS . toLong ( ) )
2020-10-09 14:24:43 +00:00
val advertisements = delete ( TABLE _ADVERTISEMENTS , " timestamp < ? " , longArrayOf ( rollingStartTime ) )
2020-09-27 09:33:02 +00:00
Log . d ( TAG , " Deleted on daily cleanup: $advertisements adv " )
2020-10-09 14:24:43 +00:00
if ( start + MAX _DELETE _TIME < System . currentTimeMillis ( ) ) return @run false
val appLogEntries = delete ( TABLE _APP _LOG , " timestamp < ? " , longArrayOf ( rollingStartTime ) )
2020-09-27 09:33:02 +00:00
Log . d ( TAG , " Deleted on daily cleanup: $appLogEntries applogs " )
2020-10-09 14:24:43 +00:00
if ( start + MAX _DELETE _TIME < System . currentTimeMillis ( ) ) return @run false
val temporaryExposureKeys = delete ( TABLE _TEK , " (rollingStartNumber + rollingPeriod) < ? " , longArrayOf ( rollingStartTime / ROLLING _WINDOW _LENGTH _MS ) )
2020-09-27 09:33:02 +00:00
Log . d ( TAG , " Deleted on daily cleanup: $temporaryExposureKeys teks " )
2020-10-09 14:24:43 +00:00
if ( start + MAX _DELETE _TIME < System . currentTimeMillis ( ) ) return @run false
val singleCheckedTemporaryExposureKeys = delete ( TABLE _TEK _CHECK _SINGLE , " rollingStartNumber < ? " , longArrayOf ( rollingStartTime / ROLLING _WINDOW _LENGTH _MS - ROLLING _PERIOD ) )
2020-09-27 09:33:02 +00:00
Log . d ( TAG , " Deleted on daily cleanup: $singleCheckedTemporaryExposureKeys tcss " )
2020-10-09 14:24:43 +00:00
if ( start + MAX _DELETE _TIME < System . currentTimeMillis ( ) ) return @run false
val fileCheckedTemporaryExposureKeys = delete ( TABLE _TEK _CHECK _FILE , " endTimestamp < ? " , longArrayOf ( rollingStartTime ) )
2020-09-27 09:33:02 +00:00
Log . d ( TAG , " Deleted on daily cleanup: $fileCheckedTemporaryExposureKeys tcfs " )
2020-10-09 14:24:43 +00:00
if ( start + MAX _DELETE _TIME < System . currentTimeMillis ( ) ) return @run false
val appPerms = delete ( TABLE _APP _PERMS , " timestamp < ? " , longArrayOf ( System . currentTimeMillis ( ) - CONFIRM _PERMISSION _VALIDITY ) )
2020-09-27 09:33:02 +00:00
Log . d ( TAG , " Deleted on daily cleanup: $appPerms perms " )
2020-10-09 14:24:43 +00:00
if ( start + MAX _DELETE _TIME < System . currentTimeMillis ( ) ) return @run false
2020-09-27 09:33:02 +00:00
execSQL ( " VACUUM; " )
Log . d ( TAG , " Done vacuuming " )
2020-10-07 21:16:53 +00:00
return @run true
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 {
2022-01-31 23:06:04 +00:00
val update = compileStatement ( " UPDATE $TABLE _ADVERTISEMENTS SET rssi = IFNULL(((rssi * duration) + (? * MAX(0, ? - timestamp - duration))) / MAX(duration, ? - timestamp), -100), duration = MAX(duration, ? - timestamp) WHERE rpi = ? AND timestamp > ? AND timestamp < ? " ) . run {
2020-08-03 16:07:06 +00:00
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-09-27 09:33:02 +00:00
delete ( TABLE _TEK _CHECK _FILE _MATCH , null , null )
update ( TABLE _TEK _CHECK _SINGLE , ContentValues ( ) . apply {
put ( " matched " , 0 )
} , null , null )
2020-08-03 16:07:06 +00:00
}
2020-12-12 11:40:48 +00:00
fun authorizeApp ( packageName : String ? , signatureDigest : String ? = PackageUtils . firstSignatureDigest ( context , packageName ) ) = writableDatabase . run {
if ( packageName == null || signatureDigest == null ) return @run
insertWithOnConflict ( TABLE _APP , " NULL " , ContentValues ( ) . apply {
put ( " package " , packageName )
put ( " sig " , signatureDigest )
} , CONFLICT _IGNORE )
}
fun isAppAuthorized ( packageName : String ? , signatureDigest : String ? = PackageUtils . firstSignatureDigest ( context , packageName ) ) : Boolean = readableDatabase . run {
if ( packageName == null || signatureDigest == null ) return @run false
query ( TABLE _APP , arrayOf ( " package " ) , " package = ? AND sig = ? " , arrayOf ( packageName , signatureDigest ) , null , null , null ) . use { cursor ->
return @use cursor . moveToNext ( )
}
}
2020-08-03 16:07:06 +00:00
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-27 09:33:02 +00:00
private fun getTekCheckSingleId ( 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 ) {
2020-09-27 09:33:02 +00:00
insertWithOnConflict ( TABLE _TEK _CHECK _SINGLE , " NULL " , ContentValues ( ) . apply {
2020-08-24 08:12:49 +00:00
put ( " keyData " , key . keyData )
put ( " rollingStartNumber " , key . rollingStartIntervalNumber )
put ( " rollingPeriod " , key . rollingPeriod )
} , CONFLICT _IGNORE )
}
2020-09-27 09:33:02 +00:00
compileStatement ( " SELECT tcsid FROM $TABLE _TEK_CHECK_SINGLE WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ? " ) . use {
2020-08-24 08:12:49 +00:00
it . bindBlob ( 1 , key . keyData )
it . bindLong ( 2 , key . rollingStartIntervalNumber . toLong ( ) )
it . bindLong ( 3 , key . rollingPeriod . toLong ( ) )
it . simpleQueryForLong ( )
}
}
2020-09-27 09:33:02 +00:00
fun getTokenId ( packageName : String , token : String , database : SQLiteDatabase = readableDatabase ) = database . run {
query ( TABLE _TOKENS , arrayOf ( " tid " ) , " package = ? AND token = ? " , arrayOf ( packageName , token ) , null , null , null , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
cursor . getLong ( 0 )
} else {
null
}
}
}
2020-12-03 09:15:18 +00:00
fun getOrCreateTokenId ( packageName : String , token : String , database : SQLiteDatabase = writableDatabase ) = database . run {
val tid = getTokenId ( packageName , token , this )
if ( tid != null ) {
tid
} else {
insert ( TABLE _TOKENS , " NULL " , ContentValues ( ) . apply {
put ( " package " , packageName )
put ( " token " , token )
put ( " timestamp " , System . currentTimeMillis ( ) )
} )
getTokenId ( packageName , token , this )
}
}
2020-09-27 09:33:02 +00:00
private fun storeSingleDiagnosisKey ( tid : Long , key : TemporaryExposureKey , database : SQLiteDatabase = writableDatabase ) = database . run {
val tcsid = getTekCheckSingleId ( key , true , database )
insert ( TABLE _TEK _CHECK _SINGLE _TOKEN , " NULL " , ContentValues ( ) . apply {
put ( " tid " , tid )
put ( " tcsid " , tcsid )
2020-08-03 16:07:06 +00:00
put ( " transmissionRiskLevel " , key . transmissionRiskLevel )
2022-01-10 14:55:10 +00:00
put ( " reportType " , key . reportType )
put ( " daysSinceOnsetOfSymptoms " , key . daysSinceOnsetOfSymptoms )
2020-08-03 16:07:06 +00:00
} )
}
2020-09-27 09:33:02 +00:00
fun batchStoreSingleDiagnosisKey ( tid : Long , keys : List < TemporaryExposureKey > , database : SQLiteDatabase = writableDatabase ) = database . run {
2020-10-07 21:16:53 +00:00
beginTransactionNonExclusive ( )
2020-09-06 12:59:41 +00:00
try {
2020-09-27 09:33:02 +00:00
keys . forEach { storeSingleDiagnosisKey ( tid , it , database ) }
2020-09-06 12:59:41 +00:00
setTransactionSuccessful ( )
} finally {
endTransaction ( )
}
}
2020-09-27 09:33:02 +00:00
fun getDiagnosisFileId ( hash : ByteArray , database : SQLiteDatabase = readableDatabase ) = database . run {
val hexHash = ByteString . of ( * hash ) . hex ( )
query ( TABLE _TEK _CHECK _FILE , arrayOf ( " tcfid " ) , " hash = ? " , arrayOf ( hexHash ) , null , null , null , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
cursor . getLong ( 0 )
} else {
null
}
2020-08-03 16:07:06 +00:00
}
}
2020-09-27 09:33:02 +00:00
fun storeDiagnosisFileUsed ( tid : Long , tcfid : Long , database : SQLiteDatabase = writableDatabase ) = database . run {
insert ( TABLE _TEK _CHECK _FILE _TOKEN , " NULL " , ContentValues ( ) . apply {
put ( " tid " , tid )
put ( " tcfid " , tcfid )
} )
}
fun storeDiagnosisFileUsed ( tid : Long , hash : ByteArray , database : SQLiteDatabase = writableDatabase ) = database . run {
val hexHash = ByteString . of ( * hash ) . hex ( )
query ( TABLE _TEK _CHECK _FILE , arrayOf ( " tcfid " , " keys " ) , " hash = ? " , arrayOf ( hexHash ) , null , null , null , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
2020-11-18 16:06:29 +00:00
insertWithOnConflict ( TABLE _TEK _CHECK _FILE _TOKEN , " NULL " , ContentValues ( ) . apply {
2020-09-27 09:33:02 +00:00
put ( " tid " , tid )
put ( " tcfid " , cursor . getLong ( 0 ) )
2020-11-18 16:06:29 +00:00
} , CONFLICT _IGNORE )
2020-09-27 09:33:02 +00:00
cursor . getLong ( 1 )
} else {
null
}
2020-09-06 12:59:41 +00:00
}
}
2020-09-27 09:33:02 +00:00
private fun listSingleDiagnosisKeysPendingSearch ( tid : Long , database : SQLiteDatabase = readableDatabase ) = database . run {
2020-08-03 16:07:06 +00:00
rawQuery ( """
2020-09-27 09:33:02 +00:00
SELECT $ TABLE_TEK_CHECK_SINGLE . keyData , $ TABLE_TEK_CHECK_SINGLE . rollingStartNumber , $ TABLE_TEK_CHECK_SINGLE . rollingPeriod
FROM $ TABLE _TEK _CHECK _SINGLE _TOKEN
LEFT JOIN $ TABLE _TEK _CHECK _SINGLE ON $ TABLE_TEK_CHECK_SINGLE . tcsid = $ TABLE_TEK_CHECK_SINGLE_TOKEN . tcsid
2020-08-03 16:07:06 +00:00
WHERE
2020-09-27 09:33:02 +00:00
$ TABLE_TEK_CHECK_SINGLE_TOKEN . tid = ? AND
$ TABLE_TEK_CHECK_SINGLE . matched IS NULL
""" , arrayOf(tid.toString())).use { cursor ->
2020-08-03 16:07:06 +00:00
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-27 09:33:02 +00:00
private fun applySingleDiagnosisKeySearchResult ( key : TemporaryExposureKey , matched : Boolean , database : SQLiteDatabase = writableDatabase ) = database . run {
compileStatement ( " UPDATE $TABLE _TEK_CHECK_SINGLE SET matched = ? WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ?; " ) . use {
2020-08-24 08:12:49 +00:00
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-27 09:33:02 +00:00
private fun applyDiagnosisFileKeySearchResult ( tcfid : Long , key : TemporaryExposureKey , database : SQLiteDatabase = writableDatabase ) = database . run {
insert ( TABLE _TEK _CHECK _FILE _MATCH , " NULL " , ContentValues ( ) . apply {
put ( " tcfid " , tcfid )
put ( " keyData " , key . keyData )
put ( " rollingStartNumber " , key . rollingStartIntervalNumber )
put ( " rollingPeriod " , key . rollingPeriod )
put ( " transmissionRiskLevel " , key . transmissionRiskLevel )
2022-01-10 14:55:10 +00:00
put ( " reportType " , key . reportType )
put ( " daysSinceOnsetOfSymptoms " , key . daysSinceOnsetOfSymptoms )
2020-09-27 09:33:02 +00:00
} )
}
private fun listMatchedSingleDiagnosisKeys ( tid : Long , database : SQLiteDatabase = readableDatabase ) = database . run {
rawQuery ( """
2022-01-10 14:55:10 +00:00
SELECT $ TABLE_TEK_CHECK_SINGLE . keyData , $ TABLE_TEK_CHECK_SINGLE . rollingStartNumber , $ TABLE_TEK_CHECK_SINGLE . rollingPeriod , $ TABLE_TEK_CHECK_SINGLE_TOKEN . transmissionRiskLevel , $ TABLE_TEK_CHECK_SINGLE_TOKEN . reportType , $ TABLE_TEK_CHECK_SINGLE_TOKEN . daysSinceOnsetOfSymptoms
2020-09-27 09:33:02 +00:00
FROM $ TABLE _TEK _CHECK _SINGLE _TOKEN
JOIN $ TABLE _TEK _CHECK _SINGLE ON $ TABLE_TEK_CHECK_SINGLE . tcsid = $ TABLE_TEK_CHECK_SINGLE_TOKEN . tcsid
WHERE
$ TABLE_TEK_CHECK_SINGLE_TOKEN . tid = ? AND
$ TABLE_TEK_CHECK_SINGLE . matched = 1
""" , arrayOf(tid.toString())).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 ( ) )
2022-01-10 14:55:10 +00:00
. setReportType ( cursor . getLong ( 4 ) . toInt ( ) )
. setDaysSinceOnsetOfSymptoms ( cursor . getLong ( 5 ) . toInt ( ) )
2020-09-27 09:33:02 +00:00
. build ( ) )
}
list
}
}
private fun listMatchedFileDiagnosisKeys ( tid : Long , database : SQLiteDatabase = readableDatabase ) = database . run {
2020-08-03 16:07:06 +00:00
rawQuery ( """
2022-01-10 14:55:10 +00:00
SELECT $ TABLE_TEK_CHECK_FILE_MATCH . keyData , $ TABLE_TEK_CHECK_FILE_MATCH . rollingStartNumber , $ TABLE_TEK_CHECK_FILE_MATCH . rollingPeriod , $ TABLE_TEK_CHECK_FILE_MATCH . transmissionRiskLevel , $ TABLE_TEK_CHECK_FILE_MATCH . reportType , $ TABLE_TEK_CHECK_FILE_MATCH . daysSinceOnsetOfSymptoms
2020-09-27 09:33:02 +00:00
FROM $ TABLE _TEK _CHECK _FILE _TOKEN
JOIN $ TABLE _TEK _CHECK _FILE _MATCH ON $ TABLE_TEK_CHECK_FILE_MATCH . tcfid = $ TABLE_TEK_CHECK_FILE_TOKEN . tcfid
2020-08-03 16:07:06 +00:00
WHERE
2020-09-27 09:33:02 +00:00
$ TABLE_TEK_CHECK_FILE_TOKEN . tid = ?
""" , arrayOf(tid.toString())).use { cursor ->
2020-08-03 16:07:06 +00:00
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 ( ) )
2022-01-10 14:55:10 +00:00
. setReportType ( cursor . getLong ( 4 ) . toInt ( ) )
. setDaysSinceOnsetOfSymptoms ( cursor . getLong ( 5 ) . toInt ( ) )
2020-08-03 16:07:06 +00:00
. build ( ) )
}
list
}
}
2020-09-27 09:33:02 +00:00
fun finishSingleMatching ( tid : Long , database : SQLiteDatabase = writableDatabase ) : Int {
2020-08-03 16:07:06 +00:00
val workQueue = LinkedBlockingQueue < Runnable > ( )
val poolSize = Runtime . getRuntime ( ) . availableProcessors ( )
val executor = ThreadPoolExecutor ( poolSize , poolSize , 1 , TimeUnit . SECONDS , workQueue )
val futures = arrayListOf < Future < * > > ( )
2020-09-27 09:33:02 +00:00
val keys = listSingleDiagnosisKeysPendingSearch ( tid , database )
2020-08-03 16:07:06 +00:00
val oldestRpi = oldestRpi
for ( key in keys ) {
2020-10-08 09:38:57 +00:00
if ( ( key . rollingStartIntervalNumber + key . rollingPeriod ) . toLong ( ) * 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-27 09:33:02 +00:00
applySingleDiagnosisKeySearchResult ( key , false , database )
2020-08-03 16:07:06 +00:00
} else {
futures . add ( executor . submit {
2020-09-27 09:33:02 +00:00
applySingleDiagnosisKeySearchResult ( key , findMeasuredExposures ( key ) . isNotEmpty ( ) , database )
2020-08-03 16:07:06 +00:00
} )
}
}
for ( future in futures ) {
future . get ( )
}
executor . shutdown ( )
2020-09-27 09:33:02 +00:00
return keys . size
}
fun finishFileMatching ( tid : Long , hash : ByteArray , endTimestamp : Long , keys : List < TemporaryExposureKey > , updates : List < TemporaryExposureKey > , database : SQLiteDatabase = writableDatabase ) = database . run {
2020-10-07 21:16:53 +00:00
beginTransactionNonExclusive ( )
2020-09-27 09:33:02 +00:00
try {
insert ( TABLE _TEK _CHECK _FILE , " NULL " , ContentValues ( ) . apply {
put ( " hash " , ByteString . of ( * hash ) . hex ( ) )
put ( " endTimestamp " , endTimestamp )
put ( " keys " , keys . size + updates . size )
} )
val tcfid = getDiagnosisFileId ( hash , this ) ?: return
val workQueue = LinkedBlockingQueue < Runnable > ( )
val poolSize = Runtime . getRuntime ( ) . availableProcessors ( )
val executor = ThreadPoolExecutor ( poolSize , poolSize , 1 , TimeUnit . SECONDS , workQueue )
2020-10-07 21:16:53 +00:00
val futures = arrayListOf < Future < TemporaryExposureKey ? > > ( )
2020-09-27 09:33:02 +00:00
val oldestRpi = oldestRpi
var ignored = 0
var processed = 0
var found = 0
2020-11-18 16:06:29 +00:00
var riskLogged = - 1
2020-11-19 17:40:11 +00:00
var startLogged = - 1
2020-09-27 09:33:02 +00:00
for ( key in keys ) {
2020-11-19 17:40:11 +00:00
if ( key . transmissionRiskLevel > riskLogged || key . rollingStartIntervalNumber > startLogged ) {
2020-10-07 21:16:53 +00:00
riskLogged = key . transmissionRiskLevel
2020-11-19 17:40:11 +00:00
startLogged = key . rollingStartIntervalNumber
2020-10-07 21:16:53 +00:00
Log . d ( TAG , " First key with risk ${key.transmissionRiskLevel} : ${ByteString.of(*key.keyData).hex()} starts ${key.rollingStartIntervalNumber} " )
}
2020-10-08 09:38:57 +00:00
if ( ( key . rollingStartIntervalNumber + key . rollingPeriod ) . toLong ( ) * ROLLING _WINDOW _LENGTH _MS + ALLOWED _KEY _OFFSET _MS < oldestRpi ) {
2020-09-27 09:33:02 +00:00
// Early ignore because key is older than since we started scanning.
ignored ++ ;
} else {
2020-10-07 21:16:53 +00:00
futures . add ( executor . submit ( Callable {
processed ++
2020-09-27 09:33:02 +00:00
if ( findMeasuredExposures ( key ) . isNotEmpty ( ) ) {
2020-10-07 21:16:53 +00:00
key
} else {
null
2020-09-27 09:33:02 +00:00
}
2020-10-07 21:16:53 +00:00
} ) )
2020-09-27 09:33:02 +00:00
}
}
for ( future in futures ) {
2020-10-07 21:16:53 +00:00
future . get ( ) ?. let {
applyDiagnosisFileKeySearchResult ( tcfid , it , this )
found ++
}
2020-09-27 09:33:02 +00:00
}
2020-10-09 14:24:43 +00:00
Log . d ( TAG , " Processed $processed keys, found $found matches, ignored $ignored keys that are older than our scanning efforts ( $oldestRpi ) " )
2020-09-27 09:33:02 +00:00
executor . shutdown ( )
for ( update in updates ) {
val matched = compileStatement ( " SELECT COUNT(tcsid) FROM $TABLE _TEK_CHECK_FILE_MATCH WHERE keyData = ? AND rollingStartNumber = ? AND rollingPeriod = ? " ) . use {
it . bindBlob ( 1 , update . keyData )
it . bindLong ( 2 , update . rollingStartIntervalNumber . toLong ( ) )
it . bindLong ( 3 , update . rollingPeriod . toLong ( ) )
it . simpleQueryForLong ( )
}
if ( matched > 0 ) {
applyDiagnosisFileKeySearchResult ( tcfid , update , this )
}
}
insert ( TABLE _TEK _CHECK _FILE _TOKEN , " NULL " , ContentValues ( ) . apply {
put ( " tid " , tid )
put ( " tcfid " , tcfid )
} )
setTransactionSuccessful ( )
} finally {
endTransaction ( )
}
}
2020-11-18 16:06:29 +00:00
private fun findAllSingleMeasuredExposures ( tid : Long , database : SQLiteDatabase = readableDatabase ) : List < MeasuredExposure > {
2020-09-27 09:33:02 +00:00
return listMatchedSingleDiagnosisKeys ( tid , database ) . flatMap { findMeasuredExposures ( it , database ) }
2020-08-03 16:07:06 +00:00
}
2020-11-18 16:06:29 +00:00
private fun findAllFileMeasuredExposures ( tid : Long , database : SQLiteDatabase = readableDatabase ) : List < MeasuredExposure > {
2020-09-27 09:33:02 +00:00
return listMatchedFileDiagnosisKeys ( tid , database ) . flatMap { findMeasuredExposures ( it , database ) }
2020-08-03 16:07:06 +00:00
}
2020-09-27 09:33:02 +00:00
fun findAllMeasuredExposures ( tid : Long , database : SQLiteDatabase = readableDatabase ) = findAllSingleMeasuredExposures ( tid , database ) + findAllFileMeasuredExposures ( tid , database )
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 )
2020-11-18 16:06:29 +00:00
val version = ( decrypted [ 0 ] and 0xf0 . toByte ( ) )
2020-12-10 15:55:19 +00:00
val txPower = if ( decrypted . size >= 4 && version >= VERSION _1 _0 ) decrypted [ 1 ] . toInt ( ) else averageDeviceInfo . txPowerCorrection . toInt ( )
2020-11-18 16:06:29 +00:00
val confidence = if ( decrypted . size >= 4 && version >= VERSION _1 _1 ) ( ( decrypted [ 0 ] and 0xc ) / 4 ) else ( averageDeviceInfo . confidence )
if ( version > VERSION _1 _1 ) {
Log . w ( TAG , " Unknown AEM version: 0x ${version.toString(16)} " )
2020-08-03 16:07:06 +00:00
}
2020-11-18 16:06:29 +00:00
MeasuredExposure ( it . timestamp , it . duration , it . rssi , txPower , confidence , key )
2020-08-03 16:07:06 +00:00
}
}
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-12-11 10:51:53 +00:00
private fun findOwnKeyAt ( intervalNumber : Int , database : SQLiteDatabase = readableDatabase ) : TemporaryExposureKey ? = database . run {
val dayRollingStartNumber = getDayRollingStartNumber ( intervalNumber )
query ( TABLE _TEK , arrayOf ( " keyData " , " rollingStartNumber " , " rollingPeriod " ) , " rollingStartNumber >= ? AND (rollingStartNumber + rollingPeriod) < ? " , arrayOf ( dayRollingStartNumber . toString ( ) , intervalNumber . toString ( ) ) , null , null , " rollingStartNumber DESC " ) . use { cursor ->
2020-08-03 16:07:06 +00:00
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
}
2020-09-27 09:33:02 +00:00
fun storeConfiguration ( packageName : String , token : String , configuration : ExposureConfiguration , database : SQLiteDatabase = writableDatabase ) = database . run {
val update = update ( TABLE _TOKENS , ContentValues ( ) . apply { put ( " configuration " , configuration . marshall ( ) ) } , " package = ? AND token = ? " , arrayOf ( packageName , token ) )
2020-08-03 16:07:06 +00:00
if ( update <= 0 ) {
2020-09-27 09:33:02 +00:00
insert ( TABLE _TOKENS , " NULL " , ContentValues ( ) . apply {
2020-08-03 16:07:06 +00:00
put ( " package " , packageName )
put ( " token " , token )
2020-09-27 09:33:02 +00:00
put ( " timestamp " , System . currentTimeMillis ( ) )
2020-08-03 16:07:06 +00:00
put ( " configuration " , configuration . marshall ( ) )
} )
}
2020-09-27 09:33:02 +00:00
getTokenId ( packageName , token , database )
2020-08-03 16:07:06 +00:00
}
2020-11-18 16:06:29 +00:00
fun storeConfiguration ( packageName : String , token : String , mapping : DiagnosisKeysDataMapping , database : SQLiteDatabase = writableDatabase ) = database . run {
val update = update ( TABLE _TOKENS , ContentValues ( ) . apply { put ( " diagnosisKeysDataMap " , mapping . marshall ( ) ) } , " package = ? AND token = ? " , arrayOf ( packageName , token ) )
if ( update <= 0 ) {
insert ( TABLE _TOKENS , " NULL " , ContentValues ( ) . apply {
put ( " package " , packageName )
put ( " token " , token )
put ( " timestamp " , System . currentTimeMillis ( ) )
put ( " diagnosisKeysDataMap " , mapping . marshall ( ) )
} )
}
getTokenId ( packageName , token , database )
}
fun loadConfiguration ( packageName : String , token : String , database : SQLiteDatabase = readableDatabase ) : Triple < Long , ExposureConfiguration ? , DiagnosisKeysDataMapping ? > ? = database . run {
query ( TABLE _TOKENS , arrayOf ( " tid " , " configuration " , " diagnosisKeysDataMap " ) , " package = ? AND token = ? " , arrayOf ( packageName , token ) , null , null , null , null ) . use { cursor ->
2020-08-03 16:07:06 +00:00
if ( cursor . moveToNext ( ) ) {
2020-11-18 16:06:29 +00:00
val configuration = try { ExposureConfiguration . CREATOR . unmarshall ( cursor . getBlob ( 1 ) ) } catch ( e : Exception ) { null }
val diagnosisKeysDataMapping = try { DiagnosisKeysDataMapping . CREATOR . unmarshall ( cursor . getBlob ( 2 ) ) } catch ( e : Exception ) { null }
Triple ( cursor . getLong ( 0 ) , configuration , diagnosisKeysDataMapping )
2020-08-03 16:07:06 +00:00
} else {
null
}
}
}
2020-12-11 10:51:53 +00:00
fun exportKeys ( database : SQLiteDatabase = writableDatabase ) : List < TemporaryExposureKey > = database . run {
database . beginTransactionNonExclusive ( )
try {
val intervalNumber = currentIntervalNumber
val key = findOwnKeyAt ( intervalNumber , database )
if ( key != null && intervalNumber != key . rollingStartIntervalNumber ) {
// Rotate key
update ( TABLE _TEK , ContentValues ( ) . apply {
put ( " rollingPeriod " , intervalNumber - key . rollingStartIntervalNumber )
} , " rollingStartNumber = ? " , arrayOf ( key . rollingStartIntervalNumber . toString ( ) ) )
2020-12-20 10:57:24 +00:00
storeOwnKey ( generateCurrentDayTemporaryExposureKey ( ) , database )
2020-08-03 16:07:06 +00:00
}
2020-12-11 10:51:53 +00:00
database . setTransactionSuccessful ( )
val startRollingNumber = ( getDayRollingStartNumber ( intervalNumber ) - 14 * ROLLING _PERIOD )
query ( TABLE _TEK , arrayOf ( " keyData " , " rollingStartNumber " , " rollingPeriod " ) , " rollingStartNumber >= ? AND (rollingStartNumber + rollingPeriod) <= ? " , arrayOf ( startRollingNumber . toString ( ) , intervalNumber . 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
}
} finally {
database . endTransaction ( )
2020-08-03 16:07:06 +00:00
}
}
2021-01-08 14:43:05 +00:00
val rpiHourHistogram : Set < ExposureScanSummary >
2020-08-03 16:07:06 +00:00
get ( ) = readableDatabase . run {
2021-01-08 14:43:05 +00:00
rawQuery ( " SELECT round(timestamp/(60*60*1000))*60*60*1000, COUNT(*), COUNT(*) FROM $TABLE _ADVERTISEMENTS WHERE timestamp > ? GROUP BY round(timestamp/(60*60*1000)) ORDER BY timestamp ASC; " , arrayOf ( ( System . currentTimeMillis ( ) - ( 14 * 24 * 60 * 60 * 1000L ) ) . toString ( ) ) ) . use { cursor ->
val set = hashSetOf < ExposureScanSummary > ( )
2020-08-03 16:07:06 +00:00
while ( cursor . moveToNext ( ) ) {
2021-01-08 14:43:05 +00:00
set . add ( ExposureScanSummary ( cursor . getLong ( 0 ) , cursor . getInt ( 1 ) , cursor . getInt ( 2 ) ) )
2020-08-03 16:07:06 +00:00
}
2021-01-08 14:43:05 +00:00
set
2020-08-03 16:07:06 +00:00
}
}
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
}
}
}
2020-09-27 09:33:02 +00:00
val oldestRpi : Long
2020-08-03 16:07:06 +00:00
get ( ) = readableDatabase . run {
query ( TABLE _ADVERTISEMENTS , arrayOf ( " MIN(timestamp) " ) , null , null , null , null , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
2020-10-07 21:16:53 +00:00
cursor . getLong ( 0 ) . let { if ( it == 0L ) System . currentTimeMillis ( ) else it }
2020-08-03 16:07:06 +00:00
} else {
2020-09-27 09:33:02 +00:00
System . currentTimeMillis ( )
2020-08-03 16:07:06 +00:00
}
}
}
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-10-17 20:43:55 +00:00
fun countDiagnosisKeysInvolved ( tid : Long ) : Long = readableDatabase . run {
val fromFile = rawQuery ( " SELECT SUM( $TABLE _TEK_CHECK_FILE.keys) AS keys FROM $TABLE _TEK_CHECK_FILE_TOKEN JOIN $TABLE _TEK_CHECK_FILE ON $TABLE _TEK_CHECK_FILE_TOKEN.tcfid = $TABLE _TEK_CHECK_FILE.tcfid WHERE $TABLE _TEK_CHECK_FILE_TOKEN.tid = $tid ; " , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
cursor . getLong ( 0 )
} else {
0
}
}
val single = rawQuery ( " SELECT COUNT(*) as keys FROM $TABLE _TEK_CHECK_SINGLE_TOKEN WHERE $TABLE _TEK_CHECK_SINGLE_TOKEN.tid = $tid ; " , null ) . use { cursor ->
if ( cursor . moveToNext ( ) ) {
cursor . getLong ( 0 )
} else {
0
}
}
return fromFile + single
}
fun methodUsageHistogram ( packageName : String ) : List < Pair < String , Int > > = readableDatabase . run {
val list = arrayListOf < Pair < String , Int > > ( )
rawQuery ( " SELECT method, COUNT(*) AS count FROM $TABLE _APP_LOG WHERE package = ? GROUP BY method; " , arrayOf ( packageName ) ) . use { cursor ->
while ( cursor . moveToNext ( ) ) {
list . add ( cursor . getString ( 0 ) to cursor . getInt ( 1 ) )
}
}
list . sortedByDescending { it . second }
}
2020-09-08 22:50:16 +00:00
private fun ensureTemporaryExposureKey ( ) : TemporaryExposureKey = writableDatabase . let { database ->
2020-10-07 21:16:53 +00:00
database . beginTransactionNonExclusive ( )
2020-09-08 22:50:16 +00:00
try {
2020-12-12 11:40:48 +00:00
var key = findOwnKeyAt ( currentIntervalNumber , database )
2020-09-08 22:50:16 +00:00
if ( key == null ) {
2020-12-11 10:51:53 +00:00
key = generateCurrentDayTemporaryExposureKey ( )
2020-09-08 22:50:16 +00:00
storeOwnKey ( key , database )
2020-09-06 12:59:41 +00:00
}
2020-09-08 22:50:16 +00:00
database . setTransactionSuccessful ( )
key
} finally {
database . endTransaction ( )
2020-09-06 12:59:41 +00:00
}
2020-09-08 22:50:16 +00:00
}
2020-08-03 16:07:06 +00:00
2020-09-08 22:50:16 +00:00
val currentRpiId : UUID ?
2020-08-03 16:07:06 +00:00
get ( ) {
2020-12-12 11:40:48 +00:00
val key = findOwnKeyAt ( currentIntervalNumber ) ?: return null
val buffer = ByteBuffer . wrap ( key . generateRpiId ( currentIntervalNumber ) )
2020-08-03 16:07:06 +00:00
return UUID ( buffer . long , buffer . long )
}
2020-12-12 11:40:48 +00:00
fun generateCurrentPayload ( metadata : ByteArray ) = ensureTemporaryExposureKey ( ) . generatePayload ( currentIntervalNumber , metadata )
2020-08-03 16:07:06 +00:00
2020-08-05 12:17:29 +00:00
override fun getWritableDatabase ( ) : SQLiteDatabase {
2020-10-12 19:25:34 +00:00
requirePrimary ( this )
2020-08-24 08:12:49 +00:00
return super . getWritableDatabase ( )
2020-08-05 12:17:29 +00:00
}
2020-10-12 19:25:34 +00:00
@Synchronized
fun ref ( ) : ExposureDatabase {
2020-08-05 12:17:29 +00:00
refCount ++
return this
}
2020-10-12 19:25:34 +00:00
@Synchronized
fun unref ( ) {
2020-08-05 12:17:29 +00:00
refCount --
if ( refCount == 0 ) {
2020-10-12 19:25:34 +00:00
clearInstance ( this )
2020-08-05 12:17:29 +00:00
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 "
2022-01-10 14:55:10 +00:00
private const val DB _VERSION = 11
2020-10-12 19:25:34 +00:00
private const val DB _SIZE _TOO _LARGE = 256L * 1024 * 1024
2020-10-09 14:24:43 +00:00
private const val MAX _DELETE _TIME = 5000L
2020-08-03 16:07:06 +00:00
private const val TABLE _ADVERTISEMENTS = " advertisements "
2020-12-12 11:40:48 +00:00
private const val TABLE _APP = " app "
2020-08-03 16:07:06 +00:00
private const val TABLE _APP _LOG = " app_log "
private const val TABLE _TEK = " tek "
2020-09-27 09:33:02 +00:00
private const val TABLE _APP _PERMS = " app_perms "
private const val TABLE _TOKENS = " tokens "
private const val TABLE _TEK _CHECK _SINGLE = " tek_check_single "
private const val TABLE _TEK _CHECK _SINGLE _TOKEN = " tek_check_single_token "
private const val TABLE _TEK _CHECK _FILE = " tek_check_file "
private const val TABLE _TEK _CHECK _FILE _TOKEN = " tek_check_file_token "
private const val TABLE _TEK _CHECK _FILE _MATCH = " tek_check_file_match "
@Deprecated ( message = " No longer supported " )
2020-08-03 16:07:06 +00:00
private const val TABLE _TEK _CHECK = " tek_check "
2020-09-27 09:33:02 +00:00
@Deprecated ( message = " No longer supported " )
2020-08-03 16:07:06 +00:00
private const val TABLE _DIAGNOSIS = " diagnosis "
2020-09-27 09:33:02 +00:00
@Deprecated ( message = " No longer supported " )
2020-08-03 16:07:06 +00:00
private const val TABLE _CONFIGURATIONS = " configurations "
2020-08-05 12:17:29 +00:00
2020-10-12 19:25:34 +00:00
private var deferredInstance : Deferred < ExposureDatabase > ? = null
private var deferredRefCount : Int = 0
2020-08-05 12:17:29 +00:00
private var instance : ExposureDatabase ? = null
2020-10-12 19:25:34 +00:00
@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 )
2020-08-05 12:17:29 +00:00
}
}
2020-10-12 19:25:34 +00:00
@Synchronized
2020-10-14 09:28:28 +00:00
private fun clearInstance ( database : ExposureDatabase , errorOnNull : Boolean = true ) {
2020-10-12 19:25:34 +00:00
if ( database == instance ) {
if ( deferredRefCount == 0 ) {
deferredInstance = null
instance = null
}
2020-10-14 09:28:28 +00:00
} else if ( errorOnNull || instance != null ) {
2020-10-12 19:25:34 +00:00
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 {
completeInstance ( database )
2020-10-14 09:28:28 +00:00
finishDatabaseMigration ( database , dbMigrateFile , dbMigrateWalFile )
2020-10-12 19:25:34 +00:00
newInstance . complete ( database )
return database
} catch ( e : Exception ) {
2020-10-14 09:28:28 +00:00
clearInstance ( database , false )
2020-10-12 19:25:34 +00:00
database . close ( )
throw e
}
} catch ( e : Exception ) {
newInstance . completeExceptionally ( e )
throw e ;
}
} else {
return instance . await ( ) . ref ( )
}
} finally {
unrefDeferredInstance ( )
}
}
2020-11-30 17:12:21 +00:00
fun export ( context : Context ) {
// FileProvider cannot directly access /databases, so we have to copy the database first
// In addition, we filter out only the Advertisements table to not export sensitive TEKs
// Create a new database which will store only the Advertisements table
val exportDir = File ( context . getCacheDir ( ) , " exposureDatabase " )
exportDir . mkdir ( )
2020-12-16 09:36:47 +00:00
val exportFile = File ( exportDir , " exposure.db " )
2020-11-30 17:12:21 +00:00
if ( exportFile . delete ( ) ) {
Log . d ( " EN-DB-Exporter " , " Deleted old export database. " )
}
val db = SQLiteDatabase . openOrCreateDatabase ( exportFile , null )
// Attach the full database to the new empty db
val databaseFile = context . getDatabasePath ( DB _NAME ) ;
db . execSQL ( " ATTACH ' $databaseFile ' AS fulldb; " )
// copy TABLE_ADVERTISEMENTS over
db . execSQL ( " CREATE TABLE $TABLE _ADVERTISEMENTS AS SELECT * FROM fulldb. $TABLE _ADVERTISEMENTS; " )
// Detach original db, close new db
db . execSQL ( " DETACH DATABASE fulldb; " )
db . close ( )
// Use the FileProvider to get a content URI for the new DB
val fileUri : Uri ? = try {
2020-12-16 18:43:24 +00:00
FileProvider . getUriForFile ( context , " ${context.packageName} .microg.exposure.export " , exportFile )
2020-11-30 17:12:21 +00:00
} catch ( e : IllegalArgumentException ) {
Log . e ( " EN-DB-Exporter " , " The database file can't be shared: $exportFile $e " )
null
}
// Open a sharesheet
if ( fileUri != null ) {
// Grant temporary read permission to the content URI
val sendIntent : Intent = Intent ( ) . apply {
action = Intent . ACTION _SEND
putExtra ( Intent . EXTRA _STREAM , fileUri )
addFlags ( Intent . FLAG _GRANT _READ _URI _PERMISSION )
2020-12-14 01:46:15 +00:00
type = " application/vnd.microg.exposure+sqlite3 "
2020-11-30 17:12:21 +00:00
}
val shareIntent = Intent . createChooser ( sendIntent , null )
context . startActivity ( shareIntent )
}
}
2020-10-12 19:25:34 +00:00
@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 ) }
2020-08-05 12:17:29 +00:00
try {
return call ( it )
} finally {
it . unref ( )
}
}
2020-10-12 19:25:34 +00:00
suspend fun < T > with ( context : Context , call : suspend ( ExposureDatabase ) -> T ) : T = withContext ( Dispatchers . IO ) {
val it = ref ( context )
try {
call ( it )
} finally {
it . unref ( )
}
}
2020-08-03 16:07:06 +00:00
}
}