Extensions.kt 8.53 KB
Newer Older
1
2
3
package com.frostnerd.smokescreen.database

import android.content.Context
4
import android.util.Base64
5
import androidx.annotation.VisibleForTesting
6
import androidx.fragment.app.Fragment
7
import androidx.room.Room
8
9
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
Daniel Wolf's avatar
Daniel Wolf committed
10
import com.frostnerd.smokescreen.Logger
Daniel Wolf's avatar
Daniel Wolf committed
11
import com.frostnerd.smokescreen.util.parameterizedLazy
12
import okhttp3.internal.toImmutableList
13
14
15
import org.minidns.record.Record
import java.io.ByteArrayInputStream
import java.io.DataInputStream
16

Daniel Wolf's avatar
Daniel Wolf committed
17
18
/*
 * Copyright (C) 2019 Daniel Wolf (Ch4t4r)
19
 *
Daniel Wolf's avatar
Daniel Wolf committed
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * You can contact the developer at daniel.wolf@frostnerd.com.
34
35
 */

Daniel Wolf's avatar
Daniel Wolf committed
36
val EXECUTED_MIGRATIONS
37
38
    get() = _EXECUTED_MIGRATIONS.toImmutableList()
private var _EXECUTED_MIGRATIONS = mutableListOf<Pair<Int, Int>>()
39
40
@VisibleForTesting
val MIGRATION_2_X = migration(2) {
Daniel Wolf's avatar
Daniel Wolf committed
41
    Logger.logIfOpen("DB_MIGRATION", "Migrating from 2 to the current version (${AppDatabase.currentVersion}")
42
43
    it.execSQL("DROP TABLE CachedResponse")
    it.execSQL("CREATE TABLE CachedResponse(type INTEGER NOT NULL, dnsName TEXT NOT NULL, records TEXT NOT NULL, PRIMARY KEY(dnsName, type))")
Daniel Wolf's avatar
Daniel Wolf committed
44
    it.execSQL("DROP TABLE IF EXISTS UserServerConfiguration")
Daniel Wolf's avatar
Daniel Wolf committed
45
    MIGRATION_3_4.migrate(it)
46
    MIGRATION_5_6.migrate(it)
Daniel Wolf's avatar
Daniel Wolf committed
47
    Logger.logIfOpen("DB_MIGRATION", "Migration from 2 to current version completed")
Daniel Wolf's avatar
Daniel Wolf committed
48
}
49
50
@VisibleForTesting
val MIGRATION_3_4 = migration(3, 4) {
Daniel Wolf's avatar
Daniel Wolf committed
51
    Logger.logIfOpen("DB_MIGRATION", "Migrating from 3 to 4")
Daniel Wolf's avatar
Daniel Wolf committed
52
    it.execSQL("CREATE TABLE IF NOT EXISTS `DnsQuery` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `askedServer` TEXT, `fromCache` INTEGER NOT NULL, `questionTime` INTEGER NOT NULL, `responseTime` INTEGER NOT NULL, `responses` TEXT NOT NULL)")
Daniel Wolf's avatar
Daniel Wolf committed
53
    Logger.logIfOpen("DB_MIGRATION", "Migration from 3 to 4 completed")
54
}
55
56
@VisibleForTesting
val MIGRATION_4_5 = migration(4, 5) {
Daniel Wolf's avatar
Daniel Wolf committed
57
    Logger.logIfOpen("DB_MIGRATION", "Migrating from 4 to 5")
58
    it.execSQL("DROP TABLE IF EXISTS UserServerConfiguration")
Daniel Wolf's avatar
Daniel Wolf committed
59
    Logger.logIfOpen("DB_MIGRATION", "Migration from 4 to 5 completed")
60
}
61
62
@VisibleForTesting
val MIGRATION_5_6 = migration(5, 6) {
63
    Logger.logIfOpen("DB_MIGRATION", "Migrating from 5 to 6")
64
    it.execSQL("CREATE TABLE IF NOT EXISTS `DnsRule` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `stagingType` INTEGER NOT NULL, `type` INTEGER NOT NULL, `host` TEXT NOT NULL, `target` TEXT NOT NULL, `ipv6Target` TEXT, `importedFrom` INTEGER, FOREIGN KEY(`importedFrom`) REFERENCES `HostSource`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )")
65
    it.execSQL("CREATE TABLE IF NOT EXISTS `HostSource` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `enabled` INTEGER NOT NULL, `name` TEXT NOT NULL, `source` TEXT NOT NULL)")
66
    it.execSQL("CREATE INDEX IF NOT EXISTS `index_DnsRule_importedFrom` ON `DnsRule` (`importedFrom`)")
Daniel Wolf's avatar
Daniel Wolf committed
67
    it.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_DnsRule_host_type_stagingType` ON `DnsRule` (`host`, `type`, `stagingType`)")
68
69
    Logger.logIfOpen("DB_MIGRATION", "Migration from 5 to 6 completed")
}
70
71
@VisibleForTesting
val MIGRATION_6_7 = migration(6,7) {
Daniel Wolf's avatar
Daniel Wolf committed
72
    Logger.logIfOpen("DB_MIGRATION", "Migrating from 6 to 7")
73
74
    it.execSQL("DROP INDEX IF EXISTS `index_DnsRule_host`")
    it.execSQL("DROP INDEX IF EXISTS `index_DnsRule_host_type`")
75
    it.execSQL("DELETE FROM `DnsRule`")
76
    it.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_DnsRule_host_type_stagingType` ON `DnsRule` (`host`, `type`, `stagingType`)")
Daniel Wolf's avatar
Daniel Wolf committed
77
78
    Logger.logIfOpen("DB_MIGRATION", "Migration from 6 to 7 completed")
}
79
80
@VisibleForTesting
val MIGRATION_7_8 = migration(7,8) {
81
    Logger.logIfOpen("DB_MIGRATION", "Migrating from 7 to 8")
Daniel Wolf's avatar
Daniel Wolf committed
82
    it.execSQL("DROP TABLE IF EXISTS `DnsRule`")
83
    it.execSQL("CREATE TABLE IF NOT EXISTS `DnsRule` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `stagingType` INTEGER NOT NULL, `type` INTEGER NOT NULL, `host` TEXT NOT NULL, `target` TEXT NOT NULL, `ipv6Target` TEXT, `importedFrom` INTEGER, FOREIGN KEY(`importedFrom`) REFERENCES `HostSource`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )")
84
85
    it.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_DnsRule_host_type_stagingType` ON `DnsRule` (`host`, `type`, `stagingType`)")
    it.execSQL("CREATE INDEX IF NOT EXISTS `index_DnsRule_importedFrom` ON `DnsRule` (`importedFrom`)")
86
87
    Logger.logIfOpen("DB_MIGRATION", "Migration from 7 to 8 completed")
}
88
89
@VisibleForTesting
val MIGRATION_8_9 = migration(8, 9) {
90
91
    Logger.logIfOpen("DB_MIGRATION", "Migrating from 8 to 9")
    it.execSQL("ALTER TABLE `HostSource` ADD COLUMN `whitelistSource` INTEGER NOT NULL DEFAULT 0")
92
    it.execSQL("ALTER TABLE `HostSource` ADD COLUMN `ruleCount` INTEGER")
93
94
    Logger.logIfOpen("DB_MIGRATION", "Migration from 8 to 9 completed")
}
95
96
97
98
@VisibleForTesting
val MIGRATION_9_10 = migration(9, 10) {
    Logger.logIfOpen("DB_MIGRATION", "Migrating from 9 to 10")
    it.execSQL("ALTER TABLE `DnsRule` ADD COLUMN `isWildcard` INTEGER NOT NULL DEFAULT 0")
99
    it.execSQL("ALTER TABLE `HostSource` ADD COLUMN `checksum` TEXT DEFAULT NULL")
100
101
    Logger.logIfOpen("DB_MIGRATION", "Migration from 9 to 10 completed")
}
102

103
104
val MIGRATION_10_11 = migration(10, 11) {
    Logger.logIfOpen("DB_MIGRATION", "Migrating from 10 to 11")
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
    val count = it.query("SELECT COUNT(*) FROM 'DnsQuery'").let { cursor ->
        cursor.moveToFirst()
        val cnt = cursor.getInt(0)
        cursor.close()
        cnt
    }

    if(count > 0) {
        it.execSQL("CREATE TABLE `DnsQuery_tmp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `askedServer` TEXT, `responseSource` TEXT NOT NULL, `questionTime` INTEGER NOT NULL, `responseTime` INTEGER NOT NULL, `responses` TEXT NOT NULL)")
        it.execSQL("INSERT INTO `DnsQuery_tmp`(id, type, name, askedServer, questionTime, responseTime, responses, responseSource) SELECT id, type, name, askedServer, questionTime, responseTime, responses, CASE WHEN fromCache=1 THEN 'CACHE' else 'UPSTREAM' END as `responseSource` FROM DnsQuery")
        it.execSQL("DROP TABLE `DnsQuery`")
        it.execSQL("ALTER TABLE `DnsQuery_tmp` RENAME TO `DnsQuery`")
    } else {
        it.execSQL("DROP TABLE `DnsQuery`")
        it.execSQL("CREATE TABLE `DnsQuery` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `askedServer` TEXT, `responseSource` TEXT NOT NULL, `questionTime` INTEGER NOT NULL, `responseTime` INTEGER NOT NULL, `responses` TEXT NOT NULL)")
    }

122
123
    Logger.logIfOpen("DB_MIGRATION", "Migration from 10 to 11 completed")
}
Daniel Wolf's avatar
Daniel Wolf committed
124
125
126
val MIGRATION_11_12 = migration(11, 12) {
    it.execSQL("ALTER TABLE `DnsQuery` ADD COLUMN `isHostBlockedByDnsServer` INTEGER NOT NULL DEFAULT 0")
}
127

Daniel Wolf's avatar
Daniel Wolf committed
128
129
130
private val INSTANCE by parameterizedLazy<AppDatabase, Context> {
    Room.databaseBuilder(it, AppDatabase::class.java, "data")
        .allowMainThreadQueries()
Daniel Wolf's avatar
Daniel Wolf committed
131
        .addMigrations(MIGRATION_2_X, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12)
Daniel Wolf's avatar
Daniel Wolf committed
132
133
        .build()
}
134
135

fun Context.getDatabase(): AppDatabase {
Daniel Wolf's avatar
Daniel Wolf committed
136
    return INSTANCE(applicationContext)
137
138
}

139
140
141
142
fun Fragment.getDatabase():AppDatabase {
    return context!!.getDatabase()
}

143
144
145
146
147
private fun migration(
    from: Int,
    to: Int = AppDatabase.currentVersion,
    migrate: (database: SupportSQLiteDatabase) -> Unit
): Migration {
148
    if(from < 0 || to >AppDatabase.currentVersion || from > to) throw IllegalStateException("Version out of bounds $from->$to with bounds 0 -- ${AppDatabase.currentVersion}")
149
150
    return object : Migration(from, to) {
        override fun migrate(database: SupportSQLiteDatabase) {
151
            _EXECUTED_MIGRATIONS.add(from to to)
152
153
154
            migrate.invoke(database)
        }
    }
155
156
}

157
private fun emptyMigration(from: Int, to: Int = AppDatabase.currentVersion): Migration {
158
159
160
    return migration(from, to) { }
}

161
fun recordFromBase64(base64: String): Record<*> {
162
163
    val bytes = Base64.decode(base64, Base64.NO_WRAP)
    return Record.parse(DataInputStream(ByteArrayInputStream(bytes)), bytes)
164
}