Signal-Android/app/src/main/java/org/thoughtcrime/securesms/database/LogDatabase.kt

249 wiersze
7.4 KiB
Kotlin

package org.thoughtcrime.securesms.database
import android.annotation.SuppressLint
import android.app.Application
import android.content.ContentValues
import android.database.Cursor
import net.zetetic.database.sqlcipher.SQLiteDatabase
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.crypto.DatabaseSecret
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
import org.thoughtcrime.securesms.database.model.LogEntry
import org.thoughtcrime.securesms.util.ByteUnit
import org.thoughtcrime.securesms.util.CursorUtil
import org.thoughtcrime.securesms.util.SqlUtil
import org.thoughtcrime.securesms.util.Stopwatch
import java.io.Closeable
import java.util.concurrent.TimeUnit
/**
* Stores logs.
*
* Logs are very performance critical. Even though this database is written to on a low-priority background thread, we want to keep throughput high and ensure
* that we aren't creating excess garbage.
*
* This is it's own separate physical database, so it cannot do joins or queries with any other tables.
*/
class LogDatabase private constructor(
application: Application,
databaseSecret: DatabaseSecret
) :
SQLiteOpenHelper(
application,
DATABASE_NAME,
databaseSecret.asString(),
null,
DATABASE_VERSION,
0,
SqlCipherDeletingErrorHandler(DATABASE_NAME),
SqlCipherDatabaseHook()
),
SignalDatabaseOpenHelper {
companion object {
private val TAG = Log.tag(LogDatabase::class.java)
private val MAX_FILE_SIZE = ByteUnit.MEGABYTES.toBytes(20)
private val DEFAULT_LIFESPAN = TimeUnit.DAYS.toMillis(3)
private val LONGER_LIFESPAN = TimeUnit.DAYS.toMillis(14)
private const val DATABASE_VERSION = 2
private const val DATABASE_NAME = "signal-logs.db"
private const val TABLE_NAME = "log"
private const val ID = "_id"
private const val CREATED_AT = "created_at"
private const val KEEP_LONGER = "keep_longer"
private const val BODY = "body"
private const val SIZE = "size"
private val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$CREATED_AT INTEGER,
$KEEP_LONGER INTEGER DEFAULT 0,
$BODY TEXT,
$SIZE INTEGER
)
""".trimIndent()
private val CREATE_INDEXES = arrayOf(
"CREATE INDEX keep_longer_index ON $TABLE_NAME ($KEEP_LONGER)",
"CREATE INDEX log_created_at_keep_longer_index ON $TABLE_NAME ($CREATED_AT, $KEEP_LONGER)"
)
@SuppressLint("StaticFieldLeak") // We hold an Application context, not a view context
@Volatile
private var instance: LogDatabase? = null
@JvmStatic
fun getInstance(context: Application): LogDatabase {
if (instance == null) {
synchronized(LogDatabase::class.java) {
if (instance == null) {
SqlCipherLibraryLoader.load()
instance = LogDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context))
}
}
}
return instance!!
}
}
override fun onCreate(db: SQLiteDatabase) {
Log.i(TAG, "onCreate()")
db.execSQL(CREATE_TABLE)
CREATE_INDEXES.forEach { db.execSQL(it) }
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
Log.i(TAG, "onUpgrade($oldVersion, $newVersion)")
if (oldVersion < 2) {
db.execSQL("DROP TABLE log")
db.execSQL("CREATE TABLE log (_id INTEGER PRIMARY KEY, created_at INTEGER, keep_longer INTEGER DEFAULT 0, body TEXT, size INTEGER)")
db.execSQL("CREATE INDEX keep_longer_index ON log (keep_longer)")
db.execSQL("CREATE INDEX log_created_at_keep_longer_index ON log (created_at, keep_longer)")
}
}
override fun onOpen(db: SQLiteDatabase) {
db.enableWriteAheadLogging()
db.setForeignKeyConstraintsEnabled(true)
}
override fun getSqlCipherDatabase(): SQLiteDatabase {
return writableDatabase
}
fun insert(logs: List<LogEntry>, currentTime: Long) {
val db = writableDatabase
db.beginTransaction()
try {
logs.forEach { log ->
db.insert(TABLE_NAME, null, buildValues(log))
}
db.delete(
TABLE_NAME,
"($CREATED_AT < ? AND $KEEP_LONGER = ?) OR ($CREATED_AT < ? AND $KEEP_LONGER = ?)",
SqlUtil.buildArgs(currentTime - DEFAULT_LIFESPAN, 0, currentTime - LONGER_LIFESPAN, 1)
)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
fun getAllBeforeTime(time: Long): Reader {
return CursorReader(readableDatabase.query(TABLE_NAME, arrayOf(BODY), "$CREATED_AT < ?", SqlUtil.buildArgs(time), null, null, null))
}
fun getRangeBeforeTime(start: Int, length: Int, time: Long): List<String> {
val lines = mutableListOf<String>()
readableDatabase.query(TABLE_NAME, arrayOf(BODY), "$CREATED_AT < ?", SqlUtil.buildArgs(time), null, null, null, "$start,$length").use { cursor ->
while (cursor.moveToNext()) {
lines.add(CursorUtil.requireString(cursor, BODY))
}
}
return lines
}
fun trimToSize() {
val currentTime = System.currentTimeMillis()
val stopwatch = Stopwatch("trim")
val sizeOfSpecialLogs: Long = getSize("$KEEP_LONGER = ?", arrayOf("1"))
val remainingSize = MAX_FILE_SIZE - sizeOfSpecialLogs
stopwatch.split("keepers-size")
if (remainingSize <= 0) {
writableDatabase.delete(TABLE_NAME, "$KEEP_LONGER = ?", arrayOf("0"))
return
}
val sizeDiffThreshold = MAX_FILE_SIZE * 0.01
var lhs: Long = currentTime - DEFAULT_LIFESPAN
var rhs: Long = currentTime
var mid: Long = 0
var sizeOfChunk: Long
while (lhs < rhs - 2) {
mid = (lhs + rhs) / 2
sizeOfChunk = getSize("$CREATED_AT > ? AND $CREATED_AT < ? AND $KEEP_LONGER = ?", SqlUtil.buildArgs(mid, currentTime, 0))
if (sizeOfChunk > remainingSize) {
lhs = mid
} else if (sizeOfChunk < remainingSize) {
if (remainingSize - sizeOfChunk < sizeDiffThreshold) {
break
} else {
rhs = mid
}
} else {
break
}
}
stopwatch.split("binary-search")
writableDatabase.delete(TABLE_NAME, "$CREATED_AT < ? AND $KEEP_LONGER = ?", SqlUtil.buildArgs(mid, 0))
stopwatch.split("delete")
stopwatch.stop(TAG)
}
fun getLogCountBeforeTime(time: Long): Int {
readableDatabase.query(TABLE_NAME, arrayOf("COUNT(*)"), "$CREATED_AT < ?", SqlUtil.buildArgs(time), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
cursor.getInt(0)
} else {
0
}
}
}
private fun buildValues(log: LogEntry): ContentValues {
return ContentValues().apply {
put(CREATED_AT, log.createdAt)
put(KEEP_LONGER, if (log.keepLonger) 1 else 0)
put(BODY, log.body)
put(SIZE, log.body.length)
}
}
private fun getSize(query: String?, args: Array<String>?): Long {
readableDatabase.query(TABLE_NAME, arrayOf("SUM($SIZE)"), query, args, null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
cursor.getLong(0)
} else {
0
}
}
}
interface Reader : Iterator<String>, Closeable
class CursorReader(private val cursor: Cursor) : Reader {
override fun hasNext(): Boolean {
return !cursor.isLast && cursor.count > 0
}
override fun next(): String {
cursor.moveToNext()
return CursorUtil.requireString(cursor, BODY)
}
override fun close() {
cursor.close()
}
}
}