From d1d1da78757d52565e0aeb42753e7b002edf083e Mon Sep 17 00:00:00 2001 From: tarsin Date: Thu, 2 Mar 2023 15:08:08 +0800 Subject: [PATCH] Refactor cookies database with Room No db migration available since re-login is not difficult --- .../hippo/ehviewer/client/EhCookieStore.kt | 2 +- .../java/com/hippo/network/CookieDatabase.kt | 274 ++++++++---------- 2 files changed, 114 insertions(+), 162 deletions(-) diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhCookieStore.kt b/app/src/main/java/com/hippo/ehviewer/client/EhCookieStore.kt index 66e872b73..06d8d2307 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhCookieStore.kt +++ b/app/src/main/java/com/hippo/ehviewer/client/EhCookieStore.kt @@ -30,7 +30,7 @@ import java.util.regex.Pattern typealias KtorCookie = io.ktor.http.Cookie object EhCookieStore : CookieJar, CookiesStorage { - private val db: CookieDatabase = CookieDatabase(EhApplication.application, "okhttp3-cookie.db") + private val db: CookieDatabase = CookieDatabase(EhApplication.application, "okhttp3-cookie-2.db") private val map: MutableMap = db.allCookies fun signOut() { diff --git a/app/src/main/java/com/hippo/network/CookieDatabase.kt b/app/src/main/java/com/hippo/network/CookieDatabase.kt index ef0ef9fa1..06802701d 100644 --- a/app/src/main/java/com/hippo/network/CookieDatabase.kt +++ b/app/src/main/java/com/hippo/network/CookieDatabase.kt @@ -15,200 +15,152 @@ */ package com.hippo.network -import android.content.ContentValues import android.content.Context -import android.database.Cursor -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import android.util.Log -import com.hippo.database.MSQLiteBuilder -import com.hippo.util.SqlUtils -import okhttp3.Cookie +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Delete +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.Update +import okhttp3.Cookie as OkHttpCookie internal class CookieDatabase(context: Context, name: String) { - private val cookieIdMap: MutableMap = HashMap() - private val helper: SQLiteOpenHelper - private val db: SQLiteDatabase + private val cookieIdMap: MutableMap = HashMap() + private val db = + Room.databaseBuilder(context, CookiesDatabase::class.java, name).allowMainThreadQueries() + .build() val allCookies: MutableMap get() { val now = System.currentTimeMillis() val map: MutableMap = HashMap() val toRemove: MutableList = ArrayList() - db.rawQuery("SELECT * FROM $TABLE_COOKIE;", null).use { cursor -> - while (cursor.moveToNext()) { - val id = SqlUtils.getLong(cursor, COLUMN_ID, 0) - val cookie = getCookie(cursor, now) - if (cookie != null) { - // Save id of the cookie in db - cookieIdMap[cookie] = id + db.cookiesDao().list().forEach { + if (!it.persistent || it.expiresAt <= now) { + toRemove.add(it.id!!) + } else { + val okhttpCookie = it.toOkHttp3Cookie() + cookieIdMap[okhttpCookie] = it.id!! - // Put cookie to set - var set = map[cookie.domain] - if (set == null) { - set = CookieSet() - map[cookie.domain] = set - } - set.add(cookie) - } else { - // Mark to remove the cookie - toRemove.add(id) - } + // Put cookie to set + val set = map[it.domain] ?: CookieSet().apply { map[it.domain] = this } + set.add(okhttpCookie) } } - - // Remove invalid or expired cookie if (toRemove.isNotEmpty()) { - val statement = db.compileStatement( - "DELETE FROM $TABLE_COOKIE WHERE $COLUMN_ID = ?;" - ) - db.beginTransaction() - try { - for (id in toRemove) { - statement.bindLong(1, id) + val statement = + db.compileStatement("DELETE FROM $TABLE_COOKIE WHERE $COLUMN_ID = ?;") + db.runInTransaction { + toRemove.forEach { + statement.bindLong(1, it) statement.executeUpdateDelete() } - db.setTransactionSuccessful() - } finally { - db.endTransaction() } } return map } - private fun toContentValues(cookie: Cookie): ContentValues { - val contentValues = ContentValues(9) - contentValues.put(COLUMN_NAME, cookie.name) - contentValues.put(COLUMN_VALUE, cookie.value) - contentValues.put(COLUMN_EXPIRES_AT, cookie.expiresAt) - contentValues.put(COLUMN_DOMAIN, cookie.domain) - contentValues.put(COLUMN_PATH, cookie.path) - contentValues.put(COLUMN_SECURE, cookie.secure) - contentValues.put(COLUMN_HTTP_ONLY, cookie.httpOnly) - contentValues.put(COLUMN_PERSISTENT, cookie.persistent) - contentValues.put(COLUMN_HOST_ONLY, cookie.hostOnly) - return contentValues + fun add(cookie: OkHttpCookie) { + db.cookiesDao().insert(cookie.toCookie()).apply { cookieIdMap[cookie] = this } } - fun add(cookie: Cookie) { - val id = db.insert(TABLE_COOKIE, null, toContentValues(cookie)) - if (id != -1L) { - val oldId = cookieIdMap.put(cookie, id) - if (oldId != null) Log.e(LOG_TAG, "Add a duplicate cookie") - } else { - Log.e(LOG_TAG, "An error occurred when insert a cookie") - } - } - - fun update(from: Cookie, to: Cookie) { - val id = cookieIdMap[from] - if (id == null) { - Log.e(LOG_TAG, "Can't get id when update the cookie") - return - } - val values = toContentValues(to) - val whereClause = "$COLUMN_ID = ?" - val whereArgs = arrayOf(id.toString()) - val count = db.update(TABLE_COOKIE, values, whereClause, whereArgs) - if (count != 1) { - Log.e(LOG_TAG, "Bad result when update cookie: $count") - } - - // Update it in cookie-id map + fun update(from: OkHttpCookie, to: OkHttpCookie) { + val id = cookieIdMap[from] ?: return + db.cookiesDao().update(to.toCookie(id)) cookieIdMap.remove(from) cookieIdMap[to] = id } - fun remove(cookie: Cookie) { - val id = cookieIdMap[cookie] - if (id == null) { - Log.e(LOG_TAG, "Can't get id when remove the cookie") - return - } - val whereClause = "$COLUMN_ID = ?" - val whereArgs = arrayOf(id.toString()) - val count = db.delete(TABLE_COOKIE, whereClause, whereArgs) - if (count != 1) { - Log.e(LOG_TAG, "Bad result when remove cookie: $count") - } - - // Remove it from cookie-id map + fun remove(cookie: OkHttpCookie) { + val id = cookieIdMap[cookie] ?: return + db.cookiesDao().delete(cookie.toCookie(id)) cookieIdMap.remove(cookie) } fun clear() { - db.delete(TABLE_COOKIE, null, null) + db.clearAllTables() cookieIdMap.clear() } +} - fun close() { - db.close() - helper.close() - } +@Entity(tableName = TABLE_COOKIE) +data class Cookie( + @ColumnInfo(name = COLUMN_NAME) + var name: String, + @ColumnInfo(name = COLUMN_VALUE) + var value: String, + @ColumnInfo(name = COLUMN_EXPIRES_AT) + var expiresAt: Long, + @ColumnInfo(name = COLUMN_DOMAIN) + var domain: String, + @ColumnInfo(name = COLUMN_PATH) + var path: String, + @ColumnInfo(name = COLUMN_SECURE) + var secure: Boolean, + @ColumnInfo(name = COLUMN_HTTP_ONLY) + var httpOnly: Boolean, + @ColumnInfo(name = COLUMN_PERSISTENT) + var persistent: Boolean, + @ColumnInfo(name = COLUMN_HOST_ONLY) + var hostOnly: Boolean, + @PrimaryKey + @ColumnInfo(name = COLUMN_ID) + var id: Long? +) - companion object { - private val LOG_TAG = CookieDatabase::class.java.simpleName - private const val VERSION_1 = 1 - private const val TABLE_COOKIE = "OK_HTTP_3_COOKIE" - private const val COLUMN_ID = "_id" - private const val COLUMN_NAME = "NAME" - private const val COLUMN_VALUE = "VALUE" - private const val COLUMN_EXPIRES_AT = "EXPIRES_AT" - private const val COLUMN_DOMAIN = "DOMAIN" - private const val COLUMN_PATH = "PATH" - private const val COLUMN_SECURE = "SECURE" - private const val COLUMN_HTTP_ONLY = "HTTP_ONLY" - private const val COLUMN_PERSISTENT = "PERSISTENT" - private const val COLUMN_HOST_ONLY = "HOST_ONLY" - private const val DB_VERSION = VERSION_1 - private fun getCookie(cursor: Cursor, now: Long): Cookie? { - val name = SqlUtils.getString(cursor, COLUMN_NAME, null) - val value = SqlUtils.getString(cursor, COLUMN_VALUE, null) - val expiresAt = SqlUtils.getLong(cursor, COLUMN_EXPIRES_AT, 0) - val domain = SqlUtils.getString(cursor, COLUMN_DOMAIN, null) - val path = SqlUtils.getString(cursor, COLUMN_PATH, null) - val secure = SqlUtils.getBoolean(cursor, COLUMN_SECURE, false) - val httpOnly = SqlUtils.getBoolean(cursor, COLUMN_HTTP_ONLY, false) - val persistent = SqlUtils.getBoolean(cursor, COLUMN_PERSISTENT, false) - val hostOnly = SqlUtils.getBoolean(cursor, COLUMN_HOST_ONLY, false) - if (name == null || domain == null || path == null) { - return null - } +fun Cookie.toOkHttp3Cookie(): OkHttpCookie { + return OkHttpCookie.Builder().apply { + name(name) + value(value) + expiresAt(expiresAt) + if (hostOnly) hostOnlyDomain(domain) else domain(domain) + path(path) + if (secure) secure() + if (httpOnly) httpOnly() + }.build() +} - // Check non-persistent or expired - if (!persistent || expiresAt <= now) { - return null - } - val builder = Cookie.Builder() - builder.name(name) - builder.value(value) - if (hostOnly) { - builder.hostOnlyDomain(domain) - } else { - builder.domain(domain) - } - builder.path(path) - builder.expiresAt(expiresAt) - if (secure) builder.secure() - if (httpOnly) builder.httpOnly() - return builder.build() - } - } +private fun OkHttpCookie.toCookie(id: Long? = null): Cookie { + return Cookie( + name, value, expiresAt, domain, path, secure, httpOnly, persistent, hostOnly, id + ) +} - init { - helper = MSQLiteBuilder() - .version(VERSION_1) - .createTable(TABLE_COOKIE) - .insertColumn(TABLE_COOKIE, COLUMN_NAME, String::class.java) - .insertColumn(TABLE_COOKIE, COLUMN_VALUE, String::class.java) - .insertColumn(TABLE_COOKIE, COLUMN_EXPIRES_AT, Long::class.javaPrimitiveType) - .insertColumn(TABLE_COOKIE, COLUMN_DOMAIN, String::class.java) - .insertColumn(TABLE_COOKIE, COLUMN_PATH, String::class.java) - .insertColumn(TABLE_COOKIE, COLUMN_SECURE, Boolean::class.javaPrimitiveType) - .insertColumn(TABLE_COOKIE, COLUMN_HTTP_ONLY, Boolean::class.javaPrimitiveType) - .insertColumn(TABLE_COOKIE, COLUMN_PERSISTENT, Boolean::class.javaPrimitiveType) - .insertColumn(TABLE_COOKIE, COLUMN_HOST_ONLY, Boolean::class.javaPrimitiveType) - .build(context, name, DB_VERSION) - db = helper.writableDatabase - } -} \ No newline at end of file +@Dao +interface CookiesDao { + @Query("SELECT * FROM $TABLE_COOKIE") + fun list(): List + + @Delete + fun delete(cookie: Cookie) + + @Insert + fun insert(cookie: Cookie): Long + + @Update + fun update(cookie: Cookie) +} + +@Database(entities = [Cookie::class], version = DB_VERSION, exportSchema = false) +abstract class CookiesDatabase : RoomDatabase() { + abstract fun cookiesDao(): CookiesDao +} + +private const val VERSION_1 = 1 +private const val TABLE_COOKIE = "OK_HTTP_3_COOKIE" +private const val COLUMN_ID = "_id" +private const val COLUMN_NAME = "NAME" +private const val COLUMN_VALUE = "VALUE" +private const val COLUMN_EXPIRES_AT = "EXPIRES_AT" +private const val COLUMN_DOMAIN = "DOMAIN" +private const val COLUMN_PATH = "PATH" +private const val COLUMN_SECURE = "SECURE" +private const val COLUMN_HTTP_ONLY = "HTTP_ONLY" +private const val COLUMN_PERSISTENT = "PERSISTENT" +private const val COLUMN_HOST_ONLY = "HOST_ONLY" +private const val DB_VERSION = VERSION_1