Init commit
This commit is contained in:
58
app/src/main/java/io/nekohasekai/sfa/database/Profile.kt
Normal file
58
app/src/main/java/io/nekohasekai/sfa/database/Profile.kt
Normal file
@@ -0,0 +1,58 @@
|
||||
package io.nekohasekai.sfa.database
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Insert
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.Update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Entity(
|
||||
tableName = "profiles",
|
||||
)
|
||||
@TypeConverters(TypedProfile.Convertor::class)
|
||||
@Parcelize
|
||||
class Profile(
|
||||
@PrimaryKey(autoGenerate = true) var id: Long = 0L,
|
||||
var userOrder: Long = 0L,
|
||||
var name: String = "",
|
||||
var typed: TypedProfile = TypedProfile()
|
||||
) : Parcelable {
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
|
||||
@Insert
|
||||
fun insert(profile: Profile): Long
|
||||
|
||||
@Update
|
||||
fun update(profile: Profile): Int
|
||||
|
||||
@Update
|
||||
fun update(profile: List<Profile>): Int
|
||||
|
||||
@Delete
|
||||
fun delete(profile: Profile): Int
|
||||
|
||||
@Delete
|
||||
fun delete(profile: List<Profile>): Int
|
||||
|
||||
@Query("SELECT * FROM profiles WHERE id = :profileId")
|
||||
fun get(profileId: Long): Profile?
|
||||
|
||||
@Query("select * from profiles order by userOrder asc")
|
||||
fun list(): List<Profile>
|
||||
|
||||
@Query("DELETE FROM profiles")
|
||||
fun clear()
|
||||
|
||||
@Query("SELECT MAX(userOrder) + 1 FROM profiles")
|
||||
fun nextOrder(): Long?
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.nekohasekai.sfa.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(
|
||||
entities = [Profile::class], version = 1
|
||||
)
|
||||
abstract class ProfileDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun profileDao(): Profile.Dao
|
||||
|
||||
}
|
||||
53
app/src/main/java/io/nekohasekai/sfa/database/Profiles.kt
Normal file
53
app/src/main/java/io/nekohasekai/sfa/database/Profiles.kt
Normal file
@@ -0,0 +1,53 @@
|
||||
package io.nekohasekai.sfa.database
|
||||
|
||||
import androidx.room.Room
|
||||
import io.nekohasekai.sfa.Application
|
||||
import io.nekohasekai.sfa.constant.Path
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("RedundantSuspendModifier")
|
||||
object Profiles {
|
||||
|
||||
private val instance by lazy {
|
||||
Application.application.getDatabasePath(Path.PROFILES_DATABASE_PATH).parentFile?.mkdirs()
|
||||
Room.databaseBuilder(
|
||||
Application.application, ProfileDatabase::class.java, Path.PROFILES_DATABASE_PATH
|
||||
).fallbackToDestructiveMigration().setQueryExecutor { GlobalScope.launch { it.run() } }
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun nextOrder(): Long {
|
||||
return instance.profileDao().nextOrder() ?: 0
|
||||
}
|
||||
|
||||
suspend fun get(id: Long): Profile? {
|
||||
return instance.profileDao().get(id)
|
||||
}
|
||||
|
||||
suspend fun create(profile: Profile): Profile {
|
||||
profile.id = instance.profileDao().insert(profile)
|
||||
return profile
|
||||
}
|
||||
|
||||
suspend fun update(profile: Profile): Int {
|
||||
return instance.profileDao().update(profile)
|
||||
}
|
||||
|
||||
suspend fun update(profiles: List<Profile>): Int {
|
||||
return instance.profileDao().update(profiles)
|
||||
}
|
||||
|
||||
suspend fun delete(profile: Profile): Int {
|
||||
return instance.profileDao().delete(profile)
|
||||
}
|
||||
|
||||
suspend fun delete(profiles: List<Profile>): Int {
|
||||
return instance.profileDao().delete(profiles)
|
||||
}
|
||||
|
||||
suspend fun list(): List<Profile> {
|
||||
return instance.profileDao().list()
|
||||
}
|
||||
|
||||
}
|
||||
83
app/src/main/java/io/nekohasekai/sfa/database/Settings.kt
Normal file
83
app/src/main/java/io/nekohasekai/sfa/database/Settings.kt
Normal file
@@ -0,0 +1,83 @@
|
||||
package io.nekohasekai.sfa.database
|
||||
|
||||
import androidx.room.Room
|
||||
import io.nekohasekai.sfa.Application
|
||||
import io.nekohasekai.sfa.bg.ProxyService
|
||||
import io.nekohasekai.sfa.bg.VPNService
|
||||
import io.nekohasekai.sfa.constant.Path
|
||||
import io.nekohasekai.sfa.constant.ServiceMode
|
||||
import io.nekohasekai.sfa.constant.SettingsKey
|
||||
import io.nekohasekai.sfa.database.preference.KeyValueDatabase
|
||||
import io.nekohasekai.sfa.database.preference.RoomPreferenceDataStore
|
||||
import io.nekohasekai.sfa.ktx.boolean
|
||||
import io.nekohasekai.sfa.ktx.int
|
||||
import io.nekohasekai.sfa.ktx.long
|
||||
import io.nekohasekai.sfa.ktx.string
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
object Settings {
|
||||
|
||||
private val instance by lazy {
|
||||
Application.application.getDatabasePath(Path.SETTINGS_DATABASE_PATH).parentFile?.mkdirs()
|
||||
Room.databaseBuilder(
|
||||
Application.application,
|
||||
KeyValueDatabase::class.java,
|
||||
Path.SETTINGS_DATABASE_PATH
|
||||
).allowMainThreadQueries()
|
||||
.fallbackToDestructiveMigration()
|
||||
.setQueryExecutor { GlobalScope.launch { it.run() } }
|
||||
.build()
|
||||
}
|
||||
val dataStore = RoomPreferenceDataStore(instance.keyValuePairDao())
|
||||
var selectedProfile by dataStore.long(SettingsKey.SELECTED_PROFILE) { -1L }
|
||||
var serviceMode by dataStore.string(SettingsKey.SERVICE_MODE) { ServiceMode.NORMAL }
|
||||
var startedByUser by dataStore.boolean(SettingsKey.STARTED_BY_USER)
|
||||
|
||||
const val ANALYSIS_UNKNOWN = -1
|
||||
const val ANALYSIS_ALLOWED = 0
|
||||
const val ANALYSIS_DISALLOWED = 1
|
||||
|
||||
var analyticsAllowed by dataStore.int(SettingsKey.ANALYTICS_ALLOWED) { ANALYSIS_UNKNOWN }
|
||||
var checkUpdateEnabled by dataStore.boolean(SettingsKey.CHECK_UPDATE_ENABLED) { true }
|
||||
|
||||
fun serviceClass(): Class<*> {
|
||||
return when (serviceMode) {
|
||||
ServiceMode.VPN -> VPNService::class.java
|
||||
else -> ProxyService::class.java
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun rebuildServiceMode(): Boolean {
|
||||
var newMode = ServiceMode.NORMAL
|
||||
try {
|
||||
if (needVPNService()) {
|
||||
newMode = ServiceMode.VPN
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
if (serviceMode == newMode) {
|
||||
return false
|
||||
}
|
||||
serviceMode = newMode
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun needVPNService(): Boolean {
|
||||
val selectedProfileId = selectedProfile
|
||||
if (selectedProfileId == -1L) return false
|
||||
val profile = Profiles.get(selectedProfile) ?: return false
|
||||
val content = JSONObject(File(profile.typed.path).readText())
|
||||
val inbounds = content.getJSONArray("inbounds")
|
||||
for (index in 0 until inbounds.length()) {
|
||||
val inbound = inbounds.getJSONObject(index)
|
||||
if (inbound.getString("type") == "tun") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package io.nekohasekai.sfa.database
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.room.TypeConverter
|
||||
import io.nekohasekai.sfa.ktx.marshall
|
||||
import io.nekohasekai.sfa.ktx.unmarshall
|
||||
import java.util.Date
|
||||
|
||||
class TypedProfile() : Parcelable {
|
||||
|
||||
enum class Type {
|
||||
Local, Remote;
|
||||
|
||||
companion object {
|
||||
fun valueOf(value: Int): Type {
|
||||
for (it in values()) {
|
||||
if (it.ordinal == value) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return Local
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var path = ""
|
||||
var type = Type.Local
|
||||
var remoteURL: String = ""
|
||||
var lastUpdated: Date = Date(0)
|
||||
var autoUpdate: Boolean = false
|
||||
var autoUpdateInterval = 60
|
||||
|
||||
constructor(reader: Parcel) : this() {
|
||||
val version = reader.readInt()
|
||||
path = reader.readString() ?: ""
|
||||
type = Type.valueOf(reader.readInt())
|
||||
remoteURL = reader.readString() ?: ""
|
||||
autoUpdate = reader.readInt() == 1
|
||||
lastUpdated = Date(reader.readLong())
|
||||
if (version >= 1) {
|
||||
autoUpdateInterval = reader.readInt()
|
||||
}
|
||||
}
|
||||
|
||||
override fun writeToParcel(writer: Parcel, flags: Int) {
|
||||
writer.writeInt(1)
|
||||
writer.writeString(path)
|
||||
writer.writeInt(type.ordinal)
|
||||
writer.writeString(remoteURL)
|
||||
writer.writeInt(if (autoUpdate) 1 else 0)
|
||||
writer.writeLong(lastUpdated.time)
|
||||
writer.writeInt(autoUpdateInterval)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<TypedProfile> {
|
||||
override fun createFromParcel(parcel: Parcel): TypedProfile {
|
||||
return TypedProfile(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<TypedProfile?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
|
||||
class Convertor {
|
||||
|
||||
@TypeConverter
|
||||
fun marshall(profile: TypedProfile) = profile.marshall()
|
||||
|
||||
@TypeConverter
|
||||
fun unmarshall(content: ByteArray) =
|
||||
content.unmarshall(::TypedProfile)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.nekohasekai.sfa.database.preference
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(
|
||||
entities = [KeyValueEntity::class], version = 1
|
||||
)
|
||||
abstract class KeyValueDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun keyValuePairDao(): KeyValueEntity.Dao
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package io.nekohasekai.sfa.database.preference
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.Query
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
@Entity
|
||||
class KeyValueEntity() : Parcelable {
|
||||
companion object {
|
||||
const val TYPE_UNINITIALIZED = 0
|
||||
const val TYPE_BOOLEAN = 1
|
||||
const val TYPE_FLOAT = 2
|
||||
const val TYPE_LONG = 3
|
||||
const val TYPE_STRING = 4
|
||||
const val TYPE_STRING_SET = 5
|
||||
|
||||
@JvmField
|
||||
val CREATOR = object : Parcelable.Creator<KeyValueEntity> {
|
||||
override fun createFromParcel(parcel: Parcel): KeyValueEntity {
|
||||
return KeyValueEntity(parcel)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<KeyValueEntity?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.room.Dao
|
||||
interface Dao {
|
||||
|
||||
@Query("SELECT * FROM KeyValueEntity")
|
||||
fun all(): List<KeyValueEntity>
|
||||
|
||||
@Query("SELECT * FROM KeyValueEntity WHERE `key` = :key")
|
||||
operator fun get(key: String): KeyValueEntity?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun put(value: KeyValueEntity): Long
|
||||
|
||||
@Query("DELETE FROM KeyValueEntity WHERE `key` = :key")
|
||||
fun delete(key: String): Int
|
||||
|
||||
@Query("DELETE FROM KeyValueEntity")
|
||||
fun reset(): Int
|
||||
|
||||
@Insert
|
||||
fun insert(list: List<KeyValueEntity>)
|
||||
}
|
||||
|
||||
@PrimaryKey
|
||||
var key: String = ""
|
||||
var valueType: Int = TYPE_UNINITIALIZED
|
||||
var value: ByteArray = ByteArray(0)
|
||||
|
||||
val boolean: Boolean?
|
||||
get() = if (valueType == TYPE_BOOLEAN) ByteBuffer.wrap(value).get() != 0.toByte() else null
|
||||
val float: Float?
|
||||
get() = if (valueType == TYPE_FLOAT) ByteBuffer.wrap(value).float else null
|
||||
|
||||
val long: Long
|
||||
get() = ByteBuffer.wrap(value).long
|
||||
|
||||
val string: String?
|
||||
get() = if (valueType == TYPE_STRING) String(value) else null
|
||||
val stringSet: Set<String>?
|
||||
get() = if (valueType == TYPE_STRING_SET) {
|
||||
val buffer = ByteBuffer.wrap(value)
|
||||
val result = HashSet<String>()
|
||||
while (buffer.hasRemaining()) {
|
||||
val chArr = ByteArray(buffer.int)
|
||||
buffer.get(chArr)
|
||||
result.add(String(chArr))
|
||||
}
|
||||
result
|
||||
} else null
|
||||
|
||||
@Ignore
|
||||
constructor(key: String) : this() {
|
||||
this.key = key
|
||||
}
|
||||
|
||||
// putting null requires using DataStore
|
||||
fun put(value: Boolean): KeyValueEntity {
|
||||
valueType = TYPE_BOOLEAN
|
||||
this.value = ByteBuffer.allocate(1).put((if (value) 1 else 0).toByte()).array()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(value: Float): KeyValueEntity {
|
||||
valueType = TYPE_FLOAT
|
||||
this.value = ByteBuffer.allocate(4).putFloat(value).array()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(value: Long): KeyValueEntity {
|
||||
valueType = TYPE_LONG
|
||||
this.value = ByteBuffer.allocate(8).putLong(value).array()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(value: String): KeyValueEntity {
|
||||
valueType = TYPE_STRING
|
||||
this.value = value.toByteArray()
|
||||
return this
|
||||
}
|
||||
|
||||
fun put(value: Set<String>): KeyValueEntity {
|
||||
valueType = TYPE_STRING_SET
|
||||
val stream = ByteArrayOutputStream()
|
||||
val intBuffer = ByteBuffer.allocate(4)
|
||||
for (v in value) {
|
||||
intBuffer.rewind()
|
||||
stream.write(intBuffer.putInt(v.length).array())
|
||||
stream.write(v.toByteArray())
|
||||
}
|
||||
this.value = stream.toByteArray()
|
||||
return this
|
||||
}
|
||||
|
||||
@Suppress("IMPLICIT_CAST_TO_ANY")
|
||||
override fun toString(): String {
|
||||
return when (valueType) {
|
||||
TYPE_BOOLEAN -> boolean
|
||||
TYPE_FLOAT -> float
|
||||
TYPE_LONG -> long
|
||||
TYPE_STRING -> string
|
||||
TYPE_STRING_SET -> stringSet
|
||||
else -> null
|
||||
}?.toString() ?: "null"
|
||||
}
|
||||
|
||||
constructor(parcel: Parcel) : this() {
|
||||
key = parcel.readString()!!
|
||||
valueType = parcel.readInt()
|
||||
value = parcel.createByteArray()!!
|
||||
}
|
||||
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
parcel.writeString(key)
|
||||
parcel.writeInt(valueType)
|
||||
parcel.writeByteArray(value)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.nekohasekai.sfa.database.preference
|
||||
|
||||
import androidx.preference.PreferenceDataStore
|
||||
|
||||
interface OnPreferenceDataStoreChangeListener {
|
||||
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package io.nekohasekai.sfa.database.preference
|
||||
|
||||
import androidx.preference.PreferenceDataStore
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
open class RoomPreferenceDataStore(private val kvPairDao: KeyValueEntity.Dao) :
|
||||
PreferenceDataStore() {
|
||||
|
||||
fun getBoolean(key: String) = kvPairDao[key]?.boolean
|
||||
fun getFloat(key: String) = kvPairDao[key]?.float
|
||||
fun getInt(key: String) = kvPairDao[key]?.long?.toInt()
|
||||
fun getLong(key: String) = kvPairDao[key]?.long
|
||||
fun getString(key: String) = kvPairDao[key]?.string
|
||||
fun getStringSet(key: String) = kvPairDao[key]?.stringSet
|
||||
fun reset() = kvPairDao.reset()
|
||||
|
||||
override fun getBoolean(key: String, defValue: Boolean) = getBoolean(key) ?: defValue
|
||||
override fun getFloat(key: String, defValue: Float) = getFloat(key) ?: defValue
|
||||
override fun getInt(key: String, defValue: Int) = getInt(key) ?: defValue
|
||||
override fun getLong(key: String, defValue: Long) = getLong(key) ?: defValue
|
||||
override fun getString(key: String, defValue: String?) = getString(key) ?: defValue
|
||||
override fun getStringSet(key: String, defValue: MutableSet<String>?) =
|
||||
getStringSet(key) ?: defValue
|
||||
|
||||
fun putBoolean(key: String, value: Boolean?) =
|
||||
if (value == null) remove(key) else putBoolean(key, value)
|
||||
|
||||
fun putFloat(key: String, value: Float?) =
|
||||
if (value == null) remove(key) else putFloat(key, value)
|
||||
|
||||
fun putInt(key: String, value: Int?) =
|
||||
if (value == null) remove(key) else putLong(key, value.toLong())
|
||||
|
||||
fun putLong(key: String, value: Long?) = if (value == null) remove(key) else putLong(key, value)
|
||||
override fun putBoolean(key: String, value: Boolean) {
|
||||
kvPairDao.put(KeyValueEntity(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
override fun putFloat(key: String, value: Float) {
|
||||
kvPairDao.put(KeyValueEntity(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
override fun putInt(key: String, value: Int) {
|
||||
kvPairDao.put(KeyValueEntity(key).put(value.toLong()))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
override fun putLong(key: String, value: Long) {
|
||||
kvPairDao.put(KeyValueEntity(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
override fun putString(key: String, value: String?) = if (value == null) remove(key) else {
|
||||
kvPairDao.put(KeyValueEntity(key).put(value))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
override fun putStringSet(key: String, values: MutableSet<String>?) =
|
||||
if (values == null) remove(key) else {
|
||||
kvPairDao.put(KeyValueEntity(key).put(values))
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
fun remove(key: String) {
|
||||
kvPairDao.delete(key)
|
||||
fireChangeListener(key)
|
||||
}
|
||||
|
||||
private val listeners = HashSet<OnPreferenceDataStoreChangeListener>()
|
||||
private fun fireChangeListener(key: String) {
|
||||
val listeners = synchronized(listeners) {
|
||||
listeners.toList()
|
||||
}
|
||||
listeners.forEach { it.onPreferenceDataStoreChanged(this, key) }
|
||||
}
|
||||
|
||||
fun registerChangeListener(listener: OnPreferenceDataStoreChangeListener) {
|
||||
synchronized(listeners) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
}
|
||||
|
||||
fun unregisterChangeListener(listener: OnPreferenceDataStoreChangeListener) {
|
||||
synchronized(listeners) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user