Compare commits

...
18 Commits
Author SHA1 Message Date
Max Lv d0117daf6f Fix #759 #760 2016-07-14 08:58:36 +08:00
Mygod b14ff7082d Remove camera requirement 2016-07-13 11:38:23 +08:00
Max Lv 0e4cfb7a01 Bump version 2016-07-13 09:55:14 +08:00
Max Lv 73996e2ff7 Fix #722 2016-07-13 09:54:40 +08:00
Mygod 76f7306a93 Use Shell.Interactive interactively and removes iptables submodule
The following action will take only one SU request with this commit:

* Start service in NAT mode;
* Reset in NAT mode;
* Flush DNS.

And stopping service in NAT mode doesn't need SU request any more.
2016-07-13 09:42:18 +08:00
Mygod 09220ee335 Reduce notification priority 2016-07-13 00:45:19 +08:00
Mygod 6c992c5937 Refine proguard rules and flushing DNS on Android 4.x
This reverts commit 7a87eab47c.
2016-07-12 18:46:31 +08:00
Mygod 3010ed7a8b Fix incremental builds 2016-07-12 17:13:52 +08:00
Mygod 4fc56e2fea Update dependency 2016-07-12 17:12:05 +08:00
Mygod c92d66d9f3 Revert "Refine #530 again and again" and probably fix #530
This reverts commit b833cb6267.
2016-07-12 15:56:32 +08:00
Max Lv b833cb6267 Refine #530 again and again
Put ShadowsocksRunnerService into :bg process. It helped to solve #530, but
still I'm not sure why it works.

One assumption is that the startService() in BootReceiver helps to ensure :bg
process in a valid state.
2016-07-12 13:36:23 +08:00
Max Lv 9c37f8cae6 Fix #754 2016-07-12 12:17:36 +08:00
Mygod 16f4572839 Use official icons 2016-07-11 22:29:22 +08:00
Mygod 44625ae5b0 Bugfix for DropDownPreference 2016-07-11 20:43:53 +08:00
Mygod 159e1dff78 Bugfix for NAT mode 2016-07-11 19:51:47 +08:00
Mygod 84ac2b06fa Refine #530 again
Have. To. Do. More. Refinements.

Here's what happened in the buggy versions: (I think)

* `BootReceiver` starts which invokes `Utils.startSsService` which starts `ShadowsocksRunnerService`;
* `ShadowsocksRunnerService` binds to `BaseService`;
* 1s after it's connected, it invokes `bgService.use` which returns immediately after pushing `use` in the queue since it's an oneway method;
* Then `stopSelf` => `detachService`, `BaseService` now have nothing bound to it and not started either (neither foreground nor background), so the system can destroy it any time; (at this point, `BaseService.use` isn't invoked yet)
* `com.github.shadowsocks` process then probably exits since booting is a resource-heavy task, anyway it doesn't matter;
* Some time later `com.github.shadowsocks:bg` resumes running, and `BaseService` is stopped and destroyed. Now the system is no longer aware of this `BaseService` instance but it isn't garbage collected yet;
* `use` is finally invoked;
* `startService` is invoked in `startRunner` and the destroyed background service starts running; (bug of Android?)
* `startForeground` is invoked in `ShadowsocksNotification` but the system probably refused it since the service itself is already destroyed;
* When the main activity UI is brought up and requests binding, `com.github.shadowsocks:bg` which is unaware of the existing instance of `BaseService` creates another `BaseService` and thus everything gets messed up.

This commit fixes this by invoking `getState` (which isn't an oneway method) immediately after `use` so that `ShadowsocksRunnerService` will wait for `BaseService` to start itself before destroying itself and its connection to `BaseService`. And thus its return value is not used.
2016-07-11 19:22:54 +08:00
Mygod dcf190c1f5 Bugfix for NumberPickerPreference 2016-07-11 17:51:24 +08:00
Mygod 74a0e397c1 Refine #752 and NumberPickerPreference 2016-07-11 17:49:17 +08:00
37 changed files with 152 additions and 390 deletions
-4
View File
@@ -5,10 +5,6 @@
path = src/main/jni/badvpn
url = https://github.com/shadowsocks/badvpn.git
branch = shadowsocks-android
[submodule "src/main/jni/iptables"]
path = src/main/jni/iptables
url = https://github.com/shadowsocks/iptables.git
branch = shadowsocks-android
[submodule "src/main/jni/libancillary"]
path = src/main/jni/libancillary
url = https://github.com/shadowsocks/libancillary.git
+1 -1
View File
@@ -13,7 +13,7 @@ A [shadowsocks](http://shadowsocks.org) client for Android, written in Scala.
* JDK 1.8
* SBT 0.13.0+
* Android SDK r25+
* Android NDK r10d+
* Android NDK r12b+
### BUILD
+10 -9
View File
@@ -33,7 +33,7 @@ useSupportVectors
libraryDependencies ++= Seq(
"dnsjava" % "dnsjava" % "2.1.7",
"com.github.kevinsawicki" % "http-request" % "6.0",
"eu.chainfire" % "libsuperuser" % "1.0.0.201602271131",
"eu.chainfire" % "libsuperuser" % "1.0.0.201607041850",
"net.glxn.qrgen" % "android" % "2.0",
"com.google.android.gms" % "play-services-ads" % "9.2.0",
"com.google.android.gms" % "play-services-analytics" % "9.2.0",
@@ -49,14 +49,15 @@ libraryDependencies ++= Seq(
proguardVersion in Android := "5.2.1"
proguardOptions in Android ++= Seq("-keep class com.github.shadowsocks.**",
"-keep class com.github.shadowsocks.System { *; }",
"-keep class me.dm7.**",
"-keep class com.google.android.gms.**",
"-keepattributes *Annotation*",
"-dontnote com.j256.ormlite.**",
"-dontnote org.xbill.**",
"-dontwarn org.xbill.**")
proguardCache in Android := Seq()
proguardOptions in Android ++= Seq(
"-keep class com.github.shadowsocks.System { *; }",
"-dontnote com.google.android.gms.internal.**",
"-dontnote com.j256.ormlite.**",
"-dontnote me.dm7.barcodescannerview.**",
"-dontnote org.xbill.**",
"-dontwarn org.xbill.**")
lazy val nativeBuild = TaskKey[Unit]("native-build", "Build native executables")
+1 -1
View File
@@ -1,4 +1,4 @@
addSbtPlugin("org.scala-android" % "sbt-android" % "1.6.6")
addSbtPlugin("org.scala-android" % "sbt-android" % "1.6.7")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")
+9 -8
View File
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" package="com.github.shadowsocks"
android:versionCode="139"
android:versionName="2.10.6">
android:versionCode="140"
android:versionName="2.10.7">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
@@ -9,13 +9,14 @@
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen"
android:required="false"/>
<uses-feature android:name="android.hardware.camera"
android:required="false"/>
<uses-feature android:name="android.hardware.nfc"
android:required="false"/>
<uses-feature android:name="android.software.leanback"
android:required="false" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />
android:required="false"/>
<uses-sdk
android:minSdkVersion="16"
@@ -9,4 +9,5 @@ interface IShadowsocksService {
oneway void unregisterCallback(IShadowsocksServiceCallback cb);
oneway void use(in int profileId);
void useSync(in int profileId);
}
-9
View File
@@ -552,14 +552,5 @@ openssl_subdirs := $(addprefix $(LOCAL_PATH)/openssl/,$(addsuffix /Android.mk, \
))
include $(openssl_subdirs)
# Iptables
# LOCAL_PATH := $(ROOT_PATH)
# iptables_subdirs := $(addprefix $(LOCAL_PATH)/iptables/,$(addsuffix /Android.mk, \
# iptables \
# extensions \
# libiptc \
# ))
# include $(iptables_subdirs)
# Import cpufeatures
$(call import-module,android/cpufeatures)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 610 B

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 B

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 B

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 B

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 B

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 B

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 950 B

After

Width:  |  Height:  |  Size: 843 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 B

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

-4
View File
@@ -13,10 +13,6 @@
<string name="stat_summary">Отправлено: \t\t\t\t\t%3$s\t↑\t%1$s/s\nПолучено: \t%4$s\t↓\t%2$s/s</string>
<string name="stat_profiles">\n%1$s↑\t%2$s↓</string>
<string name="traffic_summary">%1$s/s↑\t%2$s/s↓</string>
<plurals name="bytes">
<item quantity="one">Byte</item>
<item quantity="other">Bytes</item>
</plurals>
<string name="connection_test_pending">Проверить подключение</string>
<string name="connection_test_testing">Проверка…</string>
<string name="connection_test_available">Успешно: %dms задержка</string>
-3
View File
@@ -82,9 +82,6 @@
<string name="stat">网络流量</string>
<string name="stat_summary">发送: \t%3$s\t↑\t%1$s/s\n接收: \t%4$s\t↓\t%2$s/s</string>
<plurals name="bytes">
<item quantity="other">字节</item>
</plurals>
<string name="connection_test_pending">检查网络连接</string>
<string name="connection_test_testing">测试中……</string>
-3
View File
@@ -83,9 +83,6 @@
<string name="stat">網路流量</string>
<string name="stat_summary">傳送: \t%3$s\t↑\t%1$s/s\n接收: \t%4$s\t↓\t%2$s/s</string>
<plurals name="bytes">
<item quantity="other">位元組</item>
</plurals>
<string name="connection_test_pending">檢查連線能力</string>
<string name="connection_test_testing">測試中……</string>
+2 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="DropDownPreference">
<attr name="entries" format="reference" />
<attr name="entryValues" format="reference" />
<attr name="android:entries" />
<attr name="android:entryValues" />
</declare-styleable>
<declare-styleable name="NumberPickerPreference">
<attr name="max" format="integer" />
+1 -1
View File
@@ -14,7 +14,7 @@
<string name="stat_summary">Sent: \t\t\t\t\t%3$s\t↑\t%1$s/s\nReceived: \t%4$s\t↓\t%2$s/s</string>
<string name="stat_profiles">\n%1$s↑\t%2$s↓</string>
<string name="traffic_summary">%1$s/s↑\t%2$s/s↓</string>
<plurals name="bytes">
<plurals name="bytes" translatable="false">
<item quantity="one">Byte</item>
<item quantity="other">Bytes</item>
</plurals>
+4 -4
View File
@@ -37,8 +37,8 @@
<com.github.shadowsocks.preferences.DropDownPreference
android:key="encMethod"
android:persistent="false"
app:entries="@array/enc_method_entry"
app:entryValues="@array/enc_method_value"
android:entries="@array/enc_method_entry"
android:entryValues="@array/enc_method_value"
android:summary="%s"
android:title="@string/enc_method"/>
<SwitchPreference
@@ -55,8 +55,8 @@
<com.github.shadowsocks.preferences.DropDownPreference
android:key="route"
android:persistent="false"
app:entries="@array/route_entry"
app:entryValues="@array/route_value"
android:entries="@array/route_entry"
android:entryValues="@array/route_value"
android:summary="%s"
android:title="@string/route_list"/>
<SwitchPreference
@@ -158,23 +158,22 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
def onMenuItemClick(item: MenuItem): Boolean = {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
val prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext)
item.getItemId match {
case R.id.action_apply_all =>
app.profileManager.getAllProfiles match {
case Some(profiles) =>
val proxiedAppString = prefs.getString(Key.individual, "")
profiles.foreach(profile => {
profile.individual = proxiedAppString
app.profileManager.updateProfile(profile)
val proxiedAppString = profile.individual
profiles.foreach(p => {
p.individual = proxiedAppString
app.profileManager.updateProfile(p)
})
Toast.makeText(this, R.string.action_apply_all, Toast.LENGTH_SHORT).show
case _ => Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show
}
return true
case R.id.action_export =>
val bypass = prefs.getBoolean(Key.bypass, false)
val proxiedAppString = prefs.getString(Key.individual, "")
val bypass = profile.bypass
val proxiedAppString = profile.individual
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
clipboard.setPrimaryClip(clip)
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
@@ -185,13 +184,13 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
if (proxiedAppSequence != null) {
val proxiedAppString = proxiedAppSequence.toString
if (!proxiedAppString.isEmpty) {
val editor = prefs.edit
val i = proxiedAppString.indexOf('\n')
try {
val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.setChecked(enabled.toBoolean)
editor.putString(Key.individual, apps).apply()
profile.individual = apps
app.profileManager.updateProfile(profile)
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE)
@@ -114,6 +114,8 @@ trait BaseService extends Service {
case _ => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + state)
}
})
override def useSync(profileId: Int) = use(profileId)
}
def checkProfile(profile: Profile) = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
@@ -140,9 +142,7 @@ trait BaseService extends Service {
profile.password = proxy(2).trim
profile.method = proxy(3).trim
} catch {
case ex: Exception =>
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner(true)
case ex: Exception => stopRunner(true, getString(R.string.service_failed))
}
def startRunner(profile: Profile) {
@@ -169,7 +169,7 @@ trait BaseService extends Service {
Utils.ThrowableFuture(connect)
}
def stopRunner(stopService: Boolean) {
def stopRunner(stopService: Boolean, msg: String = null) {
// clean up recevier
if (closeReceiverRegistered) {
unregisterReceiver(closeReceiver)
@@ -186,7 +186,7 @@ trait BaseService extends Service {
}
// change the state
changeState(State.STOPPED)
changeState(State.STOPPED, msg)
// stop the service if nothing has bound to it
if (stopService) stopSelf()
@@ -242,7 +242,7 @@ trait BaseService extends Service {
protected def changeState(s: Int, msg: String = null) {
val handler = new Handler(getMainLooper)
handler.post(() => if (state != s) {
handler.post(() => if (state != s || msg != null) {
if (callbacksCount > 0) {
val n = callbacks.beginBroadcast()
for (i <- 0 until n) {
@@ -4,6 +4,7 @@ import java.nio.charset.Charset
import android.app.Activity
import android.content._
import android.content.pm.PackageManager
import android.nfc.NfcAdapter.CreateNdefMessageCallback
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter, NfcEvent}
import android.os.{Bundle, Handler}
@@ -263,6 +264,8 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
val importAddFAB = findViewById(R.id.fab_import_add).asInstanceOf[FloatingActionButton]
importAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_content_paste))
importAddFAB.setOnClickListener(this)
menu.setOnMenuToggleListener(opened => if (opened) qrcodeAddFAB.setVisibility(
if (getPackageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)) View.VISIBLE else View.GONE))
}
@@ -1,33 +1,24 @@
package com.github.shadowsocks
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.Manifest
import android.os.Bundle
import android.os.Handler
import android.support.v4.app.ActivityCompat
import android.support.v4.app.NavUtils
import android.support.v4.content.ContextCompat
import android.support.v7.app.ActionBar
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.{MenuItem, ViewGroup}
import android.widget.Toast
import com.google.zxing.Result
import me.dm7.barcodescanner.core.IViewFinder
import me.dm7.barcodescanner.core.ViewFinderView
import me.dm7.barcodescanner.zxing.ZXingScannerView
class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHandler {
val MY_PERMISSIONS_REQUEST_CAMERA = 1
var scannerView: ZXingScannerView = null
var scannerView: ZXingScannerView = _
override def onRequestPermissionsResult(requestCode: Int, permissions: Array[String],
grantResults: Array[Int]) {
@@ -65,6 +65,7 @@ import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils._
import com.github.shadowsocks.ShadowsocksApplication.app
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
import eu.chainfire.libsuperuser.Shell
import scala.collection.mutable.ArrayBuffer
@@ -110,7 +111,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
// Services
private val callback = new IShadowsocksServiceCallback.Stub {
def stateChanged(s: Int, m: String) {
handler.post(() => if (state != s) {
handler.post(() => {
s match {
case State.CONNECTING =>
fab.setBackgroundTintList(greyTint)
@@ -286,34 +287,17 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
val cmd = new ArrayBuffer[String]()
for (task <- Array("ss-local", "ss-tunnel", "pdnsd", "redsocks", "tun2socks")) {
cmd.append("chmod 666 %s/%s-nat.pid".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
cmd.append("chmod 666 %s/%s-vpn.pid".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
}
if (app.isNatEnabled) Console.runRootCommand(cmd.toArray) else Console.runCommand(cmd.toArray)
cmd.clear()
for (task <- Array("ss-local", "ss-tunnel", "pdnsd", "redsocks", "tun2socks")) {
try {
val pid_nat = scala.io.Source.fromFile(getApplicationInfo.dataDir + "/" + task + "-nat.pid").mkString.trim.toInt
val pid_vpn = scala.io.Source.fromFile(getApplicationInfo.dataDir + "/" + task + "-vpn.pid").mkString.trim.toInt
cmd.append("kill -9 %d".formatLocal(Locale.ENGLISH, pid_nat))
cmd.append("kill -9 %d".formatLocal(Locale.ENGLISH, pid_vpn))
Process.killProcess(pid_nat)
Process.killProcess(pid_vpn)
} catch {
case e: Throwable => // Ignore
}
cmd.append("rm -f %s/%s-nat.pid".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
cmd.append("rm -f %s/%s-nat.conf".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
cmd.append("rm -f %s/%s-vpn.pid".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
cmd.append("rm -f %s/%s-vpn.conf".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
cmd.append("killall %s".formatLocal(Locale.ENGLISH, task))
cmd.append("rm -f %1$s/%2$s-nat.conf %1$s/%2$s-vpn.conf"
.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, task))
}
if (app.isNatEnabled) {
Console.runRootCommand(cmd.toArray)
Console.runRootCommand(Utils.iptables + " -t nat -F OUTPUT")
} else Console.runCommand(cmd.toArray)
cmd.append("iptables -t nat -F OUTPUT")
cmd.append("echo done")
val result = Shell.SU.run(cmd.toArray)
if (result != null && !result.isEmpty) return // fallback to SH
}
Shell.SH.run(cmd.toArray)
}
def cancelStart() {
@@ -535,7 +519,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
for (executable <- EXECUTABLES) {
ab.append("chmod 755 " + getApplicationInfo.dataDir + "/" + executable)
}
Console.runCommand(ab.toArray)
Shell.SH.run(ab.toArray)
}
def recovery() {
@@ -45,12 +45,12 @@ import java.net.{Inet6Address, InetAddress}
import java.util.Locale
import android.content._
import android.net.{ConnectivityManager, Network}
import android.os._
import android.util.{Log, SparseArray}
import com.github.shadowsocks.utils._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
import eu.chainfire.libsuperuser.Shell
import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer
@@ -59,8 +59,7 @@ class ShadowsocksNatService extends BaseService {
val TAG = "ShadowsocksNatService"
val CMD_IPTABLES_RETURN = " -t nat -A OUTPUT -p tcp -d 0.0.0.0 -j RETURN"
val CMD_IPTABLES_DNAT_ADD_SOCKS = " -t nat -A OUTPUT -p tcp " +
val CMD_IPTABLES_DNAT_ADD_SOCKS = "iptables -t nat -A OUTPUT -p tcp " +
"-j DNAT --to-destination 127.0.0.1:8123"
private var notification: ShadowsocksNotification = _
@@ -70,78 +69,7 @@ class ShadowsocksNatService extends BaseService {
var sstunnelProcess: Process = _
var redsocksProcess: Process = _
var pdnsdProcess: Process = _
private val dnsAddressCache = new SparseArray[String]
def getNetId(network: Network): Int = {
network.getClass.getDeclaredField("netId").get(network).asInstanceOf[Int]
}
def restoreDnsForAllNetwork() {
val manager = getSystemService(Context.CONNECTIVITY_SERVICE).asInstanceOf[ConnectivityManager]
val networks = manager.getAllNetworks
val cmdBuf = new ArrayBuffer[String]()
networks.foreach(network => {
val netId = getNetId(network)
val oldDns = dnsAddressCache.get(netId)
if (oldDns != null) {
cmdBuf.append("ndc resolver setnetdns %d \"\" %s".formatLocal(Locale.ENGLISH, netId, oldDns))
dnsAddressCache.remove(netId)
}
})
if (cmdBuf.nonEmpty) Console.runRootCommand(cmdBuf.toArray)
}
def setDnsForAllNetwork(dns: String) {
val manager = getSystemService(Context.CONNECTIVITY_SERVICE).asInstanceOf[ConnectivityManager]
val networks = manager.getAllNetworks
if (networks == null) return
val cmdBuf = new ArrayBuffer[String]()
networks.foreach(network => {
val networkInfo = manager.getNetworkInfo(network)
if (networkInfo == null) return
if (networkInfo.isConnected) {
val netId = getNetId(network)
val curDnsList = manager.getLinkProperties(network).getDnsServers
if (curDnsList != null) {
import scala.collection.JavaConverters._
val curDns = curDnsList.asScala.map(ip => ip.getHostAddress).mkString(" ")
if (curDns != dns) {
dnsAddressCache.put(netId, curDns)
cmdBuf.append("ndc resolver setnetdns %d \"\" %s".formatLocal(Locale.ENGLISH, netId, dns))
}
}
}
})
if (cmdBuf.nonEmpty) Console.runRootCommand(cmdBuf.toArray)
}
def setupDns() {
setDnsForAllNetwork("127.0.0.1")
}
def resetDns() = {
restoreDnsForAllNetwork()
}
def flushDns() {
if (Utils.isLollipopOrAbove) {
val manager = getSystemService(Context.CONNECTIVITY_SERVICE).asInstanceOf[ConnectivityManager]
val networks = manager.getAllNetworks
val cmdBuf = new ArrayBuffer[String]()
networks.foreach(network => {
val networkInfo = manager.getNetworkInfo(network)
if (networkInfo.isAvailable) {
val netId = network.getClass.getDeclaredField("netId").get(network).asInstanceOf[Int]
cmdBuf.append("ndc resolver flushnet %d".formatLocal(Locale.ENGLISH, netId))
}
})
Console.runRootCommand(cmdBuf.toArray)
} else {
Console.runRootCommand("ndc resolver flushdefaultif", "ndc resolver flushif wlan0")
}
}
var su: Shell.Interactive = _
def startShadowsocksDaemon() {
if (profile.route != Route.ALL) {
@@ -264,7 +192,7 @@ class ShadowsocksNatService extends BaseService {
}
/** Called when the activity is first created. */
def handleConnection: Boolean = {
def handleConnection {
startTunnel()
if (!profile.udpdns) startDnsDaemon()
@@ -272,7 +200,6 @@ class ShadowsocksNatService extends BaseService {
startShadowsocksDaemon()
setupIptables()
true
}
def onBind(intent: Intent): IBinder = {
@@ -302,7 +229,7 @@ class ShadowsocksNatService extends BaseService {
pdnsdProcess = null
}
Console.runRootCommand(Utils.iptables + " -t nat -F OUTPUT")
su.addCommand("iptables -t nat -F OUTPUT")
}
def setupIptables() = {
@@ -310,9 +237,9 @@ class ShadowsocksNatService extends BaseService {
val http_sb = new ArrayBuffer[String]
init_sb.append("ulimit -n 4096")
init_sb.append(Utils.iptables + " -t nat -F OUTPUT")
init_sb.append("iptables -t nat -F OUTPUT")
val cmd_bypass = Utils.iptables + CMD_IPTABLES_RETURN
val cmd_bypass = "iptables -t nat -A OUTPUT -p tcp -d 0.0.0.0 -j RETURN"
if (!InetAddress.getByName(profile.host.toUpperCase).isInstanceOf[Inet6Address]) {
init_sb.append(cmd_bypass.replace("-p tcp -d 0.0.0.0", "-d " + profile.host))
}
@@ -320,18 +247,17 @@ class ShadowsocksNatService extends BaseService {
init_sb.append(cmd_bypass.replace("-p tcp -d 0.0.0.0", "-m owner --uid-owner " + myUid))
init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "--dport 53"))
init_sb.append(Utils.iptables
+ " -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:8153")
init_sb.append("iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:8153")
if (!profile.proxyApps || profile.bypass) {
http_sb.append(Utils.iptables + CMD_IPTABLES_DNAT_ADD_SOCKS)
http_sb.append(CMD_IPTABLES_DNAT_ADD_SOCKS)
}
if (profile.proxyApps) {
val uidMap = getPackageManager.getInstalledApplications(0).map(ai => ai.packageName -> ai.uid).toMap
for (pn <- profile.individual.split('\n')) uidMap.get(pn) match {
case Some(uid) =>
if (!profile.bypass) {
http_sb.append((Utils.iptables + CMD_IPTABLES_DNAT_ADD_SOCKS)
http_sb.append(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))
@@ -339,31 +265,38 @@ class ShadowsocksNatService extends BaseService {
case _ => // probably removed package, ignore
}
}
Console.runRootCommand((init_sb ++ http_sb).toArray)
su.addCommand((init_sb ++ http_sb).toArray)
}
override def startRunner(profile: Profile) {
if (!Console.isRoot) {
changeState(State.STOPPED, getString(R.string.nat_no_root))
stopRunner(true)
return
override def startRunner(profile: Profile) = if (su == null) {
su = new Shell.Builder().useSU().setWantSTDERR(true).setWatchdogTimeout(10).open((_, exitCode, _) =>
if (exitCode == 0) super.startRunner(profile) else {
Log.wtf(TAG, "libsuperuser#55 has been fixed. Please remove the redundant code.")
su.close()
su = null
super.stopRunner(true, getString(R.string.nat_no_root))
})
su.waitForIdle()
if (!su.isRunning) {
su.close()
su = null
super.stopRunner(true, getString(R.string.nat_no_root))
}
super.startRunner(profile)
}
override def connect() {
super.connect()
if (this.profile != null) {
if (profile != null) {
// Clean up
killProcesses()
var resolved: Boolean = false
if (!Utils.isNumeric(this.profile.host)) {
Utils.resolve(this.profile.host, enableIPv6 = true) match {
var resolved = false
if (!Utils.isNumeric(profile.host)) {
Utils.resolve(profile.host, enableIPv6 = true) match {
case Some(a) =>
this.profile.host = a
profile.host = a
resolved = true
case None => resolved = false
}
@@ -371,33 +304,31 @@ class ShadowsocksNatService extends BaseService {
resolved = true
}
if (!resolved) {
changeState(State.STOPPED, getString(R.string.invalid_server))
stopRunner(true)
} else if (handleConnection) {
if (!resolved) stopRunner(true, getString(R.string.invalid_server)) else {
handleConnection
// Set DNS
flushDns()
su.addCommand(Utils.FLUSH_DNS)
changeState(State.CONNECTED)
notification = new ShadowsocksNotification(this, profile.name, true)
} else {
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner(true)
}
}
}
override def stopRunner(stopService: Boolean) {
override def stopRunner(stopService: Boolean, msg: String = null) {
if (notification != null) notification.destroy()
// channge the state
changeState(State.STOPPING)
if (notification != null) notification.destroy()
app.track(TAG, "stop")
// reset NAT
killProcesses()
super.stopRunner(stopService)
su.close()
su = null
super.stopRunner(stopService, msg)
}
}
@@ -79,13 +79,11 @@ class ShadowsocksNotification(private val service: BaseService, profileName: Str
def setVisible(visible: Boolean, forceShow: Boolean = false) = if (isVisible != visible) {
isVisible = visible
builder.setPriority(if (visible) NotificationCompat.PRIORITY_DEFAULT else NotificationCompat.PRIORITY_MIN)
builder.setPriority(if (visible) NotificationCompat.PRIORITY_LOW else NotificationCompat.PRIORITY_MIN)
show()
} else if (forceShow) show()
def show() = {
service.startForeground(1, builder.build)
}
def show() = service.startForeground(1, builder.build)
def destroy() {
if (lockReceiver != null) {
@@ -43,7 +43,6 @@ import android.app.Service
import android.content.Intent
import android.net.VpnService
import android.os.{IBinder, Handler}
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.ConfigUtils
import com.github.shadowsocks.ShadowsocksApplication.app
@@ -55,29 +54,18 @@ class ShadowsocksRunnerService extends Service with ServiceBoundContext {
}
override def onServiceConnected() {
handler.postDelayed(() => if (bgService != null) startBackgroundService(), 1000)
handler.postDelayed(() => if (bgService != null) {
if (app.isNatEnabled) startBackgroundService()
else if (VpnService.prepare(ShadowsocksRunnerService.this) == null) startBackgroundService()
stopSelf()
}, 1000)
}
def startBackgroundService() {
if (app.isNatEnabled) bgService.use(app.profileId) else {
val intent = VpnService.prepare(ShadowsocksRunnerService.this)
if (intent == null) {
if (bgService != null) {
bgService.use(app.profileId)
}
}
}
stopSelf()
}
def startBackgroundService() = bgService.useSync(app.profileId)
override def onCreate() {
super.onCreate()
attachService()
val s = if (app.isNatEnabled) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService]
val intent = new Intent(this, s)
intent.setAction(Action.SERVICE)
startService(intent)
}
override def onDestroy() {
@@ -82,7 +82,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
stopRunner(true)
}
override def stopRunner(stopService: Boolean) {
override def stopRunner(stopService: Boolean, msg: String = null) {
if (vpnThread != null) {
vpnThread.stopThread()
@@ -105,7 +105,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
conn = null
}
super.stopRunner(stopService)
super.stopRunner(stopService, msg)
}
def killProcesses() {
@@ -165,10 +165,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
resolved = true
}
if (!resolved) {
changeState(State.STOPPED, getString(R.string.invalid_server))
stopRunner(true)
} else if (handleConnection) {
if (!resolved) stopRunner(true, getString(R.string.invalid_server)) else if (handleConnection) {
changeState(State.CONNECTED)
notification = new ShadowsocksNotification(this, profile.name)
} else {
@@ -5,7 +5,7 @@ import android.content.Context
import android.content.res.TypedArray
import android.os.Build
import android.preference.Preference
import android.support.annotation.NonNull
import android.support.annotation.{ArrayRes, NonNull}
import android.support.v7.widget.AppCompatSpinner
import android.util.AttributeSet
import android.view.{View, ViewGroup}
@@ -23,14 +23,16 @@ final class DropDownPreference(private val mContext: Context, attrs: AttributeSe
private val mSpinner = new AppCompatSpinner(mContext)
private var mEntries: Array[CharSequence] = _
private var mEntryValues: Array[CharSequence] = _
private var mSelectedIndex: Int = _
private var mValueSet: Boolean = _
private var mSelectedIndex: Int = -1
mSpinner.setVisibility(View.INVISIBLE)
mSpinner.setAdapter(mAdapter)
mSpinner.setOnItemSelectedListener(new OnItemSelectedListener {
override def onNothingSelected(parent: AdapterView[_]) { }
override def onItemSelected(parent: AdapterView[_], view: View, position: Int, id: Long) = setValueIndex(position)
def onNothingSelected(parent: AdapterView[_]) { }
def onItemSelected(parent: AdapterView[_], view: View, position: Int, id: Long) = {
val value = getValue(position)
if (position != mSelectedIndex && callChangeListener(value)) setValue(position, value)
}
})
setOnPreferenceClickListener((preference: Preference) => {
// TODO: not working with scrolling
@@ -39,8 +41,8 @@ final class DropDownPreference(private val mContext: Context, attrs: AttributeSe
true
})
val a: TypedArray = mContext.obtainStyledAttributes(attrs, R.styleable.DropDownPreference)
setEntries(a.getTextArray(R.styleable.DropDownPreference_entries))
mEntryValues = a.getTextArray(R.styleable.DropDownPreference_entryValues)
setEntries(a.getTextArray(R.styleable.DropDownPreference_android_entries))
setEntryValues(a.getTextArray(R.styleable.DropDownPreference_android_entryValues))
a.recycle
protected override def getSummaryValue = getEntry
@@ -62,7 +64,7 @@ final class DropDownPreference(private val mContext: Context, attrs: AttributeSe
* @see #setEntries(CharSequence[])
* @param entriesResId The entries array as a resource.
*/
def setEntries(entriesResId: Int): Unit = setEntries(getContext.getResources.getTextArray(entriesResId))
def setEntries(@ArrayRes entriesResId: Int): Unit = setEntries(getContext.getResources.getTextArray(entriesResId))
/**
* The list of entries to be shown in the list in subsequent dialogs.
*
@@ -82,7 +84,7 @@ final class DropDownPreference(private val mContext: Context, attrs: AttributeSe
* @see #setEntryValues(CharSequence[])
* @param entryValuesResId The entry values array as a resource.
*/
def setEntryValues(entryValuesResId: Int): Unit =
def setEntryValues(@ArrayRes entryValuesResId: Int): Unit =
setEntryValues(getContext.getResources.getTextArray(entryValuesResId))
/**
* Returns the array of values to be saved for the preference.
@@ -91,40 +93,26 @@ final class DropDownPreference(private val mContext: Context, attrs: AttributeSe
*/
def getEntryValues: Array[CharSequence] = mEntryValues
private def getValue(index: Int) = (if (mEntryValues == null) index else mEntryValues(index)).toString
/**
* Sets the value of the key. This should be one of the entries in [[getEntryValues]].
*
* @param value The value to set for the key.
*/
def setValue(value: String) {
val i = findIndexOfValue(value)
if (i > -1) setValueIndex(i)
}
def setValue(value: String): Unit = setValue(findIndexOfValue(value), value)
/**
* Sets the value to the given index from the entry values.
*
* Remember to callChangeListener() if this extends Preference directly
*
* https://github.com/android/platform_frameworks_base/blob/
* 4535e11fb7010f2b104d3f8b3954407b9f330e0f/core/java/
* android/preference/ListPreference.java#L282
*
* @param index The index of the value to set.
*/
def setValueIndex(index: Int) {
val changed = mSelectedIndex != index
if (changed || !mValueSet) {
mSelectedIndex = index
mValueSet = true
if (mEntryValues != null) {
val value = mEntryValues(index).toString
if (changed) callChangeListener(value)
persistString(value)
}
if (changed) notifyChanged
}
def setValueIndex(index: Int) = setValue(index, getValue(index))
private def setValue(index: Int, value: String) {
persistString(value)
mSelectedIndex = index
mSpinner.setSelection(index)
notifyChanged
}
/**
@@ -132,7 +120,7 @@ final class DropDownPreference(private val mContext: Context, attrs: AttributeSe
*
* @return The value of the key.
*/
def getValue = if (mEntryValues == null) null else mEntryValues(mSelectedIndex).toString
def getValue = if (mEntryValues == null || mSelectedIndex < 0) null else mEntryValues(mSelectedIndex).toString
/**
* Returns the entry corresponding to the current value.
@@ -28,7 +28,7 @@ final class NumberPickerPreference(context: Context, attrs: AttributeSet = null)
def getMin = if (picker == null) 0 else picker.getMinValue
def getMax = picker.getMaxValue
def setValue(i: Int) {
if (i == getValue || !callChangeListener(i)) return
if (i == getValue) return
picker.setValue(i)
value = picker.getValue
persistInt(value)
@@ -49,7 +49,14 @@ final class NumberPickerPreference(context: Context, attrs: AttributeSet = null)
override protected def onDialogClosed(positiveResult: Boolean) {
picker.clearFocus // commit changes
super.onDialogClosed(positiveResult) // forward compatibility
if (positiveResult) setValue(picker.getValue) else picker.setValue(value)
if (positiveResult) {
val value = picker.getValue
if (callChangeListener(value)) {
setValue(value)
return
}
}
picker.setValue(value)
}
override protected def onGetDefaultValue(a: TypedArray, index: Int) = a.getInt(index, getMin).asInstanceOf[AnyRef]
override protected def onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any) {
@@ -1,97 +0,0 @@
/*
* Shadowsocks - A shadowsocks client for Android
* Copyright (C) 2014 <max.c.lv@gmail.com>
*
* 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/>.
*
*
* ___====-_ _-====___
* _--^^^#####// \\#####^^^--_
* _-^##########// ( ) \\##########^-_
* -############// |\^^/| \\############-
* _/############// (@::@) \\############\_
* /#############(( \\// ))#############\
* -###############\\ (oo) //###############-
* -#################\\ / VV \ //#################-
* -###################\\/ \//###################-
* _#/|##########/\######( /\ )######/\##########|\#_
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
* ` ` ` ` / | | | | \ ' ' ' '
* ( | | | | )
* __\ | | | | /__
* (vvv(VVV)(VVV)vvv)
*
* HERE BE DRAGONS
*
*/
package com.github.shadowsocks.utils
import java.util
import eu.chainfire.libsuperuser.Shell
import eu.chainfire.libsuperuser.Shell.{Builder, SU}
object Console {
private def openShell(): Shell.Interactive = {
val builder = new Builder()
builder
.useSH()
.setWatchdogTimeout(10)
.open()
}
private def openRootShell(context: String): Shell.Interactive = {
val builder = new Builder()
builder
.setShell(SU.shell(0, context))
.setWantSTDERR(true)
.setWatchdogTimeout(10)
.open()
}
def runCommand(commands: Array[String]) {
val shell = openShell()
shell.addCommand(commands, 0, ((commandCode: Int, exitCode: Int, output: util.List[String]) =>
if (exitCode < 0) shell.close()): Shell.OnCommandResultListener)
shell.waitForIdle()
shell.close()
}
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, ((_, exitCode, output) => {
if (exitCode < 0) {
shell.close()
} else {
import scala.collection.JavaConversions._
output.foreach(line => sb.append(line).append('\n'))
}
}): Shell.OnCommandResultListener)
if (shell.waitForIdle()) {
shell.close()
sb.toString()
}
else {
shell.close()
null
}
}
def isRoot: Boolean = SU.available()
}
@@ -53,6 +53,7 @@ import android.view.{Gravity, View, Window}
import android.widget.Toast
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService}
import eu.chainfire.libsuperuser.Shell
import org.xbill.DNS._
import scala.concurrent.ExecutionContext.Implicits.global
@@ -62,18 +63,6 @@ import scala.util.{Failure, Try}
object Utils {
private val TAG = "Shadowsocks"
/**
* Check the system's iptables
* Default to use the app's iptables
*/
private val DEFAULT_IPTABLES = "/system/bin/iptables"
private val ALTERNATIVE_IPTABLES = "iptables"
lazy val iptables = if (Console.isRoot) {
val lines = Console.runRootCommand(DEFAULT_IPTABLES + " --version", DEFAULT_IPTABLES + " -L -t nat -n")
if (Console.isRoot && lines != null && !lines.contains("OUTPUT") || !lines.contains("v1.4.")) ALTERNATIVE_IPTABLES
else DEFAULT_IPTABLES
} else DEFAULT_IPTABLES
def isLollipopOrAbove: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
def getSignature(context: Context): String = {
@@ -129,12 +118,17 @@ object Utils {
}
}
// Because /sys/class/net/* isn't accessible since API level 24
final val FLUSH_DNS = "for if in /sys/class/net/*; do " +
"if [ \"down\" != $(cat $if/operstate) ]; then " + // up or unknown
"ndc resolver flushif ${if##*/}; " +
"fi " +
"done; echo done"
// Blocked > 3 seconds
def toggleAirplaneMode(context: Context) = {
if (Console.isRoot) {
Console.runRootCommand("ndc resolver flushdefaultif", "ndc resolver flushif wlan0")
true
} else if (Build.VERSION.SDK_INT < 17) {
val result = Shell.SU.run(FLUSH_DNS)
if (result != null && !result.isEmpty) true else if (Build.VERSION.SDK_INT < 17) {
toggleBelowApiLevel17(context)
true
} else false