/******************************************************************************
* *
* Copyright (C) 2021 by nekohasekai
*
* *
* 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 . *
* *
******************************************************************************/
package io.nekohasekai.sagernet.database
import android.content.res.Resources
import android.database.sqlite.SQLiteCantOpenDatabaseException
import android.icu.util.ULocale
import android.os.Build
import android.os.LocaleList
import androidx.appcompat.app.AppCompatDelegate
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.aidl.TrafficStats
import io.nekohasekai.sagernet.fmt.AbstractBean
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ktx.applyDefaultValues
import java.io.IOException
import java.sql.SQLException
import java.util.*
object ProfileManager {
interface Listener {
suspend fun onAdd(profile: ProxyEntity)
suspend fun onUpdated(profileId: Long, trafficStats: TrafficStats)
suspend fun onUpdated(profile: ProxyEntity)
suspend fun onRemoved(groupId: Long, profileId: Long)
}
interface RuleListener {
suspend fun onAdd(rule: RuleEntity)
suspend fun onUpdated(rule: RuleEntity)
suspend fun onRemoved(ruleId: Long)
suspend fun onCleared()
}
private val listeners = ArrayList()
private val ruleListeners = ArrayList()
suspend fun iterator(what: suspend Listener.() -> Unit) {
synchronized(listeners) {
listeners.toList()
}.forEach { listener ->
what(listener)
}
}
suspend fun ruleIterator(what: suspend RuleListener.() -> Unit) {
val ruleListeners = synchronized(ruleListeners) {
ruleListeners.toList()
}
for (listener in ruleListeners) {
what(listener)
}
}
fun addListener(listener: Listener) {
synchronized(listeners) {
listeners.add(listener)
}
}
fun removeListener(listener: Listener) {
synchronized(listeners) {
listeners.remove(listener)
}
}
fun addListener(listener: RuleListener) {
synchronized(ruleListeners) {
ruleListeners.add(listener)
}
}
fun removeListener(listener: RuleListener) {
synchronized(ruleListeners) {
ruleListeners.remove(listener)
}
}
suspend fun createProfile(groupId: Long, bean: AbstractBean): ProxyEntity {
bean.applyDefaultValues()
val profile = ProxyEntity(groupId = groupId).apply {
id = 0
putBean(bean)
userOrder = SagerDatabase.proxyDao.nextOrder(groupId) ?: 1
}
profile.id = SagerDatabase.proxyDao.addProxy(profile)
iterator { onAdd(profile) }
return profile
}
suspend fun updateProfile(profile: ProxyEntity) {
SagerDatabase.proxyDao.updateProxy(profile)
iterator { onUpdated(profile) }
}
suspend fun updateProfile(profiles: List) {
SagerDatabase.proxyDao.updateProxy(profiles)
profiles.forEach {
iterator { onUpdated(it) }
}
}
suspend fun deleteProfile(groupId: Long, profileId: Long) {
if (SagerDatabase.proxyDao.deleteById(profileId) == 0) return
if (DataStore.selectedProxy == profileId) {
DataStore.selectedProxy = 0L
}
iterator { onRemoved(groupId, profileId) }
if (SagerDatabase.proxyDao.countByGroup(groupId) > 1) {
GroupManager.rearrange(groupId)
}
}
suspend fun deleteProfile2(groupId: Long, profileId: Long) {
if (SagerDatabase.proxyDao.deleteById(profileId) == 0) return
if (DataStore.selectedProxy == profileId) {
DataStore.selectedProxy = 0L
}
}
fun getProfile(profileId: Long): ProxyEntity? {
if (profileId == 0L) return null
return try {
SagerDatabase.proxyDao.getById(profileId)
} catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex)
} catch (ex: SQLException) {
Logs.w(ex)
null
}
}
fun getProfiles(profileIds: List): List {
if (profileIds.isEmpty()) return listOf()
return try {
SagerDatabase.proxyDao.getEntities(profileIds)
} catch (ex: SQLiteCantOpenDatabaseException) {
throw IOException(ex)
} catch (ex: SQLException) {
Logs.w(ex)
listOf()
}
}
suspend fun postUpdate(profileId: Long) {
postUpdate(getProfile(profileId) ?: return)
}
suspend fun postUpdate(profile: ProxyEntity) {
iterator { onUpdated(profile) }
}
suspend fun postTrafficUpdated(profileId: Long, stats: TrafficStats) {
iterator { onUpdated(profileId, stats) }
}
suspend fun createRule(rule: RuleEntity, post: Boolean = true): RuleEntity {
rule.userOrder = SagerDatabase.rulesDao.nextOrder() ?: 1
rule.id = SagerDatabase.rulesDao.createRule(rule)
if (post) {
ruleIterator { onAdd(rule) }
}
return rule
}
suspend fun updateRule(rule: RuleEntity) {
SagerDatabase.rulesDao.updateRule(rule)
ruleIterator { onUpdated(rule) }
}
suspend fun deleteRule(ruleId: Long) {
SagerDatabase.rulesDao.deleteById(ruleId)
ruleIterator { onRemoved(ruleId) }
}
suspend fun deleteRules(rules: List) {
SagerDatabase.rulesDao.deleteRules(rules)
ruleIterator {
rules.forEach {
onRemoved(it.id)
}
}
}
suspend fun getRules(): List {
var rules = SagerDatabase.rulesDao.allRules()
if (rules.isEmpty() && !DataStore.rulesFirstCreate) {
DataStore.rulesFirstCreate = true
val systemLocale = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> SagerNet.locale.systemLocales[0]!!
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> LocaleList.getDefault()[0]
else -> Locale.getDefault()
}
val appLocales = AppCompatDelegate.getApplicationLocales()
val appLocale = when {
appLocales.size() > 0 -> appLocales[0]!!
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> Resources.getSystem().configuration.locales[0]!!
else -> @Suppress("DEPRECATION") Resources.getSystem().configuration.locale
}
val country = systemLocale.country
val displayCountry = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ULocale.getDisplayCountry(systemLocale.toLanguageTag(), appLocale.toLanguageTag())
} else {
systemLocale.getDisplayCountry(appLocale)
}
when (country) {
"CN" -> {
createRule(
RuleEntity(
name = app.getString(R.string.route_play_store, displayCountry),
domains = "domain:googleapis.cn",
), false
)
createRule(
RuleEntity(
name = app.getString(R.string.route_bypass_domain, displayCountry),
domains = "geosite:cn",
outbound = -1
), false
)
}
"IR" -> {
createRule(
RuleEntity(
name = app.getString(R.string.route_bypass_domain, displayCountry),
domains = "geosite:category-ir",
outbound = -1
), false
)
}
"RU" -> {
createRule(
// https://habr.com/ru/articles/1020080/
// Added because of the request from users. Do not rely on it.
RuleEntity(
name = "UID -1", // TODO: l10n
customPackageNames = listOf("-1"),
outbound = -1
), false
)
createRule(
RuleEntity(
name = app.getString(R.string.route_bypass_domain, displayCountry),
domains = "geosite:category-ru",
outbound = -1
), false
)
}
}
if (country.length == 2) {
createRule(
RuleEntity(
name = app.getString(R.string.route_bypass_ip, displayCountry),
ip = "geoip:${country.lowercase()}",
outbound = -1
), false
)
}
createRule(
RuleEntity(
enabled = true,
name = app.getString(R.string.route_opt_bypass_lan),
ip = "geoip:private",
outbound = -1
), false
)
rules = SagerDatabase.rulesDao.allRules()
}
return rules
}
}