Init commit

This commit is contained in:
世界
2022-12-02 14:17:47 +08:00
commit 7736e1e644
121 changed files with 6295 additions and 0 deletions

View 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?
}
}

View File

@@ -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
}

View 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()
}
}

View 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
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,7 @@
package io.nekohasekai.sfa.database.preference
import androidx.preference.PreferenceDataStore
interface OnPreferenceDataStoreChangeListener {
fun onPreferenceDataStoreChanged(store: PreferenceDataStore, key: String)
}

View File

@@ -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)
}
}
}