Unverified Commit 3aad5fc1 authored by Mygod's avatar Mygod Committed by GitHub

Merge pull request #2160 from Mygod/metered

Support metered hint on Android 9+
parents 9a08abc0 cfc75696
{
"formatVersion": 1,
"database": {
"version": 28,
"identityHash": "b60ecca4d684ffe73173478bffd50a17",
"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, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `plugin` TEXT, `udpFallback` INTEGER)",
"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": "tx",
"columnName": "tx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "rx",
"columnName": "rx",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userOrder",
"columnName": "userOrder",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "plugin",
"columnName": "plugin",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "udpFallback",
"columnName": "udpFallback",
"affinity": "INTEGER",
"notNull": false
}
],
"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": []
}
],
"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, \"b60ecca4d684ffe73173478bffd50a17\")"
]
}
}
\ No newline at end of file
...@@ -46,4 +46,11 @@ class MigrationTest { ...@@ -46,4 +46,11 @@ class MigrationTest {
db.close() db.close()
privateDatabase.runMigrationsAndValidate(TEST_DB, 27, true, PrivateDatabase.Migration27) privateDatabase.runMigrationsAndValidate(TEST_DB, 27, true, PrivateDatabase.Migration27)
} }
@Test
@Throws(IOException::class)
fun migrate28() {
val db = privateDatabase.createDatabase(TEST_DB, 27)
db.close()
privateDatabase.runMigrationsAndValidate(TEST_DB, 28, true, PrivateDatabase.Migration28)
}
} }
...@@ -101,12 +101,15 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -101,12 +101,15 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
private var conn: ParcelFileDescriptor? = null private var conn: ParcelFileDescriptor? = null
private var worker: ProtectWorker? = null private var worker: ProtectWorker? = null
private var active = false private var active = false
private var metered = false
private var underlyingNetwork: Network? = null private var underlyingNetwork: Network? = null
set(value) { set(value) {
field = value field = value
if (active && Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(underlyingNetworks) if (active && Build.VERSION.SDK_INT >= 22) setUnderlyingNetworks(underlyingNetworks)
} }
private val underlyingNetworks get() = underlyingNetwork?.let { arrayOf(it) } private val underlyingNetworks get() =
// clearing underlyingNetworks makes Android 9+ consider the network to be metered
if (Build.VERSION.SDK_INT >= 28 && metered) null else underlyingNetwork?.let { arrayOf(it) }
override fun onBind(intent: Intent) = when (intent.action) { override fun onBind(intent: Intent) = when (intent.action) {
SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent) SERVICE_INTERFACE -> super<BaseVpnService>.onBind(intent)
...@@ -190,6 +193,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface { ...@@ -190,6 +193,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
} }
} }
metered = profile.metered
active = true // possible race condition here? active = true // possible race condition here?
if (Build.VERSION.SDK_INT >= 22) builder.setUnderlyingNetworks(underlyingNetworks) if (Build.VERSION.SDK_INT >= 22) builder.setUnderlyingNetworks(underlyingNetworks)
......
...@@ -29,14 +29,15 @@ import com.github.shadowsocks.Core.app ...@@ -29,14 +29,15 @@ import com.github.shadowsocks.Core.app
import com.github.shadowsocks.database.migration.RecreateSchemaMigration import com.github.shadowsocks.database.migration.RecreateSchemaMigration
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
@Database(entities = [Profile::class, KeyValuePair::class], version = 27) @Database(entities = [Profile::class, KeyValuePair::class], version = 28)
abstract class PrivateDatabase : RoomDatabase() { abstract class PrivateDatabase : RoomDatabase() {
companion object { companion object {
private val instance by lazy { private val instance by lazy {
Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE) Room.databaseBuilder(app, PrivateDatabase::class.java, Key.DB_PROFILE)
.addMigrations( .addMigrations(
Migration26, Migration26,
Migration27 Migration27,
Migration28
) )
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.allowMainThreadQueries() .allowMainThreadQueries()
...@@ -61,4 +62,8 @@ abstract class PrivateDatabase : RoomDatabase() { ...@@ -61,4 +62,8 @@ abstract class PrivateDatabase : RoomDatabase() {
override fun migrate(database: SupportSQLiteDatabase) = override fun migrate(database: SupportSQLiteDatabase) =
database.execSQL("ALTER TABLE `Profile` ADD COLUMN `udpFallback` INTEGER") database.execSQL("ALTER TABLE `Profile` ADD COLUMN `udpFallback` INTEGER")
} }
object Migration28 : Migration(27, 28) {
override fun migrate(database: SupportSQLiteDatabase) =
database.execSQL("ALTER TABLE `Profile` ADD COLUMN `metered` INTEGER NOT NULL DEFAULT 0")
}
} }
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
package com.github.shadowsocks.database package com.github.shadowsocks.database
import android.annotation.TargetApi
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import android.util.Base64 import android.util.Base64
...@@ -58,6 +59,8 @@ data class Profile( ...@@ -58,6 +59,8 @@ data class Profile(
var bypass: Boolean = false, var bypass: Boolean = false,
var udpdns: Boolean = false, var udpdns: Boolean = false,
var ipv6: Boolean = true, var ipv6: Boolean = true,
@TargetApi(28)
var metered: Boolean = false,
var individual: String = "", var individual: String = "",
var tx: Long = 0, var tx: Long = 0,
var rx: Long = 0, var rx: Long = 0,
...@@ -70,7 +73,7 @@ data class Profile( ...@@ -70,7 +73,7 @@ data class Profile(
) : Parcelable, Serializable { ) : Parcelable, Serializable {
companion object { companion object {
private const val TAG = "ShadowParser" private const val TAG = "ShadowParser"
private const val serialVersionUID = 0L private const val serialVersionUID = 1L
private val pattern = private val pattern =
"""(?i)ss://[-a-zA-Z0-9+&@#/%?=.~*'()|!:,;\[\]]*[-a-zA-Z0-9+&@#/%=.~*'()|\[\]]""".toRegex() """(?i)ss://[-a-zA-Z0-9+&@#/%?=.~*'()|!:,;\[\]]*[-a-zA-Z0-9+&@#/%=.~*'()|\[\]]""".toRegex()
private val userInfoPattern = "^(.+?):(.*)$".toRegex() private val userInfoPattern = "^(.+?):(.*)$".toRegex()
...@@ -156,6 +159,7 @@ data class Profile( ...@@ -156,6 +159,7 @@ data class Profile(
if (fallback) return@apply if (fallback) return@apply
remoteDns = json.optString("remote_dns", remoteDns) remoteDns = json.optString("remote_dns", remoteDns)
ipv6 = json.optBoolean("ipv6", ipv6) ipv6 = json.optBoolean("ipv6", ipv6)
metered = json.optBoolean("metered", metered)
json.optJSONObject("proxy_apps")?.also { json.optJSONObject("proxy_apps")?.also {
proxyApps = it.optBoolean("enabled", proxyApps) proxyApps = it.optBoolean("enabled", proxyApps)
bypass = it.optBoolean("bypass", bypass) bypass = it.optBoolean("bypass", bypass)
...@@ -232,6 +236,7 @@ data class Profile( ...@@ -232,6 +236,7 @@ data class Profile(
fun copyFeatureSettingsTo(profile: Profile) { fun copyFeatureSettingsTo(profile: Profile) {
profile.route = route profile.route = route
profile.ipv6 = ipv6 profile.ipv6 = ipv6
profile.metered = metered
profile.proxyApps = proxyApps profile.proxyApps = proxyApps
profile.bypass = bypass profile.bypass = bypass
profile.individual = individual profile.individual = individual
...@@ -269,6 +274,7 @@ data class Profile( ...@@ -269,6 +274,7 @@ data class Profile(
put("route", route) put("route", route)
put("remote_dns", remoteDns) put("remote_dns", remoteDns)
put("ipv6", ipv6) put("ipv6", ipv6)
put("metered", metered)
put("proxy_apps", JSONObject().apply { put("proxy_apps", JSONObject().apply {
put("enabled", proxyApps) put("enabled", proxyApps)
if (proxyApps) { if (proxyApps) {
...@@ -295,6 +301,7 @@ data class Profile( ...@@ -295,6 +301,7 @@ data class Profile(
DataStore.bypass = bypass DataStore.bypass = bypass
DataStore.privateStore.putBoolean(Key.udpdns, udpdns) DataStore.privateStore.putBoolean(Key.udpdns, udpdns)
DataStore.privateStore.putBoolean(Key.ipv6, ipv6) DataStore.privateStore.putBoolean(Key.ipv6, ipv6)
DataStore.privateStore.putBoolean(Key.metered, metered)
DataStore.individual = individual DataStore.individual = individual
DataStore.plugin = plugin ?: "" DataStore.plugin = plugin ?: ""
DataStore.udpFallback = udpFallback DataStore.udpFallback = udpFallback
...@@ -315,6 +322,7 @@ data class Profile( ...@@ -315,6 +322,7 @@ data class Profile(
bypass = DataStore.bypass bypass = DataStore.bypass
udpdns = DataStore.privateStore.getBoolean(Key.udpdns, false) udpdns = DataStore.privateStore.getBoolean(Key.udpdns, false)
ipv6 = DataStore.privateStore.getBoolean(Key.ipv6, false) ipv6 = DataStore.privateStore.getBoolean(Key.ipv6, false)
metered = DataStore.privateStore.getBoolean(Key.metered, false)
individual = DataStore.individual individual = DataStore.individual
plugin = DataStore.plugin plugin = DataStore.plugin
udpFallback = DataStore.udpFallback udpFallback = DataStore.udpFallback
......
...@@ -50,6 +50,7 @@ object Key { ...@@ -50,6 +50,7 @@ object Key {
const val bypass = "isBypassApps" const val bypass = "isBypassApps"
const val udpdns = "isUdpDns" const val udpdns = "isUdpDns"
const val ipv6 = "isIpv6" const val ipv6 = "isIpv6"
const val metered = "metered"
const val host = "proxy" const val host = "proxy"
const val password = "sitekey" const val password = "sitekey"
......
...@@ -36,6 +36,8 @@ ...@@ -36,6 +36,8 @@
<!-- feature category --> <!-- feature category -->
<string name="ipv6">IPv6 Route</string> <string name="ipv6">IPv6 Route</string>
<string name="ipv6_summary">Redirect IPv6 traffic to remote</string> <string name="ipv6_summary">Redirect IPv6 traffic to remote</string>
<string name="metered">Metered Hint</string>
<string name="metered_summary">Hint system to treat VPN as metered</string>
<string name="route_list">Route</string> <string name="route_list">Route</string>
<string name="route_entry_all">All</string> <string name="route_entry_all">All</string>
<string name="route_entry_bypass_lan">Bypass LAN</string> <string name="route_entry_bypass_lan">Bypass LAN</string>
......
...@@ -24,6 +24,7 @@ import android.app.Activity ...@@ -24,6 +24,7 @@ import android.app.Activity
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.MenuItem import android.view.MenuItem
...@@ -40,10 +41,7 @@ import com.github.shadowsocks.preference.DataStore ...@@ -40,10 +41,7 @@ import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.preference.IconListPreference import com.github.shadowsocks.preference.IconListPreference
import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener import com.github.shadowsocks.preference.OnPreferenceDataStoreChangeListener
import com.github.shadowsocks.preference.PluginConfigurationDialogFragment import com.github.shadowsocks.preference.PluginConfigurationDialogFragment
import com.github.shadowsocks.utils.Action import com.github.shadowsocks.utils.*
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.github.shadowsocks.utils.readableMessage
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.takisoft.preferencex.EditTextPreference import com.takisoft.preferencex.EditTextPreference
import com.takisoft.preferencex.PreferenceFragmentCompat import com.takisoft.preferencex.PreferenceFragmentCompat
...@@ -84,6 +82,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -84,6 +82,7 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
addPreferencesFromResource(R.xml.pref_profile) addPreferencesFromResource(R.xml.pref_profile)
val serviceMode = DataStore.serviceMode val serviceMode = DataStore.serviceMode
findPreference(Key.remoteDns).isEnabled = serviceMode != Key.modeProxy findPreference(Key.remoteDns).isEnabled = serviceMode != Key.modeProxy
findPreference(Key.ipv6)!!.isEnabled = serviceMode == Key.modeVpn
isProxyApps = findPreference(Key.proxyApps) as SwitchPreference isProxyApps = findPreference(Key.proxyApps) as SwitchPreference
isProxyApps.isEnabled = serviceMode == Key.modeVpn isProxyApps.isEnabled = serviceMode == Key.modeVpn
isProxyApps.setOnPreferenceClickListener { isProxyApps.setOnPreferenceClickListener {
...@@ -91,6 +90,9 @@ class ProfileConfigFragment : PreferenceFragmentCompat(), ...@@ -91,6 +90,9 @@ class ProfileConfigFragment : PreferenceFragmentCompat(),
isProxyApps.isChecked = true isProxyApps.isChecked = true
false false
} }
findPreference(Key.metered)!!.apply {
if (Build.VERSION.SDK_INT >= 28) isEnabled = serviceMode == Key.modeVpn else remove()
}
findPreference(Key.udpdns).isEnabled = serviceMode != Key.modeProxy findPreference(Key.udpdns).isEnabled = serviceMode != Key.modeProxy
plugin = findPreference(Key.plugin) as IconListPreference plugin = findPreference(Key.plugin) as IconListPreference
pluginConfigure = findPreference(Key.pluginConfigure) as EditTextPreference pluginConfigure = findPreference(Key.pluginConfigure) as EditTextPreference
......
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal">
<path android:fillColor="#FF000000" android:pathData="M13,2.05v3.03c3.39,0.49 6,3.39 6,6.92 0,0.9 -0.18,1.75 -0.48,2.54l2.6,1.53c0.56,-1.24 0.88,-2.62 0.88,-4.07 0,-5.18 -3.95,-9.45 -9,-9.95zM12,19c-3.87,0 -7,-3.13 -7,-7 0,-3.53 2.61,-6.43 6,-6.92V2.05c-5.06,0.5 -9,4.76 -9,9.95 0,5.52 4.47,10 9.99,10 3.31,0 6.24,-1.61 8.06,-4.09l-2.6,-1.53C16.17,17.98 14.21,19 12,19z"/>
</vector>
...@@ -64,6 +64,11 @@ ...@@ -64,6 +64,11 @@
android:icon="@drawable/ic_navigation_apps" android:icon="@drawable/ic_navigation_apps"
android:summary="@string/proxied_apps_summary" android:summary="@string/proxied_apps_summary"
android:title="@string/proxied_apps"/> android:title="@string/proxied_apps"/>
<SwitchPreference
android:key="metered"
android:icon="@drawable/ic_device_data_usage"
android:summary="@string/metered_summary"
android:title="@string/metered"/>
<SwitchPreference <SwitchPreference
android:key="isUdpDns" android:key="isUdpDns"
android:icon="@drawable/ic_action_dns" android:icon="@drawable/ic_action_dns"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment