From 93084f0ed08a5aaba355179821a833ff6ea46cc5 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 6 Dec 2015 18:49:48 +0800 Subject: [PATCH 1/4] Ask for ROOT only when it's needed --- src/main/res/values-zh/strings.xml | 9 ++-- src/main/res/values/strings.xml | 9 ++-- src/main/res/xml/pref_all.xml | 10 +++-- .../com/github/shadowsocks/AppManager.scala | 4 +- .../com/github/shadowsocks/Shadowsocks.scala | 43 ++++++++----------- .../shadowsocks/ShadowsocksApplication.scala | 5 +-- .../shadowsocks/ShadowsocksNatService.scala | 20 ++++----- .../shadowsocks/ShadowsocksSettings.scala | 17 +++----- .../shadowsocks/ShadowsocksVpnService.scala | 6 +-- .../com/github/shadowsocks/package.scala | 13 ++++++ .../preferences/NatSwitchPreference.scala | 27 ------------ .../github/shadowsocks/utils/Console.scala | 14 ++---- .../com/github/shadowsocks/utils/Utils.scala | 17 ++++---- 13 files changed, 82 insertions(+), 112 deletions(-) create mode 100644 src/main/scala/com/github/shadowsocks/package.scala delete mode 100644 src/main/scala/com/github/shadowsocks/preferences/NatSwitchPreference.scala diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml index c3910a87..b79e5088 100755 --- a/src/main/res/values-zh/strings.xml +++ b/src/main/res/values-zh/strings.xml @@ -34,6 +34,8 @@ 随系统启动后台服务 后台服务已开始运行。 无法连接远程服务器 + NAT 模式需要 ROOT 权限 + 切换到 VPN 模式 停止服务 正在关闭…… 后台服务启动失败:%s @@ -52,14 +54,15 @@ 关于 影梭(shadowsocks)%s 刷新 DNS 缓存 - 需要 ROOT 权限 + 只需在 NAT 模式下使用 + 只需在 NAT 模式下使用,需要 ROOT 权限 + 在没有 ROOT 权限的情况下无法刷新 DNS 缓存 刷新中…… 二维码 添加配置文件 NAT 模式 (仅限调试) - 从 VPN 模式切换为 NAT 模式 - NAT 模式已禁用,需要 ROOT 权限 + 从 VPN 模式切换为 NAT 模式,需要 ROOT 权限 UDP 转发 由远程服务器转发 UDP 协议的数据包 diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 7c154a9f..d3d7696f 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -10,8 +10,7 @@ Profile Switch to another profile or add new profiles NAT mode (deprecated) - Use NAT mode instead of VPN mode - NAT mode is disabled without root + Use NAT mode instead of VPN mode. Requires ROOT permission. Network Traffic Sent: \t\t\t\t\t%3$s\t↑\t%1$s/s\nReceived: \t%4$s\t↓\t%2$s/s \n%1$s↑\t%2$s↓ @@ -61,6 +60,8 @@ Shadowsocks started. Failed to connect the remote server + NAT mode requires ROOT permission + Switch to VPN mode Stop the service Shutting down… %s @@ -84,7 +85,9 @@ Shadowsocks %s QR Code Flush DNS cache - Requires ROOT permission + Only useful in NAT mode + Only useful in NAT mode. Requires ROOT permission. + Flush DNS cache cannot proceed without ROOT permission Flushing Add Profile Export to Clipboard diff --git a/src/main/res/xml/pref_all.xml b/src/main/res/xml/pref_all.xml index 20f23e2a..c5d7a3b5 100644 --- a/src/main/res/xml/pref_all.xml +++ b/src/main/res/xml/pref_all.xml @@ -83,12 +83,14 @@ android:summary="@string/auto_connect_summary" android:title="@string/auto_connect"/> - + - + diff --git a/src/main/scala/com/github/shadowsocks/AppManager.scala b/src/main/scala/com/github/shadowsocks/AppManager.scala index d08e8acb..81c17db3 100644 --- a/src/main/scala/com/github/shadowsocks/AppManager.scala +++ b/src/main/scala/com/github/shadowsocks/AppManager.scala @@ -58,8 +58,6 @@ import com.github.shadowsocks.utils.{Key, Utils} import scala.collection.JavaConversions._ import scala.collection.mutable -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future import scala.language.implicitConversions object AppManager { @@ -249,7 +247,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC } def loadAppsAsync() { - Future { + ThrowableFuture { while (!appsLoading) loadApps() appsLoading = false handler.post(() => { diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala index 37895f30..881c999e 100644 --- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala +++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala @@ -65,8 +65,6 @@ import com.github.shadowsocks.utils._ import com.google.android.gms.ads.{AdRequest, AdSize, AdView} import scala.collection.mutable.ArrayBuffer -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future object Typefaces { def get(c: Context, assetPath: String): Typeface = { @@ -182,8 +180,13 @@ class Shadowsocks handler.postDelayed(() => fabProgressCircle.hide(), 1000) fab.setEnabled(true) changeSwitch(checked = false) - if (m != null) Snackbar.make(findViewById(android.R.id.content), - getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG).show + if (m != null) { + val snackbar = Snackbar.make(findViewById(android.R.id.content), + getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG) + if (m == getString(R.string.nat_no_root)) snackbar.setAction(R.string.switch_to_vpn, + (_ => preferences.natSwitch.setChecked(false)): View.OnClickListener) + snackbar.show + } setPreferenceEnabled(enabled = true) case State.STOPPING => fab.setBackgroundTintList(greyTint) @@ -317,10 +320,10 @@ class Shadowsocks cmd.append("chmod 666 %s%s-vpn.pid".formatLocal(Locale.ENGLISH, Path.BASE, task)) } - if (!ShadowsocksApplication.isVpnEnabled) { - Console.runRootCommand(cmd.toArray) - } else { + if (ShadowsocksApplication.isVpnEnabled) { Console.runCommand(cmd.toArray) + } else { + Console.runRootCommand(cmd.toArray) } cmd.clear() @@ -341,8 +344,7 @@ class Shadowsocks cmd.append("rm -f %s%s-vpn.pid".formatLocal(Locale.ENGLISH, Path.BASE, task)) cmd.append("rm -f %s%s-vpn.conf".formatLocal(Locale.ENGLISH, Path.BASE, task)) } - Console.runCommand(cmd.toArray) - if (!ShadowsocksApplication.isVpnEnabled) { + if (ShadowsocksApplication.isVpnEnabled) Console.runCommand(cmd.toArray) else { Console.runRootCommand(cmd.toArray) Console.runRootCommand(Utils.getIptables + " -t nat -F OUTPUT") } @@ -361,7 +363,7 @@ class Shadowsocks } def prepareStartService() { - Future { + ThrowableFuture { if (ShadowsocksApplication.isVpnEnabled) { val intent = VpnService.prepare(this) if (intent != null) { @@ -534,18 +536,6 @@ class Shadowsocks handler.removeCallbacksAndMessages(null) } - def copyToSystem() { - val ab = new ArrayBuffer[String] - ab.append("mount -o rw,remount -t yaffs2 /dev/block/mtdblock3 /system") - for (executable <- Shadowsocks.EXECUTABLES) { - ab.append("cp %s%s /system/bin/".formatLocal(Locale.ENGLISH, Path.BASE, executable)) - ab.append("chmod 755 /system/bin/" + executable) - ab.append("chown root:shell /system/bin/" + executable) - } - ab.append("mount -o ro,remount -t yaffs2 /dev/block/mtdblock3 /system") - Console.runRootCommand(ab.toArray) - } - def install() { copyAssets(System.getABI) @@ -565,7 +555,7 @@ class Shadowsocks def recovery() { serviceStop() val h = showProgress(R.string.recovering) - Future { + ThrowableFuture { reset() h.sendEmptyMessage(0) } @@ -573,8 +563,9 @@ class Shadowsocks def flushDnsCache() { val h = showProgress(R.string.flushing) - Future { - Utils.toggleAirplaneMode(getBaseContext) + ThrowableFuture { + if (!Utils.toggleAirplaneMode(getBaseContext)) h.post(() => Snackbar.make(findViewById(android.R.id.content), + R.string.flush_dnscache_no_root, Snackbar.LENGTH_LONG).show) h.sendEmptyMessage(0) } } @@ -595,7 +586,7 @@ class Shadowsocks def checkText(key: String): Boolean = { val text = ShadowsocksApplication.settings.getString(key, "") if (text != null && text.length > 0) return true - Snackbar.make(findViewById(android.R.id.content), getString(R.string.proxy_empty), Snackbar.LENGTH_LONG).show + Snackbar.make(findViewById(android.R.id.content), R.string.proxy_empty, Snackbar.LENGTH_LONG).show false } diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala index 7bd76fe6..07eb2a60 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala @@ -46,7 +46,7 @@ import android.app.Application import android.content.pm.PackageManager import android.preference.PreferenceManager import com.github.shadowsocks.database.{DBHelper, ProfileManager} -import com.github.shadowsocks.utils.{Console, Key, Utils} +import com.github.shadowsocks.utils.{Key, Utils} import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders} import com.google.android.gms.common.api.ResultCallback import com.google.android.gms.tagmanager.{ContainerHolder, TagManager} @@ -60,8 +60,7 @@ object ShadowsocksApplication { lazy val settings = PreferenceManager.getDefaultSharedPreferences(instance) lazy val profileManager = new ProfileManager(settings, dbHelper) - val isRoot = Console.isRoot - def isVpnEnabled = !(isRoot && settings.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove)) + def isVpnEnabled = !settings.getBoolean(Key.isNAT, false) def getVersionName = try { instance.getPackageManager.getPackageInfo(instance.getPackageName, 0).versionName diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index 865cc586..26374ffc 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -55,8 +55,6 @@ import com.github.shadowsocks.utils._ import scala.collection.JavaConversions._ import scala.collection.mutable.ArrayBuffer -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future class ShadowsocksNatService extends BaseService { @@ -144,7 +142,7 @@ class ShadowsocksNatService extends BaseService { }) Console.runRootCommand(cmdBuf.toArray) } else { - Console.runRootCommand(Array("ndc resolver flushdefaultif", "ndc resolver flushif wlan0")) + Console.runRootCommand("ndc resolver flushdefaultif", "ndc resolver flushif wlan0") } } @@ -384,12 +382,17 @@ class ShadowsocksNatService extends BaseService { } } } - Console.runRootCommand(init_sb.toArray) - Console.runRootCommand(http_sb.toArray) + Console.runRootCommand((init_sb ++ http_sb).toArray) } override def startRunner(config: Config) { + changeState(State.CONNECTING) + if (!Console.isRoot) { + changeState(State.STOPPED, getString(R.string.nat_no_root)) + return + } + super.startRunner(config) // register close receiver @@ -404,10 +407,7 @@ class ShadowsocksNatService extends BaseService { ShadowsocksApplication.track(TAG, "start") - changeState(State.CONNECTING) - - Future { - + ThrowableFuture { if (config.proxy == "198.199.101.152") { val holder = ShadowsocksApplication.containerHolder try { @@ -465,7 +465,7 @@ class ShadowsocksNatService extends BaseService { closeReceiver = null } - notification.destroy() + if (notification != null) notification.destroy() ShadowsocksApplication.track(TAG, "stop") diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala index b63c7d81..cc67d043 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala @@ -14,6 +14,7 @@ import com.github.shadowsocks.utils.Key // TODO: Move related logic here class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChangeListener { private lazy val activity = getActivity.asInstanceOf[Shadowsocks] + lazy val natSwitch = findPreference(Key.isNAT).asInstanceOf[SwitchPreference] private var isProxyApps: SwitchPreference = _ @@ -35,10 +36,8 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan }) val flush = findPreference("flush_dnscache") - if (Build.VERSION.SDK_INT >= 17 && !ShadowsocksApplication.isRoot) { - flush.setEnabled(false) - flush.setSummary(R.string.flush_dnscache_summary_disabled) - } else flush.setOnPreferenceClickListener((preference: Preference) => { + if (Build.VERSION.SDK_INT < 17) flush.setSummary(R.string.flush_dnscache_summary) + flush.setOnPreferenceClickListener(_ => { ShadowsocksApplication.track(Shadowsocks.TAG, "flush_dnscache") activity.flushDnsCache() true @@ -73,12 +72,10 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan } def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) = key match { - case Key.isNAT => if (ShadowsocksApplication.isRoot && activity != null) { - activity.handler.post(() => { - activity.deattachService - activity.attachService - }) - } + case Key.isNAT => activity.handler.post(() => { + activity.deattachService + activity.attachService + }) case _ => } } diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index 3140baaf..ac832422 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -55,8 +55,6 @@ import org.apache.commons.net.util.SubnetUtils import scala.collection.JavaConversions._ import scala.collection.mutable.ArrayBuffer -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future class ShadowsocksVpnService extends VpnService with BaseService { val TAG = "ShadowsocksVpnService" @@ -122,7 +120,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { vpnThread = null } - notification.destroy() + if (notification != null) notification.destroy() // channge the state changeState(State.STOPPING) @@ -213,7 +211,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { changeState(State.CONNECTING) - Future { + ThrowableFuture { if (config.proxy == "198.199.101.152") { val holder = ShadowsocksApplication.containerHolder try { diff --git a/src/main/scala/com/github/shadowsocks/package.scala b/src/main/scala/com/github/shadowsocks/package.scala new file mode 100644 index 00000000..a6db148d --- /dev/null +++ b/src/main/scala/com/github/shadowsocks/package.scala @@ -0,0 +1,13 @@ +package com.github + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +/** + * @author Mygod + */ +package object shadowsocks { + def ThrowableFuture[T](f: => T) = Future(f) onFailure { + case e: Throwable => e.printStackTrace() + } +} diff --git a/src/main/scala/com/github/shadowsocks/preferences/NatSwitchPreference.scala b/src/main/scala/com/github/shadowsocks/preferences/NatSwitchPreference.scala deleted file mode 100644 index 4471c329..00000000 --- a/src/main/scala/com/github/shadowsocks/preferences/NatSwitchPreference.scala +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.shadowsocks.preferences - -import android.content.Context -import android.content.res.TypedArray -import android.preference.SwitchPreference -import android.util.AttributeSet -import com.github.shadowsocks.{R, ShadowsocksApplication} - -/** - * TODO: No animations? - * @author Mygod - */ -final class NatSwitchPreference(context: Context, attrs: AttributeSet) extends SwitchPreference(context, attrs) { - private var disabled = false - - override def isEnabled = super.isEnabled && !disabled - override def setEnabled(b: Boolean) = super.setEnabled(b && !disabled) - - if (!ShadowsocksApplication.isRoot) { - disabled = true - setEnabled(false) - setSummary(R.string.nat_summary_no_root) - } - - override def onGetDefaultValue(a: TypedArray, index: Int) = - (!ShadowsocksApplication.isVpnEnabled).asInstanceOf[AnyRef] -} diff --git a/src/main/scala/com/github/shadowsocks/utils/Console.scala b/src/main/scala/com/github/shadowsocks/utils/Console.scala index 60edd8f7..042dc9ff 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Console.scala +++ b/src/main/scala/com/github/shadowsocks/utils/Console.scala @@ -75,17 +75,11 @@ object Console { shell.close() } - def runRootCommand(command: String): String = runRootCommand(Array(command)) - def runRootCommand(command: String, context: String): String = runRootCommand(Array(command), context) - def runRootCommand(commands: Array[String]): String = runRootCommand(commands, "u:r:init_shell:s0") - - def runRootCommand(commands: Array[String], context: String): String = { - if (!isRoot) { - return null - } - val shell = openRootShell(context) + def runRootCommand(commands: String*): String = runRootCommand(commands.toArray) + def runRootCommand(commands: Array[String]): String = { + val shell = openRootShell("u:r:init_shell:s0") val sb = new StringBuilder - shell.addCommand(commands, 0, ((commandCode: Int, exitCode: Int, output: util.List[String]) => { + shell.addCommand(commands, 0, ((_, exitCode, output) => { if (exitCode < 0) { shell.close() } else { diff --git a/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/src/main/scala/com/github/shadowsocks/utils/Utils.scala index f0f7301d..3754efaa 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Utils.scala +++ b/src/main/scala/com/github/shadowsocks/utils/Utils.scala @@ -140,12 +140,14 @@ object Utils { } // Blocked > 3 seconds - def toggleAirplaneMode(context: Context) { - if (ShadowsocksApplication.isRoot) { - Console.runRootCommand(Array("ndc resolver flushdefaultif", "ndc resolver flushif wlan0")) + def toggleAirplaneMode(context: Context) = { + if (Console.isRoot) { + Console.runRootCommand("ndc resolver flushdefaultif", "ndc resolver flushif wlan0") + true } else if (Build.VERSION.SDK_INT < 17) { toggleBelowApiLevel17(context) - } + true + } else false } //noinspection ScalaDeprecation @@ -305,8 +307,7 @@ object Utils { var compatible: Boolean = false var version: Boolean = false - val command = Array(iptables + " --version", iptables + " -L -t nat -n") - val lines = Console.runRootCommand(command) + val lines = Console.runRootCommand(iptables + " --version", iptables + " -L -t nat -n") if (lines == null) return if (lines.contains("OUTPUT")) { @@ -384,9 +385,7 @@ object Utils { hasRedirectSupport = 1 val command = Utils.getIptables + " -t nat -A OUTPUT -p udp --dport 54 -j REDIRECT --to 8154" - val lines = Console.runRootCommand(command) - - Console.runRootCommand(command.replace("-A", "-D")) + val lines = Console.runRootCommand(command, command.replace("-A", "-D")) if (lines == null) return if (lines.contains("No chain/target/match")) { hasRedirectSupport = 0 From ad831aa1f43fbd89c067ccb798dfcfe8e7bb4fc4 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 6 Dec 2015 21:20:30 +0800 Subject: [PATCH 2/4] Persist package names in individual/proxied --- .../com/github/shadowsocks/AppManager.scala | 39 ++++++++++--------- .../com/github/shadowsocks/Shadowsocks.scala | 15 ++++--- .../shadowsocks/ShadowsocksNatService.scala | 18 +++++---- .../shadowsocks/ShadowsocksVpnService.scala | 2 +- .../shadowsocks/database/DBHelper.scala | 26 ++++++++++--- 5 files changed, 62 insertions(+), 38 deletions(-) diff --git a/src/main/scala/com/github/shadowsocks/AppManager.scala b/src/main/scala/com/github/shadowsocks/AppManager.scala index 81c17db3..0d9dbab8 100644 --- a/src/main/scala/com/github/shadowsocks/AppManager.scala +++ b/src/main/scala/com/github/shadowsocks/AppManager.scala @@ -61,13 +61,13 @@ import scala.collection.mutable import scala.language.implicitConversions object AppManager { - case class ProxiedApp(uid: Int, name: String, packageName: String, icon: Drawable) + case class ProxiedApp(name: String, packageName: String, icon: Drawable) private case class ListEntry(switch: Switch, text: TextView, icon: ImageView) var cachedApps: Array[ProxiedApp] = _ private def getApps(pm: PackageManager) = { - if (cachedApps == null) cachedApps = pm.getInstalledApplications(0).filter(_.uid >= 10000) - .map(a => new ProxiedApp(a.uid, pm.getApplicationLabel(a).toString, a.packageName, a.loadIcon(pm))).toArray + if (cachedApps == null) cachedApps = pm.getInstalledApplications(0) + .map(a => new ProxiedApp(pm.getApplicationLabel(a).toString, a.packageName, a.loadIcon(pm))).toArray cachedApps } } @@ -77,7 +77,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC import AppManager._ private var apps: Array[ProxiedApp] = _ - private var proxiedApps: mutable.HashSet[Int] = _ + private var proxiedApps: mutable.HashSet[String] = _ private var toolbar: Toolbar = _ private var appListView: ListView = _ private var loadingView: View = _ @@ -87,10 +87,10 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC def loadApps() { appsLoading = true - proxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "").split('|').map(_.toInt).to[mutable.HashSet] + proxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "").split('\n').to[mutable.HashSet] apps = getApps(getPackageManager).sortWith((a, b) => { - val aProxied = proxiedApps.contains(a.uid) - if (aProxied ^ proxiedApps.contains(b.uid)) aProxied else a.name < b.name + val aProxied = proxiedApps.contains(a.packageName) + if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name < b.name }) adapter = new ArrayAdapter[ProxiedApp](this, R.layout.layout_apps_item, R.id.itemtext, apps) { override def getView(position: Int, view: View, parent: ViewGroup): View = { @@ -114,19 +114,19 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC entry.icon.setImageDrawable(app.icon) val switch = entry.switch switch.setTag(app) - switch.setChecked(proxiedApps.contains(app.uid)) + switch.setChecked(proxiedApps.contains(app.packageName)) entry.text.setTag(switch) convertView } } } - private def setProxied(uid: Int, proxied: Boolean) = if (proxied) proxiedApps.add(uid) else proxiedApps.remove(uid) + private def setProxied(pn: String, proxied: Boolean) = if (proxied) proxiedApps.add(pn) else proxiedApps.remove(pn) /** Called an application is check/unchecked */ def onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { val app: ProxiedApp = buttonView.getTag.asInstanceOf[ProxiedApp] - if (app != null) setProxied(app.uid, isChecked) + if (app != null) setProxied(app.packageName, isChecked) saveAppSettings(this) } @@ -134,8 +134,8 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC val switch = v.getTag.asInstanceOf[ListEntry].switch val app: ProxiedApp = switch.getTag.asInstanceOf[ProxiedApp] if (app != null) { - val proxied = !proxiedApps.contains(app.uid) - setProxied(app.uid, proxied) + val proxied = !proxiedApps.contains(app.packageName) + setProxied(app.packageName, proxied) switch.setChecked(proxied) } saveAppSettings(this) @@ -156,7 +156,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC case R.id.action_export => val bypass = prefs.getBoolean(Key.isBypassApps, false) val proxiedAppString = prefs.getString(Key.proxied, "") - val clip = ClipData.newPlainText(Key.proxied, bypass + " " + proxiedAppString) + val clip = ClipData.newPlainText(Key.proxied, bypass + "\n" + proxiedAppString) clipboard.setPrimaryClip(clip) Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show() return true @@ -169,11 +169,12 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC if (proxiedAppSequence != null) { val proxiedAppString = proxiedAppSequence.toString if (!proxiedAppString.isEmpty) { - val array = proxiedAppString.split(" ") - val bypass = array(0).toBoolean - val apps = if (array.size > 1) array(1) else "" - prefs.edit.putBoolean(Key.isBypassApps, bypass).apply() - prefs.edit.putString(Key.proxied, apps).apply() + val editor = prefs.edit + val i = proxiedAppString.indexOf('\n') + if (i < 0) + editor.putBoolean(Key.isBypassApps, proxiedAppString.toBoolean).putString(Key.proxied, "").apply() + else editor.putBoolean(Key.isBypassApps, proxiedAppString.substring(0, i).toBoolean) + .putString(Key.proxied, proxiedAppString.substring(i + 1)).apply() Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show() // Restart activity appListView.setVisibility(View.GONE) @@ -258,7 +259,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC } def saveAppSettings(context: Context) { - if (!appsLoading) ShadowsocksApplication.settings.edit.putString(Key.proxied, proxiedApps.mkString("|")).apply + if (!appsLoading) ShadowsocksApplication.settings.edit.putString(Key.proxied, proxiedApps.mkString("\n")).apply } var handler: Handler = null diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala index 881c999e..a301deed 100644 --- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala +++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala @@ -218,18 +218,21 @@ class Shadowsocks ShadowsocksApplication.settings.edit.putBoolean(ShadowsocksApplication.getVersionName, true).apply() try { // Workaround that convert port(String) to port(Int) - val oldLocalPort = ShadowsocksApplication.settings.getString("port", "-1") - val oldRemotePort = ShadowsocksApplication.settings.getString("remotePort", "-1") + val oldLocalPort = ShadowsocksApplication.settings.getString(Key.localPort, "") + val oldRemotePort = ShadowsocksApplication.settings.getString(Key.remotePort, "") - if (oldLocalPort != "-1") { - ShadowsocksApplication.settings.edit.putInt(Key.localPort, oldLocalPort.toInt).commit() + if (oldLocalPort != "") { + ShadowsocksApplication.settings.edit.putInt(Key.localPort, oldLocalPort.toInt).apply() } - if (oldRemotePort != "-1") { - ShadowsocksApplication.settings.edit.putInt(Key.remotePort, oldRemotePort.toInt).commit() + if (oldRemotePort != "") { + ShadowsocksApplication.settings.edit.putInt(Key.remotePort, oldRemotePort.toInt).apply() } } catch { case ex: Exception => // Ignore } + val oldProxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "") + if (oldProxiedApps.contains('|')) ShadowsocksApplication.settings.edit + .putString(Key.proxied, DBHelper.updateProxiedApps(this, oldProxiedApps)).apply() recovery() } diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index 26374ffc..47f91d78 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -374,12 +374,16 @@ class ShadowsocksNatService extends BaseService { http_sb.append(Utils.getIptables + CMD_IPTABLES_DNAT_ADD_SOCKS) } if (config.isProxyApps) { - for (uid <- config.proxiedAppString.split('|').distinct) { - if (!config.isBypassApps) { - http_sb.append((Utils.getIptables + CMD_IPTABLES_DNAT_ADD_SOCKS).replace("-t nat", "-t nat -m owner --uid-owner " + uid)) - } else { - init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-m owner --uid-owner " + uid)) - } + val uidMap = getPackageManager.getInstalledApplications(0).map(ai => ai.packageName -> ai.uid).toMap + for (pn <- config.proxiedAppString.split('\n')) uidMap.get(pn) match { + case Some(uid) => + if (!config.isBypassApps) { + http_sb.append((Utils.getIptables + CMD_IPTABLES_DNAT_ADD_SOCKS) + .replace("-t nat", "-t nat -m owner --uid-owner " + uid)) + } else { + init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-m owner --uid-owner " + uid)) + } + case _ => // probably removed package, ignore } } Console.runRootCommand((init_sb ++ http_sb).toArray) @@ -392,7 +396,7 @@ class ShadowsocksNatService extends BaseService { changeState(State.STOPPED, getString(R.string.nat_no_root)) return } - + super.startRunner(config) // register close receiver diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index ac832422..849af214 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -388,7 +388,7 @@ class ShadowsocksVpnService extends VpnService with BaseService { if (Utils.isLollipopOrAbove) { if (config.isProxyApps) { - for (pkg <- config.proxiedAppString.split('|').distinct) { + for (pkg <- config.proxiedAppString.split('\n')) { if (!config.isBypassApps) { builder.addAllowedApplication(pkg) } else { diff --git a/src/main/scala/com/github/shadowsocks/database/DBHelper.scala b/src/main/scala/com/github/shadowsocks/database/DBHelper.scala index ae685307..c5c55df1 100644 --- a/src/main/scala/com/github/shadowsocks/database/DBHelper.scala +++ b/src/main/scala/com/github/shadowsocks/database/DBHelper.scala @@ -40,18 +40,30 @@ package com.github.shadowsocks.database import android.content.Context +import android.content.pm.ApplicationInfo import android.database.sqlite.SQLiteDatabase import com.j256.ormlite.android.apptools.OrmLiteSqliteOpenHelper import com.j256.ormlite.dao.Dao import com.j256.ormlite.support.ConnectionSource import com.j256.ormlite.table.TableUtils +import scala.collection.JavaConverters._ +import scala.collection.mutable + object DBHelper { - val PROFILE = "profile.db" + final val PROFILE = "profile.db" + private var apps: mutable.Buffer[ApplicationInfo] = _ + + def updateProxiedApps(context: Context, old: String) = { + synchronized(if (apps == null) apps = context.getPackageManager.getInstalledApplications(0).asScala) + val uidSet = old.split('|').map(_.toInt).toSet + apps.filter(ai => uidSet.contains(ai.uid)).map(_.packageName).mkString("\n") + } } class DBHelper(val context: Context) - extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 13) { + extends OrmLiteSqliteOpenHelper(context, DBHelper.PROFILE, null, 14) { + import DBHelper._ lazy val profileDao: Dao[Profile, Int] = getDao(classOf[Profile]) @@ -91,13 +103,17 @@ class DBHelper(val context: Context) " ipv6, individual FROM `tmp`;") profileDao.executeRawNoArgs("DROP TABLE `tmp`;") profileDao.executeRawNoArgs("COMMIT;") - return - } - if (oldVersion < 13) { + } else if (oldVersion < 13) { profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN tx LONG;") profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN rx LONG;") profileDao.executeRawNoArgs("ALTER TABLE `profile` ADD COLUMN date VARCHAR;") } + if (oldVersion < 14) { + for (profile <- profileDao.queryForAll.asScala) { + profile.individual = updateProxiedApps(context, profile.individual) + profileDao.update(profile) + } + } } } } From 3f93ad079bee4f2d2432f8ff90ab9927be0131ef Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 6 Dec 2015 21:34:50 +0800 Subject: [PATCH 3/4] Sort packages with case ignored --- src/main/scala/com/github/shadowsocks/AppManager.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/com/github/shadowsocks/AppManager.scala b/src/main/scala/com/github/shadowsocks/AppManager.scala index 0d9dbab8..d5b0c83c 100644 --- a/src/main/scala/com/github/shadowsocks/AppManager.scala +++ b/src/main/scala/com/github/shadowsocks/AppManager.scala @@ -90,7 +90,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC proxiedApps = ShadowsocksApplication.settings.getString(Key.proxied, "").split('\n').to[mutable.HashSet] apps = getApps(getPackageManager).sortWith((a, b) => { val aProxied = proxiedApps.contains(a.packageName) - if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name < b.name + if (aProxied ^ proxiedApps.contains(b.packageName)) aProxied else a.name.compareToIgnoreCase(b.name) < 0 }) adapter = new ArrayAdapter[ProxiedApp](this, R.layout.layout_apps_item, R.id.itemtext, apps) { override def getView(position: Int, view: View, parent: ViewGroup): View = { From 4312e1676d0a2be74a41625781a15db6eaf9cf30 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sun, 6 Dec 2015 21:38:48 +0800 Subject: [PATCH 4/4] Fix label --- src/main/scala/com/github/shadowsocks/AppManager.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/com/github/shadowsocks/AppManager.scala b/src/main/scala/com/github/shadowsocks/AppManager.scala index d5b0c83c..05697a64 100644 --- a/src/main/scala/com/github/shadowsocks/AppManager.scala +++ b/src/main/scala/com/github/shadowsocks/AppManager.scala @@ -233,7 +233,7 @@ class AppManager extends AppCompatActivity with OnCheckedChangeListener with OnC totalItemCount: Int) { if (visible) { val name = apps(firstVisibleItem).name - overlay.setText(if (name != null && name.length > 1) name(0).toString else "*") + overlay.setText(if (name != null && name.length > 1) name(0).toUpper.toString else "*") overlay.setVisibility(View.VISIBLE) } }