Refactor cookies database with Room
No db migration available since re-login is not difficult
This commit is contained in:
1 parent
a800094b21
commit
d1d1da7875
2 files changed
+114
-162
No files matched your search
@@ -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<String, CookieSet> = db.allCookies
|
||||
|
||||
fun signOut() {
|
||||
|
||||
@@ -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<Cookie, Long> = HashMap()
|
||||
private val helper: SQLiteOpenHelper
|
||||
private val db: SQLiteDatabase
|
||||
private val cookieIdMap: MutableMap<OkHttpCookie, Long> = HashMap()
|
||||
private val db =
|
||||
Room.databaseBuilder(context, CookiesDatabase::class.java, name).allowMainThreadQueries()
|
||||
.build()
|
||||
|
||||
val allCookies: MutableMap<String, CookieSet>
|
||||
get() {
|
||||
val now = System.currentTimeMillis()
|
||||
val map: MutableMap<String, CookieSet> = HashMap()
|
||||
val toRemove: MutableList<Long> = 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
|
||||
}
|
||||
}
|
||||
@Dao
|
||||
interface CookiesDao {
|
||||
@Query("SELECT * FROM $TABLE_COOKIE")
|
||||
fun list(): List<Cookie>
|
||||
|
||||
@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
|
||||
Reference in new issue
Block a user