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 _ =>