Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subscription support #2392

Merged
merged 15 commits into from Jan 11, 2020
28 changes: 18 additions & 10 deletions core/src/main/java/com/github/shadowsocks/database/Profile.kt
Expand Up @@ -82,7 +82,8 @@ data class Profile(
private val userInfoPattern = "^(.+?):(.*)$".toRegex()
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()

fun findAllUrls(data: CharSequence?, feature: Profile? = null) = pattern.findAll(data ?: "").map {
fun findAllUrls(data: CharSequence?, feature: Profile? = null) = pattern.findAll(data
?: "").map {
val uri = it.value.toUri()
try {
if (uri.userInfo == null) {
Expand Down Expand Up @@ -139,13 +140,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 +179,10 @@ 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()?.joinToString("\n")
?: individual
// JSONArray will return strings with "", which should be removed
individual = individual.replace("\"", "")
}
udpdns = json["udpdns"].optBoolean ?: udpdns
(json["udp_fallback"] as? JsonObject)?.let { tryParse(it, true) }?.also { fallbackMap[this] = it }
Expand All @@ -194,6 +199,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 +216,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 +280,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
Expand Up @@ -54,6 +54,27 @@ object ProfileManager {
return profile
}

fun createProfilesFromSubscription(jsons: Sequence<InputStream>, replace: Boolean,
oldProfiles: List<Profile>?) {
val profiles = oldProfiles?.associateBy { it.formattedAddress }
val feature = profiles?.values?.singleOrNull { it.id == DataStore.profileId }
val lazyClear = lazy { clear() }

jsons.asIterable().forEachTry { json ->
Profile.parseJson(JsonStreamParser(json.bufferedReader()).asSequence().single(), feature) {
if (replace) {
lazyClear.value
}
// if two profiles has the same address, treat them as the same profile and copy stats over
profiles?.get(it.formattedAddress)?.apply {
it.tx = tx
it.rx = rx
}
createProfile(it)
}
}
}

fun createProfilesFromJson(jsons: Sequence<InputStream>, replace: Boolean = false) {
val profiles = if (replace) getAllProfiles()?.associateBy { it.formattedAddress } else null
val feature = if (replace) {
Expand All @@ -74,6 +95,7 @@ object ProfileManager {
}
}
}

fun serializeToJson(profiles: List<Profile>? = getAllProfiles()): JSONArray? {
if (profiles == null) return null
val lookup = LongSparseArray<Profile>(profiles.size).apply { profiles.forEach { put(it.id, it) } }
Expand Down
@@ -0,0 +1,78 @@
/*******************************************************************************
* *
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
* Copyright (C) 2017 by Mygod Studio <contact-shadowsocks-android@mygod.be> *
* *
* 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/>. *
* *
*******************************************************************************/

package com.github.shadowsocks.subscription

import androidx.recyclerview.widget.SortedList
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.asIterable
import java.io.Reader
import java.net.URL

class Subscription {
companion object {
const val SUBSCRIPTION = "subscription"

var instance: Subscription
get() {
val sub = Subscription()
val str = DataStore.publicStore.getString(SUBSCRIPTION)
if (str != null) sub.fromReader(str.reader())
return sub
}
set(value) = DataStore.publicStore.putString(SUBSCRIPTION, value.toString())
}

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 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 urls = SortedList(URL::class.java, URLSorter)

fun fromReader(reader: Reader): Subscription {
urls.clear()
reader.useLines {
for (line in it) {
urls.add(URL(line))
}
}
return this
}

override fun toString(): String {
val result = StringBuilder()
result.append(urls.asIterable().joinToString("\n"))
return result.toString()
}
}
6 changes: 6 additions & 0 deletions core/src/main/res/values/strings.xml
Expand Up @@ -135,6 +135,12 @@
<string name="vpn_connected">Connected, tap to check connection</string>
<string name="not_connected">Not connected</string>

<!-- subscriptions -->
<string name="subscriptions">Subscriptions</string>
<string name="add_subscription">Add a subscription</string>
<string name="edit_subscription">Edit subscription</string>
<string name="update_subscription">Refresh servers from subscription</string>

<!-- acl -->
<string name="custom_rules">Custom rules</string>
<string name="action_add_rule">Add rule(s)…</string>
Expand Down
1 change: 1 addition & 0 deletions mobile/build.gradle
Expand Up @@ -70,6 +70,7 @@ dependencies {
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'me.zhanghai.android.fastscroll:library:1.1.0'
implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1'
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidEspressoVersion"
Expand Down
2 changes: 2 additions & 0 deletions mobile/src/main/java/com/github/shadowsocks/MainActivity.kt
Expand Up @@ -48,6 +48,7 @@ import com.github.shadowsocks.aidl.TrafficStats
import com.github.shadowsocks.bg.BaseService
import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.subscription.SubscriptionFragment
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.SingleInstanceActivity
import com.github.shadowsocks.widget.ListHolderListener
Expand Down Expand Up @@ -215,6 +216,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
return true
}
R.id.customRules -> displayFragment(CustomRulesFragment())
R.id.subscriptions -> displayFragment(SubscriptionFragment())
else -> return false
}
item.isChecked = true
Expand Down
Expand Up @@ -257,6 +257,7 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
}.build().loadAd(AdRequest.Builder().apply {
addTestDevice("B08FC1764A7B250E91EA9D0D5EBEB208")
addTestDevice("7509D18EB8AF82F915874FEF53877A64")
addTestDevice("F58907F28184A828DD0DB6F8E38189C6")
}.build())
} else if (nativeAd != null) populateUnifiedNativeAdView(nativeAd!!, nativeAdView!!)
}
Expand Down Expand Up @@ -590,8 +591,12 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
profilesAdapter.deepRefreshId(profileId)
}

override fun onDestroyView() {
override fun onPause() {
undoManager.flush()
super.onPause()
}

override fun onDestroyView() {
madeye marked this conversation as resolved.
Show resolved Hide resolved
nativeAd?.destroy()
super.onDestroyView()
}
Expand Down