Skip to content

Commit

Permalink
Merge pull request #2392 from shadowsocks/subscription
Browse files Browse the repository at this point in the history
Add subscription support
  • Loading branch information
madeye committed Jan 11, 2020
2 parents ce361da + fed0ca8 commit 406edac
Show file tree
Hide file tree
Showing 21 changed files with 885 additions and 45 deletions.
180 changes: 180 additions & 0 deletions core/schemas/com.github.shadowsocks.database.PrivateDatabase/29.json
@@ -0,0 +1,180 @@
{
"formatVersion": 1,
"database": {
"version": 29,
"identityHash": "5b5c55a1277c63e14416316f9198ed43",
"entities": [
{
"tableName": "Profile",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `host` TEXT NOT NULL, `remotePort` INTEGER NOT NULL, `password` TEXT NOT NULL, `method` TEXT NOT NULL, `route` TEXT NOT NULL, `remoteDns` TEXT NOT NULL, `proxyApps` INTEGER NOT NULL, `bypass` INTEGER NOT NULL, `udpdns` INTEGER NOT NULL, `ipv6` INTEGER NOT NULL, `metered` INTEGER NOT NULL, `individual` TEXT NOT NULL, `plugin` TEXT, `udpFallback` INTEGER, `subscription` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remotePort",
"columnName": "remotePort",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "method",
"columnName": "method",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "route",
"columnName": "route",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remoteDns",
"columnName": "remoteDns",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "proxyApps",
"columnName": "proxyApps",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bypass",
"columnName": "bypass",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "udpdns",
"columnName": "udpdns",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ipv6",
"columnName": "ipv6",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "metered",
"columnName": "metered",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "individual",
"columnName": "individual",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "plugin",
"columnName": "plugin",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "udpFallback",
"columnName": "udpFallback",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "subscription",
"columnName": "subscription",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "tx",
"columnName": "tx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "rx",
"columnName": "rx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userOrder",
"columnName": "userOrder",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "KeyValuePair",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `valueType` INTEGER NOT NULL, `value` BLOB NOT NULL, PRIMARY KEY(`key`))",
"fields": [
{
"fieldPath": "key",
"columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "valueType",
"columnName": "valueType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "value",
"columnName": "value",
"affinity": "BLOB",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"key"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5b5c55a1277c63e14416316f9198ed43')"
]
}
}
17 changes: 2 additions & 15 deletions core/src/main/java/com/github/shadowsocks/acl/Acl.kt
Expand Up @@ -27,6 +27,8 @@ import com.crashlytics.android.Crashlytics
import com.github.shadowsocks.Core
import com.github.shadowsocks.net.Subnet
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.BaseSorter
import com.github.shadowsocks.utils.URLSorter
import com.github.shadowsocks.utils.asIterable
import kotlinx.coroutines.Job
import kotlinx.coroutines.ensureActive
Expand Down Expand Up @@ -111,26 +113,11 @@ class Acl {
}
}

private abstract class BaseSorter<T> : SortedList.Callback<T>() {
override fun onInserted(position: Int, count: Int) { }
override fun areContentsTheSame(oldItem: T?, newItem: T?): Boolean = oldItem == newItem
override fun onMoved(fromPosition: Int, toPosition: Int) { }
override fun onChanged(position: Int, count: Int) { }
override fun onRemoved(position: Int, count: Int) { }
override fun areItemsTheSame(item1: T?, item2: T?): Boolean = item1 == item2
override fun compare(o1: T?, o2: T?): Int =
if (o1 == null) if (o2 == null) 0 else 1 else if (o2 == null) -1 else compareNonNull(o1, o2)
abstract fun compareNonNull(o1: T, o2: T): Int
}
private open class DefaultSorter<T : Comparable<T>> : BaseSorter<T>() {
override fun compareNonNull(o1: T, o2: T): Int = o1.compareTo(o2)
}
private object StringSorter : DefaultSorter<String>()
private object SubnetSorter : DefaultSorter<Subnet>()
private object URLSorter : BaseSorter<URL>() {
private val ordering = compareBy<URL>({ it.host }, { it.port }, { it.file }, { it.protocol })
override fun compareNonNull(o1: URL, o2: URL): Int = ordering.compare(o1, o2)
}

val bypassHostnames = SortedList(String::class.java, StringSorter)
val proxyHostnames = SortedList(String::class.java, StringSorter)
Expand Down
Expand Up @@ -23,6 +23,7 @@ package com.github.shadowsocks.database
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.shadowsocks.Core.app
Expand All @@ -31,15 +32,17 @@ import com.github.shadowsocks.utils.Key
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch

@Database(entities = [Profile::class, KeyValuePair::class], version = 28)
@Database(entities = [Profile::class, KeyValuePair::class], version = 29)
@TypeConverters(Profile.SubscriptionStatus::class)
abstract class PrivateDatabase : RoomDatabase() {
companion object {
private val instance by lazy {
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE).apply {
addMigrations(
Migration26,
Migration27,
Migration28
Migration28,
Migration29
)
allowMainThreadQueries()
enableMultiInstanceInvalidation()
Expand Down Expand Up @@ -70,4 +73,9 @@ abstract class PrivateDatabase : RoomDatabase() {
override fun migrate(database: SupportSQLiteDatabase) =
database.execSQL("ALTER TABLE `Profile` ADD COLUMN `metered` INTEGER NOT NULL DEFAULT 0")
}
object Migration29 : Migration(28, 29) {
override fun migrate(database: SupportSQLiteDatabase) =
database.execSQL("ALTER TABLE `Profile` ADD COLUMN `subscription` INTEGER NOT NULL DEFAULT " +
Profile.SubscriptionStatus.UserConfigured.persistedValue)
}
}
51 changes: 40 additions & 11 deletions core/src/main/java/com/github/shadowsocks/database/Profile.kt
Expand Up @@ -50,6 +50,8 @@ import java.util.*
data class Profile(
@PrimaryKey(autoGenerate = true)
var id: Long = 0,

// user configurable fields
var name: String? = "",
var host: String = sponsored,
var remotePort: Int = 8388,
Expand All @@ -64,15 +66,37 @@ data class Profile(
@TargetApi(28)
var metered: Boolean = false,
var individual: String = "",
var plugin: String? = null,
var udpFallback: Long? = null,

// managed fields
var subscription: SubscriptionStatus = SubscriptionStatus.UserConfigured,
var tx: Long = 0,
var rx: Long = 0,
var userOrder: Long = 0,
var plugin: String? = null,
var udpFallback: Long? = null,

@Ignore // not persisted in db, only used by direct boot
var dirty: Boolean = false
) : Parcelable, Serializable {
enum class SubscriptionStatus(val persistedValue: Int) {
UserConfigured(0),
Active(1),
/**
* This profile is no longer present in subscriptions.
*/
Obsolete(2),
;

companion object {
@JvmStatic
@TypeConverter
fun of(value: Int) = values().single { it.persistedValue == value }
@JvmStatic
@TypeConverter
fun toInt(status: SubscriptionStatus) = status.persistedValue
}
}

companion object {
private const val TAG = "ShadowParser"
private const val serialVersionUID = 1L
Expand Down Expand Up @@ -139,13 +163,15 @@ data class Profile(
private val fallbackMap = mutableMapOf<Profile, Profile>()

private val JsonElement?.optString get() = (this as? JsonPrimitive)?.asString
private val JsonElement?.optBoolean get() = // asBoolean attempts to cast everything to boolean
(this as? JsonPrimitive)?.run { if (isBoolean) asBoolean else null }
private val JsonElement?.optInt get() = try {
(this as? JsonPrimitive)?.asInt
} catch (_: NumberFormatException) {
null
}
private val JsonElement?.optBoolean
get() = // asBoolean attempts to cast everything to boolean
(this as? JsonPrimitive)?.run { if (isBoolean) asBoolean else null }
private val JsonElement?.optInt
get() = try {
(this as? JsonPrimitive)?.asInt
} catch (_: NumberFormatException) {
null
}

private fun tryParse(json: JsonObject, fallback: Boolean = false): Profile? {
val host = json["server"].optString
Expand Down Expand Up @@ -176,8 +202,8 @@ data class Profile(
(json["proxy_apps"] as? JsonObject)?.also {
proxyApps = it["enabled"].optBoolean ?: proxyApps
bypass = it["bypass"].optBoolean ?: bypass
individual = (json["android_list"] as? JsonArray)?.asIterable()?.joinToString("\n") ?:
individual
individual = (it["android_list"] as? JsonArray)?.asIterable()?.mapNotNull { it.optString }
?.joinToString("\n") ?: individual
}
udpdns = json["udpdns"].optBoolean ?: udpdns
(json["udp_fallback"] as? JsonObject)?.let { tryParse(it, true) }?.also { fallbackMap[this] = it }
Expand All @@ -194,6 +220,7 @@ data class Profile(
// ignore other types
}
}

fun finalize(create: (Profile) -> Unit) {
val profiles = ProfileManager.getAllProfiles() ?: emptyList()
for ((profile, fallback) in fallbackMap) {
Expand All @@ -210,6 +237,7 @@ data class Profile(
}
}
}

fun parseJson(json: JsonElement, feature: Profile? = null, create: (Profile) -> Unit) {
JsonParser(feature).run {
process(json)
Expand Down Expand Up @@ -273,6 +301,7 @@ data class Profile(
if (!name.isNullOrEmpty()) builder.fragment(name)
return builder.build()
}

override fun toString() = toUri().toString()

fun toJson(profiles: LongSparseArray<Profile>? = null): JSONObject = JSONObject().apply {
Expand Down

0 comments on commit 406edac

Please sign in to comment.