diff --git a/build.sbt b/build.sbt
index 2194c008..3abae4dd 100644
--- a/build.sbt
+++ b/build.sbt
@@ -37,6 +37,7 @@ libraryDependencies ++= Seq(
"com.google.android.gms" % "play-services-analytics" % "8.1.0",
"com.android.support" % "support-v4" % "23.1.0",
"com.android.support" % "design" % "23.1.0",
+ "com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01",
"com.nostra13.universalimageloader" % "universal-image-loader" % "1.9.4",
"com.j256.ormlite" % "ormlite-core" % "4.48",
"com.j256.ormlite" % "ormlite-android" % "4.48"
diff --git a/src/main/res/drawable-hdpi-v21/ic_drawer.png b/src/main/res/drawable-hdpi-v21/ic_drawer.png
deleted file mode 100755
index 5c2c7f1e..00000000
Binary files a/src/main/res/drawable-hdpi-v21/ic_drawer.png and /dev/null differ
diff --git a/src/main/res/drawable-hdpi/ic_drawer.png b/src/main/res/drawable-hdpi/ic_drawer.png
index ec8c51f8..5c2c7f1e 100644
Binary files a/src/main/res/drawable-hdpi/ic_drawer.png and b/src/main/res/drawable-hdpi/ic_drawer.png differ
diff --git a/src/main/res/drawable-mdpi-v21/ic_drawer.png b/src/main/res/drawable-mdpi-v21/ic_drawer.png
deleted file mode 100755
index c9121257..00000000
Binary files a/src/main/res/drawable-mdpi-v21/ic_drawer.png and /dev/null differ
diff --git a/src/main/res/drawable-mdpi/ic_drawer.png b/src/main/res/drawable-mdpi/ic_drawer.png
index 7420a68d..c9121257 100644
Binary files a/src/main/res/drawable-mdpi/ic_drawer.png and b/src/main/res/drawable-mdpi/ic_drawer.png differ
diff --git a/src/main/res/drawable-xhdpi-v21/ic_drawer.png b/src/main/res/drawable-xhdpi-v21/ic_drawer.png
deleted file mode 100755
index 56f01f3e..00000000
Binary files a/src/main/res/drawable-xhdpi-v21/ic_drawer.png and /dev/null differ
diff --git a/src/main/res/drawable-xhdpi/ic_drawer.png b/src/main/res/drawable-xhdpi/ic_drawer.png
index 7ffbe5c5..56f01f3e 100644
Binary files a/src/main/res/drawable-xhdpi/ic_drawer.png and b/src/main/res/drawable-xhdpi/ic_drawer.png differ
diff --git a/src/main/res/drawable-xxhdpi-v21/ic_drawer.png b/src/main/res/drawable-xxhdpi/ic_drawer.png
old mode 100755
new mode 100644
similarity index 100%
rename from src/main/res/drawable-xxhdpi-v21/ic_drawer.png
rename to src/main/res/drawable-xxhdpi/ic_drawer.png
diff --git a/src/main/res/layout/layout_apps.xml b/src/main/res/layout/layout_apps.xml
index 0f02c21a..f84d4bbd 100644
--- a/src/main/res/layout/layout_apps.xml
+++ b/src/main/res/layout/layout_apps.xml
@@ -1,35 +1,37 @@
-
-
+
+
+ android:text="@string/bypass_apps"/>
-
+ android:layout_marginEnd="6dp"
+ android:layout_marginRight="6dp"/>
+
-
+ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent" android:layout_height="match_parent"
+ tools:ignore="UnknownIdInLayout" android:clipChildren="false">
-
+ android:layout_width="match_parent" android:layout_height="match_parent"/>
+
+
+
diff --git a/src/main/res/values-v21/dimen.xml b/src/main/res/values-v21/dimen.xml
new file mode 100644
index 00000000..4c95daeb
--- /dev/null
+++ b/src/main/res/values-v21/dimen.xml
@@ -0,0 +1,4 @@
+
+
+ -28dp
+
diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml
index c9457d6f..195090d5 100755
--- a/src/main/res/values-zh/strings.xml
+++ b/src/main/res/values-zh/strings.xml
@@ -26,7 +26,6 @@
国内路由
绕过所有位于中国的网站
路由
- 绕过指定路由列表中的地址
全局代理
设置系统代理
分应用代理
@@ -54,7 +53,7 @@
端口号不能为空
代理服务器地址不能为空
检测到意外退出,状态已被重置。
- 正在连接...
+ 连接
正在初始化...
重置中...
正在加载...
diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..ca0adbc5
--- /dev/null
+++ b/src/main/res/values/attrs.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml
index 2ce8dac8..157d3c4a 100644
--- a/src/main/res/values/colors.xml
+++ b/src/main/res/values/colors.xml
@@ -12,7 +12,7 @@
#00000000
#FF555555
- #2196F3
+ #43A047
#90A4AE
#607D8B
#455A64
diff --git a/src/main/res/values/colors_shadow.xml b/src/main/res/values/colors_shadow.xml
deleted file mode 100644
index 87e9fcf5..00000000
--- a/src/main/res/values/colors_shadow.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
- #CC33B5E5
- #EEE
-
diff --git a/src/main/res/values/dimen.xml b/src/main/res/values/dimen.xml
new file mode 100644
index 00000000..f8e782e7
--- /dev/null
+++ b/src/main/res/values/dimen.xml
@@ -0,0 +1,5 @@
+
+
+
+ -44dp
+
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 7b8660ee..0fa48668 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -37,7 +37,6 @@
Bypass all sites located in China
Route
- Bypass IP addresses in the route list
Global Proxy
Set up system-wide proxy
Per-App Proxy
@@ -75,7 +74,7 @@
Proxy should not be empty
An unexpected exit detected, the context has been reset.
- Connecting…
+ Connect
Initializing…
Resetting…
Loading…
diff --git a/src/main/res/xml/pref_all.xml b/src/main/res/xml/pref_all.xml
index f6471c9a..c4df91c0 100644
--- a/src/main/res/xml/pref_all.xml
+++ b/src/main/res/xml/pref_all.xml
@@ -1,5 +1,6 @@
-
+
@@ -37,40 +38,41 @@
android:summary="@string/sitekey_summary"
android:title="@string/sitekey">
-
-
+
-
-
-
+
-
-
+
-
+
-
-
-
+
-
-
+
-
+
diff --git a/src/main/scala/com/github/shadowsocks/AppManager.scala b/src/main/scala/com/github/shadowsocks/AppManager.scala
index d6c16be8..88004783 100644
--- a/src/main/scala/com/github/shadowsocks/AppManager.scala
+++ b/src/main/scala/com/github/shadowsocks/AppManager.scala
@@ -78,7 +78,7 @@ class ObjectArrayTools[T <: AnyRef](a: Array[T]) {
}
}
-case class ListEntry(box: CheckBox, text: TextView, icon: ImageView)
+case class ListEntry(switch: Switch, text: TextView, icon: ImageView)
object AppManager {
@@ -162,13 +162,13 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
if (convertView == null) {
convertView = getLayoutInflater.inflate(R.layout.layout_apps_item, parent, false)
val icon = convertView.findViewById(R.id.itemicon).asInstanceOf[ImageView]
- val box = convertView.findViewById(R.id.itemcheck).asInstanceOf[CheckBox]
+ val switch = convertView.findViewById(R.id.itemcheck).asInstanceOf[Switch]
val text = convertView.findViewById(R.id.itemtext).asInstanceOf[TextView]
- entry = new ListEntry(box, text, icon)
+ entry = new ListEntry(switch, text, icon)
entry.text.setOnClickListener(AppManager.this)
entry.text.setOnClickListener(AppManager.this)
convertView.setTag(entry)
- entry.box.setOnCheckedChangeListener(AppManager.this)
+ entry.switch.setOnCheckedChangeListener(AppManager.this)
} else {
entry = convertView.getTag.asInstanceOf[ListEntry]
}
@@ -187,10 +187,10 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
ImageLoader.getInstance().displayImage(Scheme.APP + app.packageName, entry.icon, options)
entry.text.setText(app.name)
- val box: CheckBox = entry.box
- box.setTag(app)
- box.setChecked(app.proxied)
- entry.text.setTag(box)
+ val switch = entry.switch
+ switch.setTag(app)
+ switch.setChecked(app.proxied)
+ entry.text.setTag(switch)
convertView
}
}
@@ -207,11 +207,11 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
}
def onClick(v: View) {
- val cbox: CheckBox = v.getTag.asInstanceOf[CheckBox]
- val app: ProxiedApp = cbox.getTag.asInstanceOf[ProxiedApp]
+ val switch = v.getTag.asInstanceOf[Switch]
+ val app: ProxiedApp = switch.getTag.asInstanceOf[ProxiedApp]
if (app != null) {
app.proxied = !app.proxied
- cbox.setChecked(app.proxied)
+ switch.setChecked(app.proxied)
}
saveAppSettings(this)
}
@@ -247,8 +247,8 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
val array = proxiedAppString.split(" ")
val bypass = array(0).toBoolean
val apps = if (array.size > 1) array(1) else ""
- prefs.edit.putBoolean(Key.isBypassApps, bypass).commit()
- prefs.edit.putString(Key.proxied, apps).commit()
+ prefs.edit.putBoolean(Key.isBypassApps, bypass).apply()
+ prefs.edit.putString(Key.proxied, apps).apply()
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
// Restart activity
val intent = getIntent
@@ -299,7 +299,7 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
val bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
val prefs = PreferenceManager.getDefaultSharedPreferences(getBaseContext)
bypassSwitch.setOnCheckedChangeListener((button: CompoundButton, checked: Boolean) =>
- prefs.edit().putBoolean(Key.isBypassApps, checked).commit())
+ prefs.edit().putBoolean(Key.isBypassApps, checked).apply())
bypassSwitch.setChecked(prefs.getBoolean(Key.isBypassApps, false))
appListView = findViewById(R.id.applistview).asInstanceOf[ListView]
@@ -325,7 +325,7 @@ class AppManager extends Activity with OnCheckedChangeListener with OnClickListe
})
val edit: SharedPreferences.Editor = prefs.edit
edit.putString(Key.proxied, proxiedApps.toString())
- edit.commit
+ edit.apply
}
val loadStartRunnable = new Runnable {
diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
index a10b19d6..82f77202 100644
--- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
+++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
@@ -50,17 +50,18 @@ import android.content.res.AssetManager
import android.graphics.{Bitmap, Color, Typeface}
import android.net.{Uri, VpnService}
import android.os._
-import android.preference._
+import android.preference.{Preference, PreferenceManager, SwitchPreference}
import android.support.design.widget.{FloatingActionButton, Snackbar}
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.util.{DisplayMetrics, Log}
-import android.view._
+import android.view.{View, ViewGroup, ViewParent}
import android.webkit.{WebView, WebViewClient}
import android.widget._
+import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.database._
-import com.github.shadowsocks.preferences.{PasswordEditTextPreference, ProfileEditTextPreference, SummaryEditTextPreference}
+import com.github.shadowsocks.preferences.{DropDownPreference, PasswordEditTextPreference, ProfileEditTextPreference, SummaryEditTextPreference}
import com.github.shadowsocks.utils._
import com.google.android.gms.ads.{AdRequest, AdSize, AdView}
import com.google.android.gms.analytics.HitBuilders
@@ -132,8 +133,8 @@ object Shadowsocks {
val EXECUTABLES = Array(Executable.PDNSD, Executable.REDSOCKS, Executable.SS_TUNNEL, Executable.SS_LOCAL, Executable.TUN2SOCKS)
// Helper functions
- def updateListPreference(pref: Preference, value: String) {
- pref.asInstanceOf[ListPreference].setValue(value)
+ def updateDropDownPreference(pref: Preference, value: String) {
+ pref.asInstanceOf[DropDownPreference].setValue(value)
}
def updatePasswordEditTextPreference(pref: Preference, value: String) {
@@ -151,8 +152,8 @@ object Shadowsocks {
pref.asInstanceOf[ProfileEditTextPreference].setText(value)
}
- def updateCheckBoxPreference(pref: Preference, value: Boolean) {
- pref.asInstanceOf[CheckBoxPreference].setChecked(value)
+ def updateSwitchPreference(pref: Preference, value: Boolean) {
+ pref.asInstanceOf[SwitchPreference].setChecked(value)
}
def updatePreference(pref: Preference, name: String, profile: Profile) {
@@ -162,12 +163,12 @@ object Shadowsocks {
case Key.remotePort => updateSummaryEditTextPreference(pref, profile.remotePort.toString)
case Key.localPort => updateSummaryEditTextPreference(pref, profile.localPort.toString)
case Key.sitekey => updatePasswordEditTextPreference(pref, profile.password)
- case Key.encMethod => updateListPreference(pref, profile.method)
- case Key.route => updateListPreference(pref, profile.route)
- case Key.isGlobalProxy => updateCheckBoxPreference(pref, profile.global)
- case Key.isUdpDns => updateCheckBoxPreference(pref, profile.udpdns)
- case Key.isAuth => updateCheckBoxPreference(pref, profile.auth)
- case Key.isIpv6 => updateCheckBoxPreference(pref, profile.ipv6)
+ case Key.encMethod => updateDropDownPreference(pref, profile.method)
+ case Key.route => updateDropDownPreference(pref, profile.route)
+ case Key.isGlobalProxy => updateSwitchPreference(pref, profile.global)
+ case Key.isUdpDns => updateSwitchPreference(pref, profile.udpdns)
+ case Key.isAuth => updateSwitchPreference(pref, profile.auth)
+ case Key.isIpv6 => updateSwitchPreference(pref, profile.ipv6)
case _ =>
}
}
@@ -186,6 +187,7 @@ class Shadowsocks
// Variables
private var serviceStarted = false
var fab: FloatingActionButton = _
+ var fabProgressCircle: FABProgressCircle = _
var progressDialog: ProgressDialog = _
var progressTag = -1
var state = State.INIT
@@ -221,7 +223,7 @@ class Shadowsocks
state = bgService.getState
if (!status.getBoolean(getVersionName, false)) {
- status.edit.putBoolean(getVersionName, true).commit()
+ status.edit.putBoolean(getVersionName, true).apply()
recovery()
}
}
@@ -394,7 +396,6 @@ class Shadowsocks
}
def prepareStartService() {
- showProgress(R.string.connecting)
Future {
if (isVpnEnabled) {
val intent = VpnService.prepare(this)
@@ -485,6 +486,7 @@ class Shadowsocks
val tf: Typeface = Typefaces.get(this, "fonts/Iceland.ttf")
if (tf != null) title.setTypeface(tf)
fab = findViewById(R.id.fab).asInstanceOf[FloatingActionButton]
+ fabProgressCircle = findViewById(R.id.fabProgressCircle).asInstanceOf[FABProgressCircle]
fab.setOnClickListener((v: View) => {
serviceStarted = !serviceStarted
serviceStarted match {
@@ -495,24 +497,31 @@ class Shadowsocks
changeSwitch(checked = false)
case false =>
serviceStop()
+ if (fab.isEnabled) {
+ fab.setEnabled(false)
+ handler.postDelayed(() => {
+ fab.setEnabled(true)
+ }, 1000)
+ }
}
- if (fab.isEnabled) {
- fab.setEnabled(false)
- handler.postDelayed(() => fab.setEnabled(true), 1000)
- }
+ })
+ fab.setOnLongClickListener((v: View) => {
+ Utils.positionToast(Toast.makeText(this, if (serviceStarted) R.string.stop else R.string.connect,
+ Toast.LENGTH_SHORT), fab, getWindow, 0, Utils.dpToPx(this, 8).toInt).show
+ true
})
toolbar.setNavigationIcon(R.drawable.ic_drawer)
toolbar.setNavigationOnClickListener((v: View) => drawer.toggleMenu())
title.setOnLongClickListener((v: View) => {
if (Utils.isLollipopOrAbove && bgService != null
&& (bgService.getState == State.INIT || bgService.getState == State.STOPPED)) {
- val natEnabled = status.getBoolean(Key.isNAT, false)
- status.edit().putBoolean(Key.isNAT, !natEnabled).commit()
- if (!natEnabled) {
- Toast.makeText(getBaseContext, R.string.enable_nat, Toast.LENGTH_LONG).show()
- } else {
- Toast.makeText(getBaseContext, R.string.disable_nat, Toast.LENGTH_LONG).show()
- }
+ val natEnabled = !status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove)
+ status.edit().putBoolean(Key.isNAT, natEnabled).apply()
+ deattachService()
+ attachService()
+ if (!Utils.isLollipopOrAbove) setPreferenceEnabled(State.isAvailable(bgService.getState))
+ Toast.makeText(getBaseContext, if (natEnabled) R.string.enable_nat else R.string.disable_nat, Toast.LENGTH_LONG)
+ .show()
true
} else {
false
@@ -523,13 +532,9 @@ class Shadowsocks
registerReceiver(preferenceReceiver, new IntentFilter(Action.UPDATE_PREFS))
// Bind to the service
- Future {
- val isRoot = (!Utils.isLollipopOrAbove || status.getBoolean(Key.isNAT, false)) && Console.isRoot
- handler.post(() => {
- status.edit.putBoolean(Key.isRoot, isRoot).commit()
- attachService()
- })
- }
+ handler.post(() => {
+ attachService()
+ })
}
def attachService() {
@@ -777,11 +782,17 @@ class Shadowsocks
super.onResume()
if (bgService != null) {
bgService.getState match {
- case State.CONNECTED =>
- changeSwitch(checked = true)
case State.CONNECTING =>
+ fabProgressCircle.show()
changeSwitch(checked = true)
+ case State.CONNECTED =>
+ fabProgressCircle.hide()
+ changeSwitch(checked = true)
+ case State.STOPPING =>
+ fabProgressCircle.show()
+ changeSwitch(checked = false)
case _ =>
+ fabProgressCircle.hide()
changeSwitch(checked = false)
}
state = bgService.getState
@@ -886,7 +897,6 @@ class Shadowsocks
private def showQrCode() {
val image = new ImageView(this)
- image.setPadding(0, dp2px(20), 0, dp2px(20))
image.setLayoutParams(new LinearLayout.LayoutParams(-1, -1))
val qrcode = QRCode.from(Parser.generate(currentProfile))
.withSize(dp2px(250), dp2px(250)).asInstanceOf[QRCode]
@@ -928,15 +938,10 @@ class Shadowsocks
}
}
+ var isRoot: Option[Boolean] = None
def isVpnEnabled: Boolean = {
- if (vpnEnabled < 0) {
- vpnEnabled = if (!status.getBoolean(Key.isRoot, false)) {
- 1
- } else {
- 0
- }
- }
- if (vpnEnabled == 1) true else false
+ if (isRoot.isEmpty) isRoot = Some(Console.isRoot)
+ !(isRoot.get && status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove))
}
def serviceStop() {
@@ -1015,36 +1020,31 @@ class Shadowsocks
def onStateChanged(s: Int, m: String) {
handler.post(() => if (state != s) {
- state = s
- state match {
+ s match {
case State.CONNECTING =>
- if (progressDialog == null) {
- progressDialog = ProgressDialog
- .show(Shadowsocks.this, "", getString(R.string.connecting), true, true)
- progressTag = R.string.connecting
- }
+ fab.setImageResource(R.drawable.ic_cloud_queue)
+ fab.setEnabled(false)
+ fabProgressCircle.show()
setPreferenceEnabled(enabled = false)
case State.CONNECTED =>
- if (progressTag == R.string.connecting) {
- clearDialog()
- }
+ if (state == State.CONNECTING) fabProgressCircle.beginFinalAnimation() else fabProgressCircle.hide()
+ fab.setEnabled(true)
changeSwitch(checked = true)
setPreferenceEnabled(enabled = false)
case State.STOPPED =>
- if (progressTag == R.string.stopping || progressTag == R.string.connecting) {
- clearDialog()
- }
+ handler.post(() => fabProgressCircle.hide())
+ fab.setEnabled(true)
changeSwitch(checked = false)
if (m != null) Snackbar.make(getWindow.getDecorView.findViewById(android.R.id.content),
getString(R.string.vpn_error).formatLocal(Locale.ENGLISH, m), Snackbar.LENGTH_LONG).show
setPreferenceEnabled(enabled = true)
case State.STOPPING =>
- if (progressDialog == null) {
- progressDialog = ProgressDialog
- .show(Shadowsocks.this, "", getString(R.string.stopping), true, true)
- progressTag = R.string.stopping
- }
+ fab.setImageResource(R.drawable.ic_cloud_queue)
+ fab.setEnabled(false)
+ fabProgressCircle.show()
+ setPreferenceEnabled(enabled = false)
}
+ state = s
})
}
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
index 8ca63951..6029337c 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
@@ -50,6 +50,7 @@ import android.content.pm.{PackageInfo, PackageManager}
import android.net.{Network, ConnectivityManager}
import android.os._
import android.support.v4.app.NotificationCompat
+import android.support.v4.content.ContextCompat
import android.util.{SparseArray, Log}
import android.widget.Toast
import com.github.shadowsocks.aidl.Config
@@ -319,6 +320,7 @@ class ShadowsocksNatService extends Service with BaseService {
builder
.setWhen(0)
+ .setColor(ContextCompat.getColor(this, R.color.material_accent_500))
.setTicker(title)
.setContentTitle(getString(R.string.app_name))
.setContentText(info)
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
index 133c0add..7b2d3817 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerActivity.scala
@@ -65,19 +65,13 @@ class ShadowsocksRunnerActivity extends Activity {
}
// Variables
- var vpnEnabled = -1
- var bgService: IShadowsocksService = null
- var receiver:BroadcastReceiver = null
+ var isRoot: Option[Boolean] = None
+ var bgService: IShadowsocksService = _
+ var receiver:BroadcastReceiver = _
def isVpnEnabled: Boolean = {
- if (vpnEnabled < 0) {
- vpnEnabled = if ((!Utils.isLollipopOrAbove || status.getBoolean(Key.isNAT, false)) && Console.isRoot) {
- 0
- } else {
- 1
- }
- }
- if (vpnEnabled == 1) true else false
+ if (isRoot.isEmpty) isRoot = Some(Console.isRoot)
+ !(isRoot.get && status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove))
}
@@ -127,6 +121,7 @@ class ShadowsocksRunnerActivity extends Activity {
} else {
attachService()
}
+ finish
}
override def onDestroy() {
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala
index 981c5d30..e27168f2 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksRunnerService.scala
@@ -64,18 +64,12 @@ class ShadowsocksRunnerService extends Service {
}
// Variables
- var vpnEnabled = -1
- var bgService: IShadowsocksService = null
+ var isRoot: Option[Boolean] = None
+ var bgService: IShadowsocksService = _
def isVpnEnabled: Boolean = {
- if (vpnEnabled < 0) {
- vpnEnabled = if ((!Utils.isLollipopOrAbove || status.getBoolean(Key.isNAT, false)) && Console.isRoot) {
- 0
- } else {
- 1
- }
- }
- if (vpnEnabled == 1) true else false
+ if (isRoot.isEmpty) isRoot = Some(Console.isRoot)
+ !(isRoot.get && status.getBoolean(Key.isNAT, !Utils.isLollipopOrAbove))
}
override def onBind(intent: Intent): IBinder = {
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
index d7e11665..72e9449d 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
@@ -48,6 +48,7 @@ import android.content.pm.{PackageInfo, PackageManager}
import android.net.VpnService
import android.os._
import android.support.v4.app.NotificationCompat
+import android.support.v4.content.ContextCompat
import android.util.Log
import android.widget.Toast
import com.github.shadowsocks.aidl.Config
@@ -113,6 +114,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
builder
.setWhen(0)
+ .setColor(ContextCompat.getColor(this, R.color.material_accent_500))
.setTicker(title)
.setContentTitle(getString(R.string.app_name))
.setContentText(info)
diff --git a/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala b/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala
index 149dead4..08ea17d3 100644
--- a/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala
+++ b/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala
@@ -133,7 +133,7 @@ class ProfileManager(settings: SharedPreferences, dbHelper: DBHelper) {
edit.putString(Key.proxied, profile.individual)
edit.putInt(Key.profileId, profile.id)
edit.putString(Key.route, profile.route)
- edit.commit()
+ edit.apply()
profile
}
diff --git a/src/main/scala/com/github/shadowsocks/preferences/DropDownPreference.scala b/src/main/scala/com/github/shadowsocks/preferences/DropDownPreference.scala
new file mode 100644
index 00000000..142e384a
--- /dev/null
+++ b/src/main/scala/com/github/shadowsocks/preferences/DropDownPreference.scala
@@ -0,0 +1,180 @@
+package com.github.shadowsocks.preferences
+
+import android.annotation.TargetApi
+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.v7.widget.AppCompatSpinner
+import android.util.AttributeSet
+import android.view.{View, ViewGroup}
+import android.widget.AdapterView.OnItemSelectedListener
+import android.widget.{AdapterView, ArrayAdapter}
+import com.github.shadowsocks.R
+
+/**
+ * Based on: https://github.com/Mygod/mygod-lib-android/blob/683c41cb86d10e99ab0f457d41a218c6915fccc8/lib/src/main/scala/tk/mygod/preference/DropDownPreference.scala
+ * @author Mygod
+ */
+final class DropDownPreference(private val mContext: Context, attrs: AttributeSet = null)
+ extends Preference(mContext, attrs) with SummaryPreference {
+
+ private val mAdapter = new ArrayAdapter[String](mContext, android.R.layout.simple_spinner_dropdown_item)
+ private val mSpinner = new AppCompatSpinner(mContext)
+ private var mEntries: Array[CharSequence] = _
+ private var mEntryValues: Array[CharSequence] = _
+ private var mSelectedIndex: Int = _
+ private var mValueSet: Boolean = _
+
+ 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)
+ })
+ setOnPreferenceClickListener((preference: Preference) => {
+ // TODO: not working with scrolling
+ // mSpinner.setDropDownVerticalOffset(Utils.dpToPx(getContext, -48 * mSelectedIndex).toInt)
+ mSpinner.performClick
+ true
+ })
+ initSummary(mContext, attrs)
+ val a: TypedArray = mContext.obtainStyledAttributes(attrs, R.styleable.DropDownPreference)
+ setEntries(a.getTextArray(R.styleable.DropDownPreference_entries))
+ mEntryValues = a.getTextArray(R.styleable.DropDownPreference_entryValues)
+ a.recycle
+
+ protected override def getSummaryValue = getEntry
+
+ /**
+ * Sets the human-readable entries to be shown in the list. This will be shown in subsequent dialogs.
+ *
+ * Each entry must have a corresponding index in [[setEntryValues(CharSequence[])]].
+ *
+ * @param entries The entries.
+ * @see [[setEntryValues(CharSequence[])]]
+ */
+ def setEntries(entries: Array[CharSequence]) {
+ mEntries = entries
+ mAdapter.clear
+ if (entries != null) for (entry <- entries) mAdapter.add(entry.toString)
+ }
+ /**
+ * @see #setEntries(CharSequence[])
+ * @param entriesResId The entries array as a resource.
+ */
+ def setEntries(entriesResId: Int): Unit = setEntries(getContext.getResources.getTextArray(entriesResId))
+ /**
+ * The list of entries to be shown in the list in subsequent dialogs.
+ *
+ * @return The list as an array.
+ */
+ def getEntries = mEntries
+
+ /**
+ * The array to find the value to save for a preference when an entry from
+ * entries is selected. If a user clicks on the second item in entries, the
+ * second item in this array will be saved to the preference.
+ *
+ * @param entryValues The array to be used as values to save for the preference.
+ */
+ def setEntryValues(entryValues: Array[CharSequence]) = mEntryValues = entryValues
+ /**
+ * @see #setEntryValues(CharSequence[])
+ * @param entryValuesResId The entry values array as a resource.
+ */
+ def setEntryValues(entryValuesResId: Int): Unit =
+ setEntryValues(getContext.getResources.getTextArray(entryValuesResId))
+ /**
+ * Returns the array of values to be saved for the preference.
+ *
+ * @return The array of values.
+ */
+ def getEntryValues: Array[CharSequence] = mEntryValues
+
+ /**
+ * 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)
+ }
+
+ /**
+ * Sets the value to the given index from the entry values.
+ *
+ * @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
+ persistString(value)
+ setValue(value)
+ }
+ if (changed) notifyChanged
+ }
+ mSpinner.setSelection(index)
+ }
+
+ /**
+ * Returns the value of the key. This should be one of the entries in [[getEntryValues]].
+ *
+ * @return The value of the key.
+ */
+ def getValue = if (mEntryValues == null) null else mEntryValues(mSelectedIndex).toString
+
+ /**
+ * Returns the entry corresponding to the current value.
+ *
+ * @return The entry corresponding to the current value, or null.
+ */
+ def getEntry = {
+ val index = getValueIndex
+ if (index >= 0 && mEntries != null) mEntries(index) else null
+ }
+
+ /**
+ * Returns the index of the given value (in the entry values array).
+ *
+ * @param value The value whose index should be returned.
+ * @return The index of the value, or -1 if not found.
+ */
+ def findIndexOfValue(value: String): Int = {
+ if (value != null && mEntryValues != null) {
+ var i = mEntryValues.length - 1
+ while (i >= 0) {
+ if (mEntryValues(i) == value) return i
+ i -= 1
+ }
+ }
+ -1
+ }
+
+ def getValueIndex = mSelectedIndex
+
+ protected override def onGetDefaultValue(a: TypedArray, index: Int) = a.getString(index)
+ protected override def onSetInitialValue (restoreValue: Boolean, defaultValue: AnyRef) =
+ setValue(if (restoreValue) getPersistedString(getValue) else defaultValue.asInstanceOf[String])
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ def setDropDownWidth(dimenResId: Int) =
+ mSpinner.setDropDownWidth(mContext.getResources.getDimensionPixelSize(dimenResId))
+
+ protected override def onBindView(@NonNull view: View) {
+ super.onBindView(view)
+ val parent = mSpinner.getParent.asInstanceOf[ViewGroup]
+ if (view eq parent) return
+ if (parent != null) parent.removeView(mSpinner)
+ view.asInstanceOf[ViewGroup].addView(mSpinner, 0)
+ val lp = mSpinner.getLayoutParams
+ lp.width = 0
+ mSpinner.setLayoutParams(lp)
+ }
+}
diff --git a/src/main/scala/com/github/shadowsocks/preferences/SummaryListPreference.scala b/src/main/scala/com/github/shadowsocks/preferences/SummaryListPreference.scala
deleted file mode 100644
index 4cf92bac..00000000
--- a/src/main/scala/com/github/shadowsocks/preferences/SummaryListPreference.scala
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Shadowsocks - A shadowsocks client for Android
- * Copyright (C) 2014
- *
- * 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 .
- *
- *
- * ___====-_ _-====___
- * _--^^^#####// \\#####^^^--_
- * _-^##########// ( ) \\##########^-_
- * -############// |\^^/| \\############-
- * _/############// (@::@) \\############\_
- * /#############(( \\// ))#############\
- * -###############\\ (oo) //###############-
- * -#################\\ / VV \ //#################-
- * -###################\\/ \//###################-
- * _#/|##########/\######( /\ )######/\##########|\#_
- * |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
- * ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
- * ` ` ` ` / | | | | \ ' ' ' '
- * ( | | | | )
- * __\ | | | | /__
- * (vvv(VVV)(VVV)vvv)
- *
- * HERE BE DRAGONS
- *
- */
-
-package com.github.shadowsocks.preferences
-
-import android.preference.ListPreference
-import android.content.Context
-import android.util.AttributeSet
-
-class SummaryListPreference(context: Context, attrs: AttributeSet)
- extends ListPreference(context, attrs) {
-
- override def setValue(text: String) {
- super.setValue(text)
- val entry = getEntry
- if (entry != null)
- setSummary(entry)
- else
- setSummary(text)
- }
-
- override def setSummary(summary: CharSequence) {
- if (summary == null || summary.toString.isEmpty) {
- super.setSummary("")
- } else {
- super.setSummary(summary)
- }
- }
-
-}
diff --git a/src/main/scala/com/github/shadowsocks/preferences/SummaryPreference.scala b/src/main/scala/com/github/shadowsocks/preferences/SummaryPreference.scala
new file mode 100644
index 00000000..56523494
--- /dev/null
+++ b/src/main/scala/com/github/shadowsocks/preferences/SummaryPreference.scala
@@ -0,0 +1,45 @@
+package com.github.shadowsocks.preferences
+
+import android.content.Context
+import android.preference.Preference
+import android.util.AttributeSet
+import com.github.shadowsocks.R
+
+/**
+ * Make your preference support %s in summary. Override getSummaryValue to customize what to put in.
+ * Based on: https://github.com/Mygod/mygod-lib-android/blob/eb20c57a0da70bb5dbc27104002f20fb1eb31d76/lib/src/main/scala/tk/mygod/preference/SummaryPreference.scala
+ * @author Mygod
+ */
+trait SummaryPreference extends Preference {
+ private var mSummary: String = _
+
+ protected def initSummary(context: Context, attrs: AttributeSet) {
+ val a = context.obtainStyledAttributes(attrs, R.styleable.SummaryPreference)
+ mSummary = a.getString(R.styleable.SummaryPreference_summary)
+ a.recycle
+ }
+
+ protected def getSummaryValue: AnyRef
+
+ /**
+ * Returns the summary of this SummaryPreference. If the summary has a String formatting marker in it
+ * (i.e. "%s" or "%1$s"), then the current entry value will be substituted in its place.
+ *
+ * @return the summary with appropriate string substitution
+ */
+ override def getSummary = {
+ val entry = getSummaryValue
+ if (mSummary == null || entry == null) super.getSummary else String.format(mSummary, entry)
+ }
+ /**
+ * Sets the summary for this Preference with a CharSequence. If the summary has a String formatting marker in it
+ * (i.e. "%s" or "%1$s"), then the current entry value will be substituted in its place when it's retrieved.
+ *
+ * @param summary The summary for the preference.
+ */
+ override def setSummary(summary: CharSequence) {
+ super.setSummary(summary)
+ if (summary == null && mSummary != null) mSummary = null
+ else if (summary != null && summary != mSummary) mSummary = summary.toString
+ }
+}
diff --git a/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/src/main/scala/com/github/shadowsocks/utils/Constants.scala
index 83ab3cfb..3d812e9b 100644
--- a/src/main/scala/com/github/shadowsocks/utils/Constants.scala
+++ b/src/main/scala/com/github/shadowsocks/utils/Constants.scala
@@ -66,7 +66,6 @@ object Key {
val proxied = "Proxyed"
val isNAT = "isNAT"
- val isRoot = "isRoot"
val status = "status"
val proxyedApps = "proxyedApps"
val route = "route"
diff --git a/src/main/scala/com/github/shadowsocks/utils/Utils.scala b/src/main/scala/com/github/shadowsocks/utils/Utils.scala
index e455effc..85b14a47 100644
--- a/src/main/scala/com/github/shadowsocks/utils/Utils.scala
+++ b/src/main/scala/com/github/shadowsocks/utils/Utils.scala
@@ -50,7 +50,10 @@ import android.graphics.drawable.{BitmapDrawable, Drawable}
import android.os.Build
import android.provider.Settings
import android.support.v4.content.ContextCompat
-import android.util.{Base64, Log}
+import android.util.{DisplayMetrics, Base64, Log}
+import android.view.View.MeasureSpec
+import android.view.{Gravity, View, Window}
+import android.widget.Toast
import com.github.shadowsocks.BuildConfig
import org.xbill.DNS._
@@ -72,20 +75,13 @@ object Utils {
val TIME_OUT: Int = -99
var initialized: Boolean = false
var hasRedirectSupport: Int = -1
- var isRoot: Int = -1
var shell: String = null
var root_shell: String = null
var iptables: String = null
var data_path: String = null
var rootTries = 0
- def isLollipopOrAbove: Boolean = {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- true
- } else {
- false
- }
- }
+ def isLollipopOrAbove: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
def getSignature(context: Context): String = {
val info = context
@@ -126,6 +122,23 @@ object Utils {
bitmap
}
+ // Based on: http://stackoverflow.com/a/21026866/2245107
+ def positionToast(toast: Toast, view: View, window: Window, offsetX: Int = 0, offsetY: Int = 0) = {
+ val rect = new Rect
+ window.getDecorView.getWindowVisibleDisplayFrame(rect)
+ val viewLocation = new Array[Int](2)
+ view.getLocationInWindow(viewLocation)
+ val metrics = new DisplayMetrics
+ window.getWindowManager.getDefaultDisplay.getMetrics(metrics)
+ val toastView = toast.getView
+ toastView.measure(MeasureSpec.makeMeasureSpec(metrics.widthPixels, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(metrics.heightPixels, MeasureSpec.UNSPECIFIED))
+ toast.setGravity(Gravity.LEFT | Gravity.TOP,
+ viewLocation(0) - rect.left + (view.getWidth - toast.getView.getMeasuredWidth) / 2 + offsetX,
+ viewLocation(1) - rect.top + view.getHeight + offsetY)
+ toast
+ }
+
// Blocked > 3 seconds
def toggleAirplaneMode(context: Context) {
if (Build.VERSION.SDK_INT >= 17) {