@@ -32,10 +32,8 @@
|
||||
<string name="bypass_apps_summary">启用该选项,以使所选应用程序的流量不经过代理</string>
|
||||
<string name="auto_connect">自动连接</string>
|
||||
<string name="auto_connect_summary">随系统启动后台服务</string>
|
||||
<string name="traffic_stat">流量统计</string>
|
||||
<string name="traffic_stat_summary">在通知栏上显示当前上下行速率</string>
|
||||
<string name="forward_success">后台服务已开始运行。</string>
|
||||
<string name="service_running">已连接到「%s」</string>
|
||||
<string name="service_running">影梭 → %s</string>
|
||||
<string name="service_failed">无法连接远程服务器</string>
|
||||
<string name="stop">停止服务</string>
|
||||
<string name="stopping">正在关闭……</string>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<string name="stat">Network Traffic</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="traffic_summary">%1$s/s↑\t%2$s/s↓</string>
|
||||
<plurals name="bytes">
|
||||
<item quantity="one">Byte</item>
|
||||
<item quantity="other">Bytes</item>
|
||||
@@ -54,14 +55,12 @@
|
||||
<string name="bypass_apps_summary">Enable this option to bypass selected apps</string>
|
||||
<string name="auto_connect">Auto Connect</string>
|
||||
<string name="auto_connect_summary">Enable Shadowsocks on startup</string>
|
||||
<string name="traffic_stat">Traffic Stats</string>
|
||||
<string name="traffic_stat_summary">Show traffic stats in notification bar</string>
|
||||
<string name="udp_dns">UDP Forwarding</string>
|
||||
<string name="udp_dns_summary">Forward UDP packets to remote</string>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="forward_success">Shadowsocks started.</string>
|
||||
<string name="service_running">Connected to "%s"</string>
|
||||
<string name="service_running">Shadowsocks → %s</string>
|
||||
<string name="service_failed">Failed to connect the remote server</string>
|
||||
<string name="stop">Stop the service</string>
|
||||
<string name="stopping">Shutting down…</string>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) = {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 _ =>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 _ =>
|
||||
|
||||
Reference in New Issue
Block a user