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 path = src/main/jni/badvpn
url = https://github.com/shadowsocks/badvpn.git url = https://github.com/shadowsocks/badvpn.git
branch = shadowsocks-android 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"] [submodule "src/main/jni/libancillary"]
path = src/main/jni/libancillary path = src/main/jni/libancillary
url = https://github.com/shadowsocks/libancillary.git 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 * JDK 1.8
* SBT 0.13.0+ * SBT 0.13.0+
* Android SDK r25+ * Android SDK r25+
* Android NDK r10d+ * Android NDK r12b+
### BUILD ### BUILD
+10 -9
View File
@@ -33,7 +33,7 @@ useSupportVectors
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"dnsjava" % "dnsjava" % "2.1.7", "dnsjava" % "dnsjava" % "2.1.7",
"com.github.kevinsawicki" % "http-request" % "6.0", "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", "net.glxn.qrgen" % "android" % "2.0",
"com.google.android.gms" % "play-services-ads" % "9.2.0", "com.google.android.gms" % "play-services-ads" % "9.2.0",
"com.google.android.gms" % "play-services-analytics" % "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" proguardVersion in Android := "5.2.1"
proguardOptions in Android ++= Seq("-keep class com.github.shadowsocks.**", proguardCache in Android := Seq()
"-keep class com.github.shadowsocks.System { *; }",
"-keep class me.dm7.**", proguardOptions in Android ++= Seq(
"-keep class com.google.android.gms.**", "-keep class com.github.shadowsocks.System { *; }",
"-keepattributes *Annotation*", "-dontnote com.google.android.gms.internal.**",
"-dontnote com.j256.ormlite.**", "-dontnote com.j256.ormlite.**",
"-dontnote org.xbill.**", "-dontnote me.dm7.barcodescannerview.**",
"-dontwarn org.xbill.**") "-dontnote org.xbill.**",
"-dontwarn org.xbill.**")
lazy val nativeBuild = TaskKey[Unit]("native-build", "Build native executables") 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") 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" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" package="com.github.shadowsocks" xmlns:tools="http://schemas.android.com/tools" package="com.github.shadowsocks"
android:versionCode="139" android:versionCode="140"
android:versionName="2.10.6"> android:versionName="2.10.7">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <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.NFC" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-feature <uses-feature android:name="android.hardware.touchscreen"
android:name="android.hardware.touchscreen" android:required="false"/>
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" <uses-feature android:name="android.software.leanback"
android:required="false" /> android:required="false"/>
<uses-feature android:name="android.hardware.nfc" android:required="false" />
<uses-sdk <uses-sdk
android:minSdkVersion="16" android:minSdkVersion="16"
@@ -9,4 +9,5 @@ interface IShadowsocksService {
oneway void unregisterCallback(IShadowsocksServiceCallback cb); oneway void unregisterCallback(IShadowsocksServiceCallback cb);
oneway void use(in int profileId); 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) 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 # Import cpufeatures
$(call import-module,android/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_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="stat_profiles">\n%1$s↑\t%2$s↓</string>
<string name="traffic_summary">%1$s/s↑\t%2$s/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_pending">Проверить подключение</string>
<string name="connection_test_testing">Проверка…</string> <string name="connection_test_testing">Проверка…</string>
<string name="connection_test_available">Успешно: %dms задержка</string> <string name="connection_test_available">Успешно: %dms задержка</string>
-3
View File
@@ -82,9 +82,6 @@
<string name="stat">网络流量</string> <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> <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_pending">检查网络连接</string>
<string name="connection_test_testing">测试中……</string> <string name="connection_test_testing">测试中……</string>
-3
View File
@@ -83,9 +83,6 @@
<string name="stat">網路流量</string> <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> <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_pending">檢查連線能力</string>
<string name="connection_test_testing">測試中……</string> <string name="connection_test_testing">測試中……</string>
+2 -2
View File
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<declare-styleable name="DropDownPreference"> <declare-styleable name="DropDownPreference">
<attr name="entries" format="reference" /> <attr name="android:entries" />
<attr name="entryValues" format="reference" /> <attr name="android:entryValues" />
</declare-styleable> </declare-styleable>
<declare-styleable name="NumberPickerPreference"> <declare-styleable name="NumberPickerPreference">
<attr name="max" format="integer" /> <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_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="stat_profiles">\n%1$s↑\t%2$s↓</string>
<string name="traffic_summary">%1$s/s↑\t%2$s/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="one">Byte</item>
<item quantity="other">Bytes</item> <item quantity="other">Bytes</item>
</plurals> </plurals>
+4 -4
View File
@@ -37,8 +37,8 @@
<com.github.shadowsocks.preferences.DropDownPreference <com.github.shadowsocks.preferences.DropDownPreference
android:key="encMethod" android:key="encMethod"
android:persistent="false" android:persistent="false"
app:entries="@array/enc_method_entry" android:entries="@array/enc_method_entry"
app:entryValues="@array/enc_method_value" android:entryValues="@array/enc_method_value"
android:summary="%s" android:summary="%s"
android:title="@string/enc_method"/> android:title="@string/enc_method"/>
<SwitchPreference <SwitchPreference
@@ -55,8 +55,8 @@
<com.github.shadowsocks.preferences.DropDownPreference <com.github.shadowsocks.preferences.DropDownPreference
android:key="route" android:key="route"
android:persistent="false" android:persistent="false"
app:entries="@array/route_entry" android:entries="@array/route_entry"
app:entryValues="@array/route_value" android:entryValues="@array/route_value"
android:summary="%s" android:summary="%s"
android:title="@string/route_list"/> android:title="@string/route_list"/>
<SwitchPreference <SwitchPreference
@@ -158,23 +158,22 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
def onMenuItemClick(item: MenuItem): Boolean = { def onMenuItemClick(item: MenuItem): Boolean = {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager] val clipboard = getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
val prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext)
item.getItemId match { item.getItemId match {
case R.id.action_apply_all => case R.id.action_apply_all =>
app.profileManager.getAllProfiles match { app.profileManager.getAllProfiles match {
case Some(profiles) => case Some(profiles) =>
val proxiedAppString = prefs.getString(Key.individual, "") val proxiedAppString = profile.individual
profiles.foreach(profile => { profiles.foreach(p => {
profile.individual = proxiedAppString p.individual = proxiedAppString
app.profileManager.updateProfile(profile) app.profileManager.updateProfile(p)
}) })
Toast.makeText(this, R.string.action_apply_all, Toast.LENGTH_SHORT).show 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 case _ => Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show
} }
return true return true
case R.id.action_export => case R.id.action_export =>
val bypass = prefs.getBoolean(Key.bypass, false) val bypass = profile.bypass
val proxiedAppString = prefs.getString(Key.individual, "") val proxiedAppString = profile.individual
val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString) val clip = ClipData.newPlainText(Key.individual, bypass + "\n" + proxiedAppString)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show() 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) { if (proxiedAppSequence != null) {
val proxiedAppString = proxiedAppSequence.toString val proxiedAppString = proxiedAppSequence.toString
if (!proxiedAppString.isEmpty) { if (!proxiedAppString.isEmpty) {
val editor = prefs.edit
val i = proxiedAppString.indexOf('\n') val i = proxiedAppString.indexOf('\n')
try { try {
val (enabled, apps) = if (i < 0) (proxiedAppString, "") val (enabled, apps) = if (i < 0) (proxiedAppString, "")
else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1)) else (proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.setChecked(enabled.toBoolean) 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() Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
appListView.setVisibility(View.GONE) appListView.setVisibility(View.GONE)
loadingView.setVisibility(View.VISIBLE) 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) 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)) { 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.password = proxy(2).trim
profile.method = proxy(3).trim profile.method = proxy(3).trim
} catch { } catch {
case ex: Exception => case ex: Exception => stopRunner(true, getString(R.string.service_failed))
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner(true)
} }
def startRunner(profile: Profile) { def startRunner(profile: Profile) {
@@ -169,7 +169,7 @@ trait BaseService extends Service {
Utils.ThrowableFuture(connect) Utils.ThrowableFuture(connect)
} }
def stopRunner(stopService: Boolean) { def stopRunner(stopService: Boolean, msg: String = null) {
// clean up recevier // clean up recevier
if (closeReceiverRegistered) { if (closeReceiverRegistered) {
unregisterReceiver(closeReceiver) unregisterReceiver(closeReceiver)
@@ -186,7 +186,7 @@ trait BaseService extends Service {
} }
// change the state // change the state
changeState(State.STOPPED) changeState(State.STOPPED, msg)
// stop the service if nothing has bound to it // stop the service if nothing has bound to it
if (stopService) stopSelf() if (stopService) stopSelf()
@@ -242,7 +242,7 @@ trait BaseService extends Service {
protected def changeState(s: Int, msg: String = null) { protected def changeState(s: Int, msg: String = null) {
val handler = new Handler(getMainLooper) val handler = new Handler(getMainLooper)
handler.post(() => if (state != s) { handler.post(() => if (state != s || msg != null) {
if (callbacksCount > 0) { if (callbacksCount > 0) {
val n = callbacks.beginBroadcast() val n = callbacks.beginBroadcast()
for (i <- 0 until n) { for (i <- 0 until n) {
@@ -4,6 +4,7 @@ import java.nio.charset.Charset
import android.app.Activity import android.app.Activity
import android.content._ import android.content._
import android.content.pm.PackageManager
import android.nfc.NfcAdapter.CreateNdefMessageCallback import android.nfc.NfcAdapter.CreateNdefMessageCallback
import android.nfc.{NdefMessage, NdefRecord, NfcAdapter, NfcEvent} import android.nfc.{NdefMessage, NdefRecord, NfcAdapter, NfcEvent}
import android.os.{Bundle, Handler} 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] val importAddFAB = findViewById(R.id.fab_import_add).asInstanceOf[FloatingActionButton]
importAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_content_paste)) importAddFAB.setImageDrawable(dm.getDrawable(this, R.drawable.ic_content_paste))
importAddFAB.setOnClickListener(this) 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 package com.github.shadowsocks
import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.Manifest
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.support.v4.app.ActivityCompat import android.support.v4.app.ActivityCompat
import android.support.v4.app.NavUtils
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v7.app.ActionBar
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar import android.support.v7.widget.Toolbar
import android.view.MenuItem import android.view.{MenuItem, ViewGroup}
import android.view.View
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import com.google.zxing.Result import com.google.zxing.Result
import me.dm7.barcodescanner.core.IViewFinder
import me.dm7.barcodescanner.core.ViewFinderView
import me.dm7.barcodescanner.zxing.ZXingScannerView import me.dm7.barcodescanner.zxing.ZXingScannerView
class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHandler { class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHandler {
val MY_PERMISSIONS_REQUEST_CAMERA = 1 val MY_PERMISSIONS_REQUEST_CAMERA = 1
var scannerView: ZXingScannerView = null var scannerView: ZXingScannerView = _
override def onRequestPermissionsResult(requestCode: Int, permissions: Array[String], override def onRequestPermissionsResult(requestCode: Int, permissions: Array[String],
grantResults: Array[Int]) { grantResults: Array[Int]) {
@@ -65,6 +65,7 @@ import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils._ import com.github.shadowsocks.utils._
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.google.android.gms.ads.{AdRequest, AdSize, AdView} import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
import eu.chainfire.libsuperuser.Shell
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer
@@ -110,7 +111,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
// Services // Services
private val callback = new IShadowsocksServiceCallback.Stub { private val callback = new IShadowsocksServiceCallback.Stub {
def stateChanged(s: Int, m: String) { def stateChanged(s: Int, m: String) {
handler.post(() => if (state != s) { handler.post(() => {
s match { s match {
case State.CONNECTING => case State.CONNECTING =>
fab.setBackgroundTintList(greyTint) fab.setBackgroundTintList(greyTint)
@@ -286,34 +287,17 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
val cmd = new ArrayBuffer[String]() val cmd = new ArrayBuffer[String]()
for (task <- Array("ss-local", "ss-tunnel", "pdnsd", "redsocks", "tun2socks")) { 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("killall %s".formatLocal(Locale.ENGLISH, task))
cmd.append("chmod 666 %s/%s-vpn.pid".formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir, 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) 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))
} }
if (app.isNatEnabled) { if (app.isNatEnabled) {
Console.runRootCommand(cmd.toArray) cmd.append("iptables -t nat -F OUTPUT")
Console.runRootCommand(Utils.iptables + " -t nat -F OUTPUT") cmd.append("echo done")
} else Console.runCommand(cmd.toArray) val result = Shell.SU.run(cmd.toArray)
if (result != null && !result.isEmpty) return // fallback to SH
}
Shell.SH.run(cmd.toArray)
} }
def cancelStart() { def cancelStart() {
@@ -535,7 +519,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
for (executable <- EXECUTABLES) { for (executable <- EXECUTABLES) {
ab.append("chmod 755 " + getApplicationInfo.dataDir + "/" + executable) ab.append("chmod 755 " + getApplicationInfo.dataDir + "/" + executable)
} }
Console.runCommand(ab.toArray) Shell.SH.run(ab.toArray)
} }
def recovery() { def recovery() {
@@ -45,12 +45,12 @@ import java.net.{Inet6Address, InetAddress}
import java.util.Locale import java.util.Locale
import android.content._ import android.content._
import android.net.{ConnectivityManager, Network}
import android.os._ import android.os._
import android.util.{Log, SparseArray} import android.util.Log
import com.github.shadowsocks.utils._
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
import eu.chainfire.libsuperuser.Shell
import scala.collection.JavaConversions._ import scala.collection.JavaConversions._
import scala.collection.mutable.ArrayBuffer import scala.collection.mutable.ArrayBuffer
@@ -59,8 +59,7 @@ class ShadowsocksNatService extends BaseService {
val TAG = "ShadowsocksNatService" 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 = "iptables -t nat -A OUTPUT -p tcp " +
val CMD_IPTABLES_DNAT_ADD_SOCKS = " -t nat -A OUTPUT -p tcp " +
"-j DNAT --to-destination 127.0.0.1:8123" "-j DNAT --to-destination 127.0.0.1:8123"
private var notification: ShadowsocksNotification = _ private var notification: ShadowsocksNotification = _
@@ -70,78 +69,7 @@ class ShadowsocksNatService extends BaseService {
var sstunnelProcess: Process = _ var sstunnelProcess: Process = _
var redsocksProcess: Process = _ var redsocksProcess: Process = _
var pdnsdProcess: Process = _ var pdnsdProcess: Process = _
var su: Shell.Interactive = _
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")
}
}
def startShadowsocksDaemon() { def startShadowsocksDaemon() {
if (profile.route != Route.ALL) { if (profile.route != Route.ALL) {
@@ -264,7 +192,7 @@ class ShadowsocksNatService extends BaseService {
} }
/** Called when the activity is first created. */ /** Called when the activity is first created. */
def handleConnection: Boolean = { def handleConnection {
startTunnel() startTunnel()
if (!profile.udpdns) startDnsDaemon() if (!profile.udpdns) startDnsDaemon()
@@ -272,7 +200,6 @@ class ShadowsocksNatService extends BaseService {
startShadowsocksDaemon() startShadowsocksDaemon()
setupIptables() setupIptables()
true
} }
def onBind(intent: Intent): IBinder = { def onBind(intent: Intent): IBinder = {
@@ -302,7 +229,7 @@ class ShadowsocksNatService extends BaseService {
pdnsdProcess = null pdnsdProcess = null
} }
Console.runRootCommand(Utils.iptables + " -t nat -F OUTPUT") su.addCommand("iptables -t nat -F OUTPUT")
} }
def setupIptables() = { def setupIptables() = {
@@ -310,9 +237,9 @@ class ShadowsocksNatService extends BaseService {
val http_sb = new ArrayBuffer[String] val http_sb = new ArrayBuffer[String]
init_sb.append("ulimit -n 4096") 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]) { if (!InetAddress.getByName(profile.host.toUpperCase).isInstanceOf[Inet6Address]) {
init_sb.append(cmd_bypass.replace("-p tcp -d 0.0.0.0", "-d " + profile.host)) 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("-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(cmd_bypass.replace("-d 0.0.0.0", "--dport 53"))
init_sb.append(Utils.iptables init_sb.append("iptables -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:8153")
+ " -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:8153")
if (!profile.proxyApps || profile.bypass) { 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) { if (profile.proxyApps) {
val uidMap = getPackageManager.getInstalledApplications(0).map(ai => ai.packageName -> ai.uid).toMap val uidMap = getPackageManager.getInstalledApplications(0).map(ai => ai.packageName -> ai.uid).toMap
for (pn <- profile.individual.split('\n')) uidMap.get(pn) match { for (pn <- profile.individual.split('\n')) uidMap.get(pn) match {
case Some(uid) => case Some(uid) =>
if (!profile.bypass) { 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)) .replace("-t nat", "-t nat -m owner --uid-owner " + uid))
} else { } else {
init_sb.append(cmd_bypass.replace("-d 0.0.0.0", "-m owner --uid-owner " + uid)) 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 case _ => // probably removed package, ignore
} }
} }
Console.runRootCommand((init_sb ++ http_sb).toArray) su.addCommand((init_sb ++ http_sb).toArray)
} }
override def startRunner(profile: Profile) { override def startRunner(profile: Profile) = if (su == null) {
if (!Console.isRoot) { su = new Shell.Builder().useSU().setWantSTDERR(true).setWatchdogTimeout(10).open((_, exitCode, _) =>
changeState(State.STOPPED, getString(R.string.nat_no_root)) if (exitCode == 0) super.startRunner(profile) else {
stopRunner(true) Log.wtf(TAG, "libsuperuser#55 has been fixed. Please remove the redundant code.")
return 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() { override def connect() {
super.connect() super.connect()
if (this.profile != null) { if (profile != null) {
// Clean up // Clean up
killProcesses() killProcesses()
var resolved: Boolean = false var resolved = false
if (!Utils.isNumeric(this.profile.host)) { if (!Utils.isNumeric(profile.host)) {
Utils.resolve(this.profile.host, enableIPv6 = true) match { Utils.resolve(profile.host, enableIPv6 = true) match {
case Some(a) => case Some(a) =>
this.profile.host = a profile.host = a
resolved = true resolved = true
case None => resolved = false case None => resolved = false
} }
@@ -371,33 +304,31 @@ class ShadowsocksNatService extends BaseService {
resolved = true resolved = true
} }
if (!resolved) { if (!resolved) stopRunner(true, getString(R.string.invalid_server)) else {
changeState(State.STOPPED, getString(R.string.invalid_server)) handleConnection
stopRunner(true)
} else if (handleConnection) {
// Set DNS // Set DNS
flushDns() su.addCommand(Utils.FLUSH_DNS)
changeState(State.CONNECTED) changeState(State.CONNECTED)
notification = new ShadowsocksNotification(this, profile.name, true) 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 // channge the state
changeState(State.STOPPING) changeState(State.STOPPING)
if (notification != null) notification.destroy()
app.track(TAG, "stop") app.track(TAG, "stop")
// reset NAT // reset NAT
killProcesses() 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) { def setVisible(visible: Boolean, forceShow: Boolean = false) = if (isVisible != visible) {
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() show()
} else if (forceShow) show() } else if (forceShow) show()
def show() = { def show() = service.startForeground(1, builder.build)
service.startForeground(1, builder.build)
}
def destroy() { def destroy() {
if (lockReceiver != null) { if (lockReceiver != null) {
@@ -43,7 +43,6 @@ import android.app.Service
import android.content.Intent import android.content.Intent
import android.net.VpnService import android.net.VpnService
import android.os.{IBinder, Handler} import android.os.{IBinder, Handler}
import com.github.shadowsocks.utils.Action
import com.github.shadowsocks.utils.ConfigUtils import com.github.shadowsocks.utils.ConfigUtils
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
@@ -55,29 +54,18 @@ class ShadowsocksRunnerService extends Service with ServiceBoundContext {
} }
override def onServiceConnected() { 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() { def startBackgroundService() = bgService.useSync(app.profileId)
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()
}
override def onCreate() { override def onCreate() {
super.onCreate() super.onCreate()
attachService() 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() { override def onDestroy() {
@@ -82,7 +82,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
stopRunner(true) stopRunner(true)
} }
override def stopRunner(stopService: Boolean) { override def stopRunner(stopService: Boolean, msg: String = null) {
if (vpnThread != null) { if (vpnThread != null) {
vpnThread.stopThread() vpnThread.stopThread()
@@ -105,7 +105,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
conn = null conn = null
} }
super.stopRunner(stopService) super.stopRunner(stopService, msg)
} }
def killProcesses() { def killProcesses() {
@@ -165,10 +165,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
resolved = true resolved = true
} }
if (!resolved) { if (!resolved) stopRunner(true, getString(R.string.invalid_server)) else if (handleConnection) {
changeState(State.STOPPED, getString(R.string.invalid_server))
stopRunner(true)
} else if (handleConnection) {
changeState(State.CONNECTED) changeState(State.CONNECTED)
notification = new ShadowsocksNotification(this, profile.name) notification = new ShadowsocksNotification(this, profile.name)
} else { } else {
@@ -5,7 +5,7 @@ import android.content.Context
import android.content.res.TypedArray import android.content.res.TypedArray
import android.os.Build import android.os.Build
import android.preference.Preference import android.preference.Preference
import android.support.annotation.NonNull import android.support.annotation.{ArrayRes, NonNull}
import android.support.v7.widget.AppCompatSpinner import android.support.v7.widget.AppCompatSpinner
import android.util.AttributeSet import android.util.AttributeSet
import android.view.{View, ViewGroup} 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 val mSpinner = new AppCompatSpinner(mContext)
private var mEntries: Array[CharSequence] = _ private var mEntries: Array[CharSequence] = _
private var mEntryValues: Array[CharSequence] = _ private var mEntryValues: Array[CharSequence] = _
private var mSelectedIndex: Int = _ private var mSelectedIndex: Int = -1
private var mValueSet: Boolean = _
mSpinner.setVisibility(View.INVISIBLE) mSpinner.setVisibility(View.INVISIBLE)
mSpinner.setAdapter(mAdapter) mSpinner.setAdapter(mAdapter)
mSpinner.setOnItemSelectedListener(new OnItemSelectedListener { mSpinner.setOnItemSelectedListener(new OnItemSelectedListener {
override def onNothingSelected(parent: AdapterView[_]) { } def onNothingSelected(parent: AdapterView[_]) { }
override def onItemSelected(parent: AdapterView[_], view: View, position: Int, id: Long) = setValueIndex(position) 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) => { setOnPreferenceClickListener((preference: Preference) => {
// TODO: not working with scrolling // TODO: not working with scrolling
@@ -39,8 +41,8 @@ final class DropDownPreference(private val mContext: Context, attrs: AttributeSe
true true
}) })
val a: TypedArray = mContext.obtainStyledAttributes(attrs, R.styleable.DropDownPreference) val a: TypedArray = mContext.obtainStyledAttributes(attrs, R.styleable.DropDownPreference)
setEntries(a.getTextArray(R.styleable.DropDownPreference_entries)) setEntries(a.getTextArray(R.styleable.DropDownPreference_android_entries))
mEntryValues = a.getTextArray(R.styleable.DropDownPreference_entryValues) setEntryValues(a.getTextArray(R.styleable.DropDownPreference_android_entryValues))
a.recycle a.recycle
protected override def getSummaryValue = getEntry protected override def getSummaryValue = getEntry
@@ -62,7 +64,7 @@ final class DropDownPreference(private val mContext: Context, attrs: AttributeSe
* @see #setEntries(CharSequence[]) * @see #setEntries(CharSequence[])
* @param entriesResId The entries array as a resource. * @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. * 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[]) * @see #setEntryValues(CharSequence[])
* @param entryValuesResId The entry values array as a resource. * @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)) setEntryValues(getContext.getResources.getTextArray(entryValuesResId))
/** /**
* Returns the array of values to be saved for the preference. * 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 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]]. * Sets the value of the key. This should be one of the entries in [[getEntryValues]].
* *
* @param value The value to set for the key. * @param value The value to set for the key.
*/ */
def setValue(value: String) { def setValue(value: String): Unit = setValue(findIndexOfValue(value), value)
val i = findIndexOfValue(value)
if (i > -1) setValueIndex(i)
}
/** /**
* Sets the value to the given index from the entry values. * 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. * @param index The index of the value to set.
*/ */
def setValueIndex(index: Int) { def setValueIndex(index: Int) = setValue(index, getValue(index))
val changed = mSelectedIndex != index
if (changed || !mValueSet) { private def setValue(index: Int, value: String) {
mSelectedIndex = index persistString(value)
mValueSet = true mSelectedIndex = index
if (mEntryValues != null) {
val value = mEntryValues(index).toString
if (changed) callChangeListener(value)
persistString(value)
}
if (changed) notifyChanged
}
mSpinner.setSelection(index) mSpinner.setSelection(index)
notifyChanged
} }
/** /**
@@ -132,7 +120,7 @@ final class DropDownPreference(private val mContext: Context, attrs: AttributeSe
* *
* @return The value of the key. * @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. * 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 getMin = if (picker == null) 0 else picker.getMinValue
def getMax = picker.getMaxValue def getMax = picker.getMaxValue
def setValue(i: Int) { def setValue(i: Int) {
if (i == getValue || !callChangeListener(i)) return if (i == getValue) return
picker.setValue(i) picker.setValue(i)
value = picker.getValue value = picker.getValue
persistInt(value) persistInt(value)
@@ -49,7 +49,14 @@ final class NumberPickerPreference(context: Context, attrs: AttributeSet = null)
override protected def onDialogClosed(positiveResult: Boolean) { override protected def onDialogClosed(positiveResult: Boolean) {
picker.clearFocus // commit changes picker.clearFocus // commit changes
super.onDialogClosed(positiveResult) // forward compatibility 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 onGetDefaultValue(a: TypedArray, index: Int) = a.getInt(index, getMin).asInstanceOf[AnyRef]
override protected def onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any) { 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 android.widget.Toast
import com.github.shadowsocks.ShadowsocksApplication.app import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService} import com.github.shadowsocks.{BuildConfig, ShadowsocksRunnerService}
import eu.chainfire.libsuperuser.Shell
import org.xbill.DNS._ import org.xbill.DNS._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
@@ -62,18 +63,6 @@ import scala.util.{Failure, Try}
object Utils { object Utils {
private val TAG = "Shadowsocks" 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 isLollipopOrAbove: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
def getSignature(context: Context): String = { 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 // Blocked > 3 seconds
def toggleAirplaneMode(context: Context) = { def toggleAirplaneMode(context: Context) = {
if (Console.isRoot) { val result = Shell.SU.run(FLUSH_DNS)
Console.runRootCommand("ndc resolver flushdefaultif", "ndc resolver flushif wlan0") if (result != null && !result.isEmpty) true else if (Build.VERSION.SDK_INT < 17) {
true
} else if (Build.VERSION.SDK_INT < 17) {
toggleBelowApiLevel17(context) toggleBelowApiLevel17(context)
true true
} else false } else false