diff --git a/src/main/res/values-zh/strings.xml b/src/main/res/values-zh/strings.xml
index 9db0cf6c..9015716b 100755
--- a/src/main/res/values-zh/strings.xml
+++ b/src/main/res/values-zh/strings.xml
@@ -32,10 +32,8 @@
启用该选项,以使所选应用程序的流量不经过代理
自动连接
随系统启动后台服务
- 流量统计
- 在通知栏上显示当前上下行速率
后台服务已开始运行。
- 已连接到「%s」
+ 影梭 → %s
无法连接远程服务器
停止服务
正在关闭……
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 02f32d08..422a0b3f 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -15,6 +15,7 @@
Network Traffic
Sent: \t\t\t\t\t%3$s\t↑\t%1$s/s\nReceived: \t%4$s\t↓\t%2$s/s
\n%1$s↑\t%2$s↓
+ %1$s/s↑\t%2$s/s↓
- Byte
- Bytes
@@ -54,14 +55,12 @@
Enable this option to bypass selected apps
Auto Connect
Enable Shadowsocks on startup
- Traffic Stats
- Show traffic stats in notification bar
UDP Forwarding
Forward UDP packets to remote
Shadowsocks started.
- Connected to "%s"
+ Shadowsocks → %s
Failed to connect the remote server
Stop the service
Shutting down…
diff --git a/src/main/scala/com/github/shadowsocks/BaseService.scala b/src/main/scala/com/github/shadowsocks/BaseService.scala
index bbd24e30..5b8a8314 100644
--- a/src/main/scala/com/github/shadowsocks/BaseService.scala
+++ b/src/main/scala/com/github/shadowsocks/BaseService.scala
@@ -41,14 +41,13 @@ package com.github.shadowsocks
import java.util.{Timer, TimerTask}
-import android.app.Notification
+import android.app.Service
import android.content.Context
import android.os.{Handler, RemoteCallbackList}
-import android.util.Log
import com.github.shadowsocks.aidl.{Config, IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.utils.{State, TrafficMonitor, TrafficMonitorThread}
-trait BaseService {
+trait BaseService extends Service {
@volatile private var state = State.INIT
@volatile private var callbackCount = 0
@@ -59,7 +58,7 @@ trait BaseService {
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
- protected val binder = new IShadowsocksService.Stub {
+ val binder = new IShadowsocksService.Stub {
override def getMode: Int = {
getServiceMode
}
@@ -197,8 +196,4 @@ trait BaseService {
state = s
})
}
-
- def initSoundVibrateLights(notification: Notification) {
- notification.sound = null
- }
}
diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
index aa6ecc75..d55fb902 100644
--- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
+++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala
@@ -346,7 +346,7 @@ class Shadowsocks
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_main)
- // Initialize action bar
+ // Initialize Toolbar
val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
toolbar.setTitle(getString(R.string.screen_name))
toolbar.setTitleTextAppearance(toolbar.getContext, R.style.Toolbar_Logo)
@@ -472,7 +472,8 @@ class Shadowsocks
}
private def updatePreferenceScreen() {
- if (ShadowsocksApplication.proxy == "198.199.101.152" && adView == null) {
+ val profile = currentProfile
+ if (profile.host == "198.199.101.152" && adView == null) {
adView = new AdView(this)
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
adView.setAdSize(AdSize.SMART_BANNER)
@@ -480,7 +481,6 @@ class Shadowsocks
adView.loadAd(new AdRequest.Builder().build())
}
- val profile = currentProfile
for (name <- Shadowsocks.PROXY_PREFS) {
val pref = preferences.findPreference(name)
Shadowsocks.updatePreference(pref, name, profile)
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
index eff4d808..7bd76fe6 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
@@ -78,7 +78,6 @@ object ShadowsocksApplication {
def profileId = settings.getInt(Key.profileId, -1)
def profileId(i: Int) = settings.edit.putInt(Key.profileId, i).apply
- def proxy = settings.getString(Key.proxy, "")
def currentProfile = profileManager.getProfile(profileId)
def switchProfile(id: Int) = {
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
index 75f7cca0..16bd2a42 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
@@ -44,13 +44,10 @@ import java.lang.Process
import java.net.{Inet6Address, InetAddress}
import java.util.Locale
-import android.app._
import android.content._
import android.content.pm.{PackageInfo, PackageManager}
import android.net.{ConnectivityManager, Network}
import android.os._
-import android.support.v4.app.NotificationCompat
-import android.support.v4.content.ContextCompat
import android.util.{Log, SparseArray}
import android.widget.Toast
import com.github.shadowsocks.aidl.Config
@@ -62,7 +59,7 @@ import scala.collection.mutable.ArrayBuffer
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
-class ShadowsocksNatService extends Service with BaseService {
+class ShadowsocksNatService extends BaseService {
val TAG = "ShadowsocksNatService"
@@ -70,7 +67,7 @@ class ShadowsocksNatService extends Service with BaseService {
val CMD_IPTABLES_DNAT_ADD_SOCKS = " -t nat -A OUTPUT -p tcp " +
"-j DNAT --to-destination 127.0.0.1:8123"
- var lockReceiver: BroadcastReceiver = null
+ private var notification: ShadowsocksNotification = _
var closeReceiver: BroadcastReceiver = null
var connReceiver: BroadcastReceiver = null
var apps: Array[ProxiedApp] = null
@@ -324,33 +321,6 @@ class ShadowsocksNatService extends Service with BaseService {
true
}
- def notifyForegroundAlert(title: String, info: String, visible: Boolean) {
- val openIntent = new Intent(this, classOf[Shadowsocks])
- openIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
- val contentIntent = PendingIntent.getActivity(this, 0, openIntent, 0)
- val closeIntent = new Intent(Action.CLOSE)
- val actionIntent = PendingIntent.getBroadcast(this, 0, closeIntent, 0)
- val builder = new NotificationCompat.Builder(this)
-
- builder
- .setWhen(0)
- .setColor(ContextCompat.getColor(this, R.color.material_accent_500))
- .setTicker(title)
- .setContentTitle(getString(R.string.app_name))
- .setContentText(info)
- .setContentIntent(contentIntent)
- .setSmallIcon(R.drawable.ic_stat_shadowsocks)
- .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.stop),
- actionIntent)
-
- if (visible)
- builder.setPriority(NotificationCompat.PRIORITY_DEFAULT)
- else
- builder.setPriority(NotificationCompat.PRIORITY_MIN)
-
- startForeground(1, builder.build)
- }
-
def onBind(intent: Intent): IBinder = {
Log.d(TAG, "onBind")
if (Action.SERVICE == intent.getAction) {
@@ -443,30 +413,6 @@ class ShadowsocksNatService extends Service with BaseService {
}
registerReceiver(closeReceiver, filter)
- if (Utils.isLollipopOrAbove) {
- val screenFilter = new IntentFilter()
- screenFilter.addAction(Intent.ACTION_SCREEN_ON)
- screenFilter.addAction(Intent.ACTION_SCREEN_OFF)
- screenFilter.addAction(Intent.ACTION_USER_PRESENT)
- lockReceiver = (context: Context, intent: Intent) => if (getState == State.CONNECTED) {
- val action = intent.getAction
- if (action == Intent.ACTION_SCREEN_OFF) {
- notifyForegroundAlert(getString(R.string.forward_success),
- getString(R.string.service_running).formatLocal(Locale.ENGLISH, config.profileName), false)
- } else if (action == Intent.ACTION_SCREEN_ON) {
- val keyGuard = getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
- if (!keyGuard.inKeyguardRestrictedInputMode) {
- notifyForegroundAlert(getString(R.string.forward_success),
- getString(R.string.service_running).formatLocal(Locale.ENGLISH, config.profileName), true)
- }
- } else if (action == Intent.ACTION_USER_PRESENT) {
- notifyForegroundAlert(getString(R.string.forward_success),
- getString(R.string.service_running).formatLocal(Locale.ENGLISH, config.profileName), true)
- }
- }
- registerReceiver(lockReceiver, screenFilter)
- }
-
ShadowsocksApplication.track(TAG, "start")
changeState(State.CONNECTING)
@@ -507,9 +453,8 @@ class ShadowsocksNatService extends Service with BaseService {
// Set DNS
flushDns()
- notifyForegroundAlert(getString(R.string.forward_success),
- getString(R.string.service_running).formatLocal(Locale.ENGLISH, config.profileName), true)
changeState(State.CONNECTED)
+ notification = new ShadowsocksNotification(this, config.profileName, true)
} else {
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner()
@@ -531,12 +476,7 @@ class ShadowsocksNatService extends Service with BaseService {
closeReceiver = null
}
- if (Utils.isLollipopOrAbove) {
- if (lockReceiver != null) {
- unregisterReceiver(lockReceiver)
- lockReceiver = null
- }
- }
+ notification.destroy()
ShadowsocksApplication.track(TAG, "stop")
@@ -548,8 +488,6 @@ class ShadowsocksNatService extends Service with BaseService {
stopSelf()
}
- stopForeground(true)
-
// change the state
changeState(State.STOPPED)
}
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala
new file mode 100644
index 00000000..7796b909
--- /dev/null
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNotification.scala
@@ -0,0 +1,83 @@
+package com.github.shadowsocks
+
+import java.util.Locale
+
+import android.app.{KeyguardManager, PendingIntent}
+import android.content.{BroadcastReceiver, Context, Intent, IntentFilter}
+import android.support.v4.app.NotificationCompat
+import android.support.v4.app.NotificationCompat.BigTextStyle
+import android.support.v4.content.ContextCompat
+import com.github.shadowsocks.aidl.IShadowsocksServiceCallback.Stub
+import com.github.shadowsocks.utils.{Action, State, Utils}
+
+/**
+ * @author Mygod
+ */
+class ShadowsocksNotification(private val service: BaseService, profileName: String, visible: Boolean = false) {
+ private lazy val keyGuard = service.getSystemService(Context.KEYGUARD_SERVICE).asInstanceOf[KeyguardManager]
+ private lazy val callback = new Stub {
+ override def stateChanged(state: Int, msg: String) = () // ignore
+ override def trafficUpdated(txRate: String, rxRate: String, txTotal: String, rxTotal: String) {
+ builder.setContentText(service.getString(R.string.traffic_summary).formatLocal(Locale.ENGLISH, txRate, rxRate))
+ style.bigText(service.getString(R.string.stat_summary)
+ .formatLocal(Locale.ENGLISH, txRate, rxRate, txTotal, rxTotal))
+ show()
+ }
+ }
+ private var lockReceiver: BroadcastReceiver = _
+ private var callbackRegistered: Boolean = true
+
+ private val builder = new NotificationCompat.Builder(service)
+ .setWhen(0)
+ .setColor(ContextCompat.getColor(service, R.color.material_accent_500))
+ .setTicker(service.getString(R.string.forward_success))
+ .setContentTitle(service.getString(R.string.service_running).formatLocal(Locale.ENGLISH, profileName))
+ .setContentIntent(PendingIntent.getActivity(service, 0, new Intent(service, classOf[Shadowsocks])
+ .setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
+ .setSmallIcon(R.drawable.ic_stat_shadowsocks)
+ .addAction(android.R.drawable.ic_menu_close_clear_cancel, service.getString(R.string.stop),
+ PendingIntent.getBroadcast(service, 0, new Intent(Action.CLOSE), 0))
+ private lazy val style = new BigTextStyle(builder)
+ private val showOnUnlock = visible && Utils.isLollipopOrAbove
+ update()
+ lockReceiver = (context: Context, intent: Intent) => update(intent.getAction)
+ val screenFilter = new IntentFilter()
+ screenFilter.addAction(Intent.ACTION_SCREEN_ON)
+ screenFilter.addAction(Intent.ACTION_SCREEN_OFF)
+ if (showOnUnlock) screenFilter.addAction(Intent.ACTION_USER_PRESENT)
+ service.registerReceiver(lockReceiver, screenFilter)
+
+ def update(action: String = Intent.ACTION_SCREEN_ON) = if (service.getState == State.CONNECTED) {
+ if (action == Intent.ACTION_SCREEN_OFF) {
+ if (showOnUnlock) setVisible(false)
+ unregisterCallback // unregister callback to save battery
+ } else if (action == Intent.ACTION_SCREEN_ON) {
+ service.binder.registerCallback(callback)
+ callbackRegistered = true
+ if (!keyGuard.inKeyguardRestrictedInputMode) setVisible(showOnUnlock)
+ } else if (action == Intent.ACTION_USER_PRESENT) setVisible(showOnUnlock)
+ show()
+ }
+
+ private def unregisterCallback = if (callbackRegistered) {
+ service.binder.unregisterCallback(callback)
+ callbackRegistered = false
+ }
+
+ def setVisible(visible: Boolean) = {
+ builder.setPriority(if (visible) NotificationCompat.PRIORITY_DEFAULT else NotificationCompat.PRIORITY_MIN)
+ this
+ }
+
+ def notification = if (callbackRegistered) style.build() else builder.build()
+ def show() = service.startForeground(1, notification)
+
+ def destroy() {
+ if (lockReceiver != null) {
+ service.unregisterReceiver(lockReceiver)
+ lockReceiver = null
+ }
+ unregisterCallback
+ service.stopForeground(true)
+ }
+}
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala
index f6e25db2..0831eee7 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksSettings.scala
@@ -2,7 +2,8 @@ package com.github.shadowsocks
import java.util.Locale
-import android.content.{DialogInterface, Intent}
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener
+import android.content.{DialogInterface, Intent, SharedPreferences}
import android.net.Uri
import android.os.{Build, Bundle}
import android.preference.{Preference, PreferenceFragment, SwitchPreference}
@@ -11,7 +12,7 @@ import android.webkit.{WebView, WebViewClient}
import com.github.shadowsocks.utils.Key
// TODO: Move related logic here
-class ShadowsocksSettings extends PreferenceFragment {
+class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChangeListener {
private lazy val activity = getActivity.asInstanceOf[Shadowsocks]
private var isProxyApps: SwitchPreference = _
@@ -19,6 +20,7 @@ class ShadowsocksSettings extends PreferenceFragment {
override def onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
addPreferencesFromResource(R.xml.pref_all)
+ getPreferenceManager.getSharedPreferences.registerOnSharedPreferenceChangeListener(this)
isProxyApps = findPreference(Key.isProxyApps).asInstanceOf[SwitchPreference]
isProxyApps.setOnPreferenceChangeListener((preference: Preference, newValue: Any) => {
@@ -26,12 +28,6 @@ class ShadowsocksSettings extends PreferenceFragment {
newValue.asInstanceOf[Boolean] // keep it ON
})
- findPreference(Key.isNAT).setOnPreferenceChangeListener((preference: Preference, newValue: Any) =>
- if (ShadowsocksApplication.isRoot) activity.handler.post(() => {
- activity.deattachService()
- activity.attachService()
- }) else false)
-
findPreference("recovery").setOnPreferenceClickListener((preference: Preference) => {
ShadowsocksApplication.track(Shadowsocks.TAG, "reset")
activity.recovery()
@@ -75,4 +71,12 @@ class ShadowsocksSettings extends PreferenceFragment {
super.onResume
isProxyApps.setChecked(ShadowsocksApplication.settings.getBoolean(Key.isProxyApps, false)) // update
}
+
+ def onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) = key match {
+ case Key.isNAT => if (ShadowsocksApplication.isRoot) activity.handler.post(() => {
+ activity.deattachService()
+ activity.attachService()
+ })
+ case _ =>
+ }
}
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
index 7c07e8e2..4a29ed6c 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
@@ -43,13 +43,10 @@ import java.io.File
import java.lang.Process
import java.util.Locale
-import android.app._
import android.content._
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
@@ -70,6 +67,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
var conn: ParcelFileDescriptor = null
var apps: Array[ProxiedApp] = null
var vpnThread: ShadowsocksVpnThread = null
+ private var notification: ShadowsocksNotification = _
var closeReceiver: BroadcastReceiver = null
var sslocalProcess: Process = null
@@ -108,32 +106,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
null
}
- def notifyForegroundAlert(title: String, info: String, visible: Boolean) {
- val openIntent = new Intent(this, classOf[Shadowsocks])
- val contentIntent = PendingIntent.getActivity(this, 0, openIntent, 0)
- val closeIntent = new Intent(Action.CLOSE)
- val actionIntent = PendingIntent.getBroadcast(this, 0, closeIntent, 0)
- val builder = new NotificationCompat.Builder(this)
-
- builder
- .setWhen(0)
- .setColor(ContextCompat.getColor(this, R.color.material_accent_500))
- .setTicker(title)
- .setContentTitle(getString(R.string.app_name))
- .setContentText(info)
- .setContentIntent(contentIntent)
- .setSmallIcon(R.drawable.ic_stat_shadowsocks)
- .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.stop),
- actionIntent)
-
- if (visible)
- builder.setPriority(NotificationCompat.PRIORITY_DEFAULT)
- else
- builder.setPriority(NotificationCompat.PRIORITY_MIN)
-
- startForeground(1, builder.build)
- }
-
override def onCreate() {
super.onCreate()
ConfigUtils.refresh(this)
@@ -152,7 +124,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
vpnThread = null
}
- stopForeground(true)
+ notification.destroy()
// channge the state
changeState(State.STOPPING)
@@ -275,9 +247,8 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
if (resolved && handleConnection) {
- notifyForegroundAlert(getString(R.string.forward_success),
- getString(R.string.service_running).formatLocal(Locale.ENGLISH, config.profileName), false)
changeState(State.CONNECTED)
+ notification = new ShadowsocksNotification(this, config.profileName)
} else {
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner()
diff --git a/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala b/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala
index bf01d1e3..0cf4d8e5 100644
--- a/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala
+++ b/src/main/scala/com/github/shadowsocks/database/ProfileManager.scala
@@ -59,6 +59,7 @@ class ProfileManager(settings: SharedPreferences, dbHelper: DBHelper) {
profile.route = oldProfile.route
profile.ipv6 = oldProfile.ipv6
profile.proxyApps = oldProfile.proxyApps
+ profile.bypass = oldProfile.bypass
profile.individual = oldProfile.individual
profile.udpdns = oldProfile.udpdns
case _ =>