kopia lustrzana https://github.com/ryukoposting/Signal-Android
248 wiersze
7.3 KiB
Kotlin
248 wiersze
7.3 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,
|
|
SqlCipherErrorHandler(DATABASE_NAME),
|
|
SqlCipherDatabaseHook()
|
|
),
|
|
SignalDatabase {
|
|
|
|
companion object {
|
|
private val TAG = Log.tag(LogDatabase::class.java)
|
|
|
|
private val MAX_FILE_SIZE = ByteUnit.MEGABYTES.toBytes(15)
|
|
private val DEFAULT_LIFESPAN = TimeUnit.DAYS.toMillis(2)
|
|
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()
|
|
}
|
|
}
|
|
}
|