diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 21d49d7c..513a1396 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -70,17 +70,22 @@ + android:exported="false"> + + + + + diff --git a/src/main/java/com/github/shadowsocks/aidl/Config.aidl b/src/main/java/com/github/shadowsocks/aidl/Config.aidl new file mode 100644 index 00000000..667e8d16 --- /dev/null +++ b/src/main/java/com/github/shadowsocks/aidl/Config.aidl @@ -0,0 +1,3 @@ +package com.github.shadowsocks.aidl; + +parcelable Config; \ No newline at end of file diff --git a/src/main/java/com/github/shadowsocks/aidl/Config.java b/src/main/java/com/github/shadowsocks/aidl/Config.java new file mode 100644 index 00000000..cac52c3c --- /dev/null +++ b/src/main/java/com/github/shadowsocks/aidl/Config.java @@ -0,0 +1,86 @@ +package com.github.shadowsocks.aidl; + +import android.os.Parcel; +import android.os.Parcelable; + +public class Config implements Parcelable { + + public boolean isGlobalProxy = true; + public boolean isGFWList = true; + public boolean isBypassApps = false; + public boolean isTrafficStat = false; + + public String profileName = "Untitled"; + public String proxy = "127.0.0.1"; + public String sitekey = "null"; + + public String encMethod = "rc4"; + public String proxiedAppString = ""; + + public int remotePort = 1984; + public int localPort = 1080; + + public static final Parcelable.Creator CREATOR = new + Parcelable.Creator() { + public Config createFromParcel(Parcel in) { + return new Config(in); + } + + public Config[] newArray(int size) { + return new Config[size]; + } + }; + + public Config(boolean isGlobalProxy, boolean isGFWList, boolean isBypassApps, + boolean isTrafficStat, String profileName, String proxy, String sitekey, String encMethod, + String proxiedAppString, int remotePort, int localPort) { + this.isGlobalProxy = isGlobalProxy; + this.isGFWList = isGFWList; + this.isBypassApps = isBypassApps; + this.isTrafficStat = isTrafficStat; + this.profileName = profileName; + this.proxy = proxy; + this.sitekey = sitekey; + this.encMethod = encMethod; + this.proxiedAppString = proxiedAppString; + this.remotePort = remotePort; + this.localPort = localPort; + } + + private Config(Parcel in) { + readFromParcel(in); + } + + public void readFromParcel(Parcel in) { + isGlobalProxy = in.readInt() == 1; + isGFWList = in.readInt() == 1; + isBypassApps = in.readInt() == 1; + isTrafficStat = in.readInt() == 1; + profileName = in.readString(); + proxy = in.readString(); + sitekey = in.readString(); + encMethod = in.readString(); + proxiedAppString = in.readString(); + remotePort = in.readInt(); + localPort = in.readInt(); + } + + @Override public int describeContents() { + return 0; + } + + @Override public void writeToParcel(Parcel out, int flags) { + out.writeInt(isGlobalProxy ? 1: 0); + out.writeInt(isGFWList ? 1: 0); + out.writeInt(isBypassApps ? 1: 0); + out.writeInt(isTrafficStat ? 1: 0); + out.writeString(profileName); + out.writeString(proxy); + out.writeString(sitekey); + out.writeString(encMethod); + out.writeString(proxiedAppString); + out.writeInt(remotePort); + out.writeInt(localPort); + + } +} diff --git a/src/main/java/com/github/shadowsocks/aidl/IShadowsocksService.aidl b/src/main/java/com/github/shadowsocks/aidl/IShadowsocksService.aidl new file mode 100644 index 00000000..c06de025 --- /dev/null +++ b/src/main/java/com/github/shadowsocks/aidl/IShadowsocksService.aidl @@ -0,0 +1,13 @@ +package com.github.shadowsocks.aidl; + +import com.github.shadowsocks.aidl.Config; +import com.github.shadowsocks.aidl.IShadowsocksServiceCallback; + +interface IShadowsocksService { + int getMode(); + int getState(); + void start(in Config config); + void stop(); + void registerCallback(IShadowsocksServiceCallback cb); + void unregisterCallback(IShadowsocksServiceCallback cb); +} \ No newline at end of file diff --git a/src/main/java/com/github/shadowsocks/aidl/IShadowsocksServiceCallback.aidl b/src/main/java/com/github/shadowsocks/aidl/IShadowsocksServiceCallback.aidl new file mode 100644 index 00000000..00ef9b63 --- /dev/null +++ b/src/main/java/com/github/shadowsocks/aidl/IShadowsocksServiceCallback.aidl @@ -0,0 +1,5 @@ +package com.github.shadowsocks.aidl; + +interface IShadowsocksServiceCallback { + void stateChanged(int state, String msg); +} \ No newline at end of file diff --git a/src/main/res/layout/layout_switch.xml b/src/main/res/layout/layout_switch.xml index 97bfcf70..2be5096b 100644 --- a/src/main/res/layout/layout_switch.xml +++ b/src/main/res/layout/layout_switch.xml @@ -17,6 +17,7 @@ + Log.e(getTag, "Cannot open pid file: " + name) + case e: IOException => + Log.e(getTag, "Cannot read pid file: " + name) + case e: NumberFormatException => + Log.e(getTag, "Invalid pid", e) + } + -1 + } + + def initSoundVibrateLights(notification: Notification) { + notification.sound = null + } +} diff --git a/src/main/scala/com/github/shadowsocks/ShadowVpnActivity.scala b/src/main/scala/com/github/shadowsocks/ShadowVpnActivity.scala index 25fc9944..a2e9940a 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowVpnActivity.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowVpnActivity.scala @@ -40,16 +40,28 @@ package com.github.shadowsocks import android.app.Activity -import android.os.Bundle +import android.os.{IBinder, Bundle} import android.net.VpnService -import android.content.Intent +import android.content.{ComponentName, ServiceConnection, Intent} import android.util.Log import android.preference.PreferenceManager -import com.github.shadowsocks.utils.Extra -import com.actionbarsherlock.app.SherlockActivity +import com.github.shadowsocks.utils._ +import com.github.shadowsocks.aidl.IShadowsocksService class ShadowVpnActivity extends Activity { + // Services + var bgService: IShadowsocksService = null + val connection = new ServiceConnection { + override def onServiceConnected(name: ComponentName, service: IBinder) { + bgService = IShadowsocksService.Stub.asInterface(service) + } + + override def onServiceDisconnected(name: ComponentName) { + bgService = null + } + } + override def onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) val intent = VpnService.prepare(this) @@ -62,14 +74,12 @@ class ShadowVpnActivity extends Activity { override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { resultCode match { - case Activity.RESULT_OK => { - val intent: Intent = new Intent(this, classOf[ShadowVpnService]) - Extra.put(PreferenceManager.getDefaultSharedPreferences(this), intent) - startService(intent) - } - case _ => { + case Activity.RESULT_OK => + if (bgService != null) { + bgService.start(ConfigUtils.load(PreferenceManager.getDefaultSharedPreferences(this))) + } + case _ => Log.e(Shadowsocks.TAG, "Failed to start VpnService") - } } finish() } diff --git a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala index 74a7c8ec..7993f8db 100644 --- a/src/main/scala/com/github/shadowsocks/Shadowsocks.scala +++ b/src/main/scala/com/github/shadowsocks/Shadowsocks.scala @@ -67,12 +67,11 @@ import scala.collection.mutable.ListBuffer import com.github.shadowsocks.database.Profile import com.nostra13.universalimageloader.core.download.BaseImageDownloader import com.github.shadowsocks.preferences.{ProfileEditTextPreference, PasswordEditTextPreference, SummaryEditTextPreference} -import com.github.shadowsocks.database.Item -import com.github.shadowsocks.database.Category import com.github.shadowsocks.utils._ import com.github.shadowsocks.database.Item import com.github.shadowsocks.database.Category import com.google.zxing.integration.android.IntentIntegrator +import com.github.shadowsocks.aidl.{IShadowsocksServiceCallback, IShadowsocksService} class ProfileIconDownloader(context: Context, connectTimeout: Int, readTimeout: Int) extends BaseImageDownloader(context, connectTimeout, readTimeout) { @@ -103,10 +102,9 @@ object Typefaces { val t: Typeface = Typeface.createFromAsset(c.getAssets, assetPath) cache.put(assetPath, t) } catch { - case e: Exception => { + case e: Exception => Log.e(TAG, "Could not get typeface '" + assetPath + "' because " + e.getMessage) return null - } } } return cache.get(assetPath) @@ -131,8 +129,7 @@ object Shadowsocks { // Flags var vpnEnabled = -1 - // Help functions - + // Helper functions def updateListPreference(pref: Preference, value: String) { pref.setSummary(value) pref.asInstanceOf[ListPreference].setValue(value) @@ -172,10 +169,6 @@ object Shadowsocks { } } - def isServiceStarted(context: Context): Boolean = { - ShadowsocksService.isServiceStarted(context) || ShadowVpnService.isServiceStarted(context) - } - class ProxyFragment extends UnifiedPreferenceFragment { var receiver: BroadcastReceiver = null @@ -194,8 +187,8 @@ object Shadowsocks { private def updatePreferenceScreen() { for (name <- Shadowsocks.PROXY_PREFS) { val pref = findPreference(name) - Shadowsocks.updatePreference(pref, name, - getActivity.asInstanceOf[Shadowsocks].currentProfile) + Shadowsocks + .updatePreference(pref, name, getActivity.asInstanceOf[Shadowsocks].currentProfile) } } @@ -252,8 +245,8 @@ object Shadowsocks { private def updatePreferenceScreen() { for (name <- Shadowsocks.FEATRUE_PREFS) { val pref = findPreference(name) - Shadowsocks.updatePreference(pref, name, - getActivity.asInstanceOf[Shadowsocks].currentProfile) + Shadowsocks + .updatePreference(pref, name, getActivity.asInstanceOf[Shadowsocks].currentProfile) } } @@ -304,9 +297,61 @@ class Shadowsocks var prepared = false var currentProfile = new Profile + // Services + var currentServiceName = classOf[ShadowsocksNatService].getName + var bgService: IShadowsocksService = null + val callback = new IShadowsocksServiceCallback.Stub { + override def stateChanged(state: Int, msg: String) { + onStateChanged(state, msg) + } + } + val connection = new ServiceConnection { + override def onServiceConnected(name: ComponentName, service: IBinder) { + // Initialize the background service + bgService = IShadowsocksService.Stub.asInterface(service) + try { + bgService.registerCallback(callback) + } catch { + case ignored: RemoteException => // Nothing + } + // Update the UI + switchButton.setEnabled(true) + if (State.isAvailable(bgService.getState)) { + if (status.getBoolean(Key.isRunning, false)) { + spawn { + crash_recovery() + handler.sendEmptyMessage(MSG_CRASH_RECOVER) + } + } + Crouton.cancelAllCroutons() + onStateChanged(State.STOPPED, null) + setPreferenceEnabled(enabled = true) + } else { + if (bgService.getMode == Mode.VPN) { + val style = new Style.Builder().setBackgroundColorValue(Style.holoBlueLight).build() + val config = new Configuration.Builder().setDuration(Configuration.DURATION_LONG).build() + Crouton + .makeText(Shadowsocks.this, R.string.vpn_status, style) + .setConfiguration(config) + .show() + switchButton.setEnabled(false) + } + switchButton.setChecked(true) + onStateChanged(State.CONNECTED, null) + setPreferenceEnabled(enabled = false) + } + // set the listener + switchButton.setOnCheckedChangeListener(Shadowsocks.this) + } + + override def onServiceDisconnected(name: ComponentName) { + switchButton.setEnabled(false) + bgService = null + } + } + lazy val settings = PreferenceManager.getDefaultSharedPreferences(this) lazy val status = getSharedPreferences(Key.status, Context.MODE_PRIVATE) - lazy val stateReceiver = new StateBroadcastReceiver lazy val preferenceReceiver = new PreferenceBroadcastReceiver lazy val drawer = MenuDrawer.attach(this) lazy val menuAdapter = new MenuAdapter(this, getMenuList) @@ -341,9 +386,8 @@ class Shadowsocks try { files = assetManager.list(path) } catch { - case e: IOException => { + case e: IOException => Log.e(Shadowsocks.TAG, e.getMessage) - } } if (files != null) { for (file <- files) { @@ -363,9 +407,8 @@ class Shadowsocks out.close() out = null } catch { - case ex: Exception => { + case ex: Exception => Log.e(Shadowsocks.TAG, ex.getMessage) - } } } } @@ -409,9 +452,8 @@ class Shadowsocks val pi: PackageInfo = getPackageManager.getPackageInfo(getPackageName, 0) version = pi.versionName } catch { - case e: PackageManager.NameNotFoundException => { + case e: PackageManager.NameNotFoundException => version = "Package name not found" - } } version } @@ -445,12 +487,10 @@ class Shadowsocks def onCheckedChanged(compoundButton: CompoundButton, checked: Boolean) { if (compoundButton eq switchButton) { checked match { - case true => { + case true => prepareStartService() - } - case false => { + case false => serviceStop() - } } } } @@ -494,15 +534,26 @@ class Shadowsocks /** Called when the activity is first created. */ override def onCreate(savedInstanceState: Bundle) { - // Initialize preference + // Initialize the preference setHeaderRes(R.xml.shadowsocks_headers) super.onCreate(savedInstanceState) - // Initialize profile + // Initialize the profile currentProfile = { profileManager.getProfile(settings.getInt(Key.profileId, -1)) getOrElse currentProfile } + // Update the profile + if (!status.getBoolean(getVersionName, false)) { + val h = showProgress(getString(R.string.initializing)) + status.edit.putBoolean(getVersionName, true).apply() + spawn { + reset() + currentProfile = profileManager.create() + h.sendEmptyMessage(0) + } + } + // Initialize drawer menuAdapter.setActiveId(settings.getInt(Key.profileId, -1)) menuAdapter.setListener(this) @@ -527,23 +578,19 @@ class Shadowsocks getSupportActionBar.setIcon(R.drawable.ic_stat_shadowsocks) // Register broadcast receiver - registerReceiver(stateReceiver, new IntentFilter(Action.UPDATE_STATE)) registerReceiver(preferenceReceiver, new IntentFilter(Action.UPDATE_PREFS)) - // Update status - if (!Shadowsocks.isServiceStarted(this)) { - spawn { - status.edit.putBoolean(Key.isRoot, Utils.getRoot).apply() - } - if (!status.getBoolean(getVersionName, false)) { - val h = showProgress(getString(R.string.initializing)) - status.edit.putBoolean(getVersionName, true).apply() - spawn { - reset() - currentProfile = profileManager.create() - h.sendEmptyMessage(0) + // Bind to the service + spawn { + status.edit.putBoolean(Key.isRoot, Utils.getRoot).apply() + handler.post(new Runnable { + override def run() { + val isRoot = status.getBoolean(Key.isRoot, false) + val s = if (isRoot) classOf[ShadowsocksNatService] else classOf[ShadowsocksVpnService] + bindService(new Intent(s.getName), connection, Context.BIND_AUTO_CREATE) + startService(new Intent(Shadowsocks.this, s)) } - } + }) } } @@ -571,15 +618,15 @@ class Shadowsocks drawer.setActiveView(v, pos) } - def newProfile(id: Int) { val builder = new AlertDialog.Builder(this) - builder.setTitle(R.string.add_profile) - .setItems(R.array.add_profile_methods, new DialogInterface.OnClickListener() { + builder + .setTitle(R.string.add_profile) + .setItems(R.array.add_profile_methods, new DialogInterface.OnClickListener() { def onClick(dialog: DialogInterface, which: Int) { which match { - case 0 => { + case 0 => dialog.dismiss() val h = showProgress(getString(R.string.loading)) h.postDelayed(new Runnable() { @@ -589,17 +636,14 @@ class Shadowsocks h.sendEmptyMessage(0) } }, 600) - } - case 1 => { + case 1 => dialog.dismiss() addProfile(id) - } case _ => } } }) builder.create().show() - } def addProfile(profile: Profile) { @@ -737,7 +781,7 @@ class Shadowsocks EasyTracker .getInstance(this) .send( - MapBuilder.createEvent(Shadowsocks.TAG, "flush_dnscache", getVersionName, null).build()) + MapBuilder.createEvent(Shadowsocks.TAG, "flush_dnscache", getVersionName, null).build()) flushDnsCache() }) @@ -753,10 +797,9 @@ class Shadowsocks override def onOptionsItemSelected(item: com.actionbarsherlock.view.MenuItem): Boolean = { item.getItemId match { - case android.R.id.home => { + case android.R.id.home => drawer.toggleMenu() return true - } } super.onOptionsItemSelected(item) } @@ -768,38 +811,7 @@ class Shadowsocks protected override def onResume() { super.onResume() - if (!prepared) { - if (Shadowsocks.isServiceStarted(this)) { - switchButton.setChecked(true) - if (ShadowVpnService.isServiceStarted(this)) { - val style = new Style.Builder().setBackgroundColorValue(Style.holoBlueLight).build() - val config = new Configuration.Builder().setDuration(Configuration.DURATION_LONG).build() - switchButton.setEnabled(false) - Crouton - .makeText(Shadowsocks.this, R.string.vpn_status, style) - .setConfiguration(config) - .show() - } - setPreferenceEnabled(enabled = false) - onStateChanged(State.CONNECTED, null) - } else { - switchButton.setEnabled(true) - switchButton.setChecked(false) - Crouton.cancelAllCroutons() - setPreferenceEnabled(enabled = true) - if (status.getBoolean(Key.isRunning, false)) { - spawn { - crash_recovery() - handler.sendEmptyMessage(MSG_CRASH_RECOVER) - } - } - onStateChanged(State.STOPPED, null) - } - } - - switchButton.setOnCheckedChangeListener(this) - - Config.refresh(this) + ConfigUtils.refresh(this) } private def setPreferenceEnabled(enabled: Boolean) { @@ -848,8 +860,11 @@ class Shadowsocks override def onDestroy() { super.onDestroy() + if (bgService != null) { + bgService.unregisterCallback(callback) + unbindService(connection) + } Crouton.cancelAllCroutons() - unregisterReceiver(stateReceiver) unregisterReceiver(preferenceReceiver) new BackupManager(this).dataChanged() } @@ -891,16 +906,14 @@ class Shadowsocks } } else { resultCode match { - case Activity.RESULT_OK => { + case Activity.RESULT_OK => prepared = true if (!serviceStart) { switchButton.setChecked(false) } - } - case _ => { + case _ => clearDialog() Log.e(Shadowsocks.TAG, "Failed to start VpnService") - } } } } @@ -918,7 +931,7 @@ class Shadowsocks } def serviceStop() { - sendBroadcast(new Intent(Action.CLOSE)) + if (bgService != null) bgService.stop() } /** Called when connect button is clicked. */ @@ -935,26 +948,20 @@ class Shadowsocks return false } } catch { - case ex: Exception => { + case ex: Exception => this.showDialog(getString(R.string.port_alert)) return false - } } + if (bgService == null) return false + + bgService.start(ConfigUtils.load(settings)) + if (isVpnEnabled) { - if (ShadowVpnService.isServiceStarted(this)) return false - val intent: Intent = new Intent(this, classOf[ShadowVpnService]) - Extra.put(settings, intent) - startService(intent) val style = new Style.Builder().setBackgroundColorValue(Style.holoBlueLight).build() val config = new Configuration.Builder().setDuration(Configuration.DURATION_LONG).build() Crouton.makeText(Shadowsocks.this, R.string.vpn_status, style).setConfiguration(config).show() switchButton.setEnabled(false) - } else { - if (ShadowsocksService.isServiceStarted(this)) return false - val intent: Intent = new Intent(this, classOf[ShadowsocksService]) - Extra.put(settings, intent) - startService(intent) } true } @@ -974,9 +981,8 @@ class Shadowsocks try { versionName = getPackageManager.getPackageInfo(getPackageName, 0).versionName } catch { - case ex: PackageManager.NameNotFoundException => { + case ex: PackageManager.NameNotFoundException => versionName = "" - } } new AlertDialog.Builder(this) @@ -1014,44 +1020,45 @@ class Shadowsocks } def onStateChanged(s: Int, m: String) { - if (state != s) { - state = s - state match { - case State.CONNECTING => { - if (progressDialog == null) { - progressDialog = ProgressDialog - .show(Shadowsocks.this, "", getString(R.string.connecting), true, true) + handler.post(new Runnable { + override def run() { + if (state != s) { + state = s + state match { + case State.CONNECTING => + if (progressDialog == null) { + progressDialog = ProgressDialog + .show(Shadowsocks.this, "", getString(R.string.connecting), true, true) + } + setPreferenceEnabled(enabled = false) + case State.CONNECTED => + clearDialog() + if (!switchButton.isChecked) switchButton.setChecked(true) + setPreferenceEnabled(enabled = false) + case State.STOPPED => + clearDialog() + if (switchButton.isChecked) { + switchButton.setEnabled(true) + switchButton.setChecked(false) + Crouton.cancelAllCroutons() + } + if (m != null) { + Crouton.cancelAllCroutons() + val style = new Style.Builder().setBackgroundColorValue(Style.holoRedLight).build() + val config = new Configuration.Builder() + .setDuration(Configuration.DURATION_LONG) + .build() + Crouton + .makeText(Shadowsocks.this, getString(R.string.vpn_error).format(m), style) + .setConfiguration(config) + .show() + } + setPreferenceEnabled(enabled = true) } - setPreferenceEnabled(enabled = false) - } - case State.CONNECTED => { - clearDialog() - if (!switchButton.isChecked) switchButton.setChecked(true) - setPreferenceEnabled(enabled = false) - } - case State.STOPPED => { - clearDialog() - if (switchButton.isChecked) { - switchButton.setEnabled(true) - switchButton.setChecked(false) - Crouton.cancelAllCroutons() - } - if (m != null) { - Crouton.cancelAllCroutons() - val style = new Style.Builder().setBackgroundColorValue(Style.holoRedLight).build() - val config = new Configuration.Builder() - .setDuration(Configuration.DURATION_LONG) - .build() - Crouton - .makeText(Shadowsocks.this, getString(R.string.vpn_error).format(m), style) - .setConfiguration(config) - .show() - } - setPreferenceEnabled(enabled = true) + if (!isSinglePane) sendBroadcast(new Intent(Action.UPDATE_FRAGMENT)) } } - if (!isSinglePane) sendBroadcast(new Intent(Action.UPDATE_FRAGMENT)) - } + }) } class PreferenceBroadcastReceiver extends BroadcastReceiver { @@ -1061,12 +1068,4 @@ class Shadowsocks } } - class StateBroadcastReceiver extends BroadcastReceiver { - override def onReceive(context: Context, intent: Intent) { - val state = intent.getIntExtra(Extra.STATE, State.INIT) - val message = intent.getStringExtra(Extra.MESSAGE) - onStateChanged(state, message) - } - } - } diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala similarity index 76% rename from src/main/scala/com/github/shadowsocks/ShadowsocksService.scala rename to src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala index b0a2943a..7a826ebf 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala @@ -57,22 +57,16 @@ import org.apache.http.conn.util.InetAddressUtils import scala.collection._ import java.util.{TimerTask, Timer} import android.net.TrafficStats -import scala.concurrent.ops._ import com.github.shadowsocks.utils._ import scala.Some import android.graphics.Color +import com.github.shadowsocks.aidl.Config case class TrafficStat(tx: Long, rx: Long, timestamp: Long) -object ShadowsocksService { - def isServiceStarted(context: Context): Boolean = { - Utils.isServiceStarted("com.github.shadowsocks.ShadowsocksService", context) - } -} +class ShadowsocksNatService extends Service with BaseService { -class ShadowsocksService extends Service { - - val TAG = "ShadowsocksService" + val TAG = "ShadowsocksNatService" val CMD_IPTABLES_RETURN = " -t nat -A OUTPUT -p tcp -d 0.0.0.0 -j RETURN\n" val CMD_IPTABLES_REDIRECT_ADD_SOCKS = " -t nat -A OUTPUT -p tcp " + "-j REDIRECT --to 8123\n" @@ -80,12 +74,6 @@ class ShadowsocksService extends Service { "-j DNAT --to-destination 127.0.0.1:8123\n" val DNS_PORT = 8153 - val MSG_CONNECT_FINISH = 1 - val MSG_CONNECT_SUCCESS = 2 - val MSG_CONNECT_FAIL = 3 - val MSG_STOP_SELF = 4 - val MSG_VPN_ERROR = 5 - private val mStartForegroundSignature = Array[Class[_]](classOf[Int], classOf[Notification]) private val mStopForegroundSignature = Array[Class[_]](classOf[Boolean]) private val mSetForegroundSignature = Array[Class[_]](classOf[Boolean]) @@ -104,56 +92,25 @@ class ShadowsocksService extends Service { private var mStartForegroundArgs = new Array[AnyRef](2) private var mStopForegroundArgs = new Array[AnyRef](1) - private var state = State.INIT private var last: TrafficStat = null private var lastTxRate = 0 private var lastRxRate = 0 private var timer: Timer = null private val TIMER_INTERVAL = 2 - private def changeState(s: Int) { - if (state != s) { - state = s - val intent = new Intent(Action.UPDATE_STATE) - intent.putExtra(Extra.STATE, state) - sendBroadcast(intent) - } - } - val handler: Handler = new Handler { override def handleMessage(msg: Message) { msg.what match { - case MSG_CONNECT_SUCCESS => + case Msg.CONNECT_SUCCESS => changeState(State.CONNECTED) - case MSG_CONNECT_FAIL => + case Msg.CONNECT_FAIL => changeState(State.STOPPED) - case MSG_STOP_SELF => - stopSelf() case _ => } super.handleMessage(msg) } } - def getPid(name: String): Int = { - try { - val reader: BufferedReader = new BufferedReader(new FileReader(Path.BASE + name + ".pid")) - val line = reader.readLine - return Integer.valueOf(line) - } catch { - case e: FileNotFoundException => { - Log.e(TAG, "Cannot open pid file: " + name) - } - case e: IOException => { - Log.e(TAG, "Cannot read pid file: " + name) - } - case e: NumberFormatException => { - Log.e(TAG, "Invalid pid", e) - } - } - -1 - } - def startShadowsocksDaemon() { val cmd: String = (Path.BASE + "shadowsocks -b 127.0.0.1 -s \"%s\" -p \"%d\" -l \"%d\" -k \"%s\" -m \"%s\" -f " + @@ -165,8 +122,8 @@ class ShadowsocksService extends Service { def startDnsDaemon() { val cmd: String = Path.BASE + "pdnsd -c " + Path.BASE + "pdnsd.conf" - val conf: String = Config.PDNSD.format("127.0.0.1") - Config.printToFile(new File(Path.BASE + "pdnsd.conf"))(p => { + val conf: String = ConfigUtils.PDNSD.format("127.0.0.1") + ConfigUtils.printToFile(new File(Path.BASE + "pdnsd.conf"))(p => { p.println(conf) }) Utils.runCommand(cmd) @@ -178,101 +135,17 @@ class ShadowsocksService extends Service { val pi: PackageInfo = getPackageManager.getPackageInfo(getPackageName, 0) version = pi.versionName } catch { - case e: PackageManager.NameNotFoundException => { + case e: PackageManager.NameNotFoundException => version = "Package name not found" - } } version } - def handleCommand(intent: Intent) { - if (intent == null) { - stopSelf() - return - } - - changeState(State.CONNECTING) - - config = Extra.get(intent) - - if (config.isTrafficStat) { - // initialize timer - val task = new TimerTask { - def run() { - val pm = getSystemService(Context.POWER_SERVICE).asInstanceOf[PowerManager] - val now = new TrafficStat(TrafficStats.getUidTxBytes(uid), - TrafficStats.getUidRxBytes(uid), java.lang.System.currentTimeMillis()) - val txRate = ((now.tx - last.tx) / 1024 / TIMER_INTERVAL).toInt - val rxRate = ((now.rx - last.rx) / 1024 / TIMER_INTERVAL).toInt - last = now - if (lastTxRate == txRate && lastRxRate == rxRate) { - return - } else { - lastTxRate = txRate - lastRxRate = rxRate - } - if ((pm.isScreenOn && state == State.CONNECTED) || (txRate == 0 && rxRate == 0)) { - notifyForegroundAlert(getString(R.string.forward_success), - getString(R.string.service_status).format(math.max(txRate, rxRate)), math.max(txRate, rxRate)) - } - } - } - last = new TrafficStat(TrafficStats.getUidTxBytes(uid), - TrafficStats.getUidRxBytes(uid), java.lang.System.currentTimeMillis()) - timer = new Timer(true) - timer.schedule(task, TIMER_INTERVAL * 1000, TIMER_INTERVAL * 1000) - } - - spawn { - - if (config.proxy == "198.199.101.152") { - val container = getApplication.asInstanceOf[ShadowsocksApplication].tagContainer - try { - config = Config.getPublicConfig(getBaseContext, container, config) - } catch { - case ex: Exception => { - notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed)) - stopSelf() - handler.sendEmptyMessageDelayed(MSG_CONNECT_FAIL, 500) - return - } - } - } - - killProcesses() - - var resolved: Boolean = false - if (!InetAddressUtils.isIPv4Address(config.proxy) && - !InetAddressUtils.isIPv6Address(config.proxy)) { - Utils.resolve(config.proxy, enableIPv6 = true) match { - case Some(a) => - config.proxy = a - resolved = true - case None => resolved = false - } - } else { - resolved = true - } - - hasRedirectSupport = Utils.getHasRedirectSupport - - if (resolved && handleConnection) { - notifyForegroundAlert(getString(R.string.forward_success), - getString(R.string.service_running).format(config.profileName)) - handler.sendEmptyMessageDelayed(MSG_CONNECT_SUCCESS, 500) - } else { - notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed)) - stopSelf() - handler.sendEmptyMessageDelayed(MSG_CONNECT_FAIL, 500) - } - handler.sendEmptyMessageDelayed(MSG_CONNECT_FINISH, 500) - } - } - def startRedsocksDaemon() { - val conf = Config.REDSOCKS.format(config.localPort) - val cmd = "%sredsocks -p %sredsocks.pid -c %sredsocks.conf".format(Path.BASE, Path.BASE, Path.BASE) - Config.printToFile(new File(Path.BASE + "redsocks.conf"))(p => { + val conf = ConfigUtils.REDSOCKS.format(config.localPort) + val cmd = "%sredsocks -p %sredsocks.pid -c %sredsocks.conf" + .format(Path.BASE, Path.BASE, Path.BASE) + ConfigUtils.printToFile(new File(Path.BASE + "redsocks.conf"))(p => { p.println(conf) }) Utils.runRootCommand(cmd) @@ -291,14 +164,14 @@ class ShadowsocksService extends Service { try { t.join(300) } catch { - case ignored: InterruptedException => { - } + case ignored: InterruptedException => } !t.isAlive } /** Called when the activity is first created. */ def handleConnection: Boolean = { + startShadowsocksDaemon() startDnsDaemon() startRedsocksDaemon() @@ -308,21 +181,14 @@ class ShadowsocksService extends Service { true } - def initSoundVibrateLights(notification: Notification) { - notification.sound = null - notification.defaults |= Notification.DEFAULT_LIGHTS - } - def invokeMethod(method: Method, args: Array[AnyRef]) { try { method.invoke(this, mStartForegroundArgs: _*) } catch { - case e: InvocationTargetException => { + case e: InvocationTargetException => Log.w(TAG, "Unable to invoke method", e) - } - case e: IllegalAccessException => { + case e: IllegalAccessException => Log.w(TAG, "Unable to invoke method", e) - } } } @@ -340,8 +206,9 @@ class ShadowsocksService extends Service { val icon = getResources.getDrawable(R.drawable.ic_stat_shadowsocks) if (rate >= 0) { - val bitmap =Utils.getBitmap(rate.toString, icon.getIntrinsicWidth * 4, - icon.getIntrinsicHeight * 4, Color.TRANSPARENT) + val bitmap = Utils + .getBitmap(rate.toString, icon.getIntrinsicWidth * 4, icon.getIntrinsicHeight * 4, + Color.TRANSPARENT) builder.setLargeIcon(bitmap) if (rate < 1000) { @@ -352,7 +219,6 @@ class ShadowsocksService extends Service { } else { builder.setSmallIcon(R.drawable.ic_stat_speed, 1091) } - } else { builder.setSmallIcon(R.drawable.ic_stat_shadowsocks) } @@ -364,7 +230,7 @@ class ShadowsocksService extends Service { .setContentText(info) .setContentIntent(contentIntent) .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.stop), - actionIntent) + actionIntent) startForegroundCompat(1, builder.build) } @@ -386,20 +252,17 @@ class ShadowsocksService extends Service { } def onBind(intent: Intent): IBinder = { - null + if (classOf[ShadowsocksNatService].getName equals intent.getAction) { + binder + } else { + null + } } override def onCreate() { super.onCreate() - Config.refresh(this) - - EasyTracker - .getInstance(this) - .send(MapBuilder - .createEvent(TAG, "start", getVersionName, 0L) - .set(Fields.SESSION_CONTROL, "start") - .build()) + ConfigUtils.refresh(this) notificationManager = this .getSystemService(Context.NOTIFICATION_SERVICE) @@ -408,63 +271,19 @@ class ShadowsocksService extends Service { mStartForeground = getClass.getMethod("startForeground", mStartForegroundSignature: _*) mStopForeground = getClass.getMethod("stopForeground", mStopForegroundSignature: _*) } catch { - case e: NoSuchMethodException => { + case e: NoSuchMethodException => mStartForeground = { mStopForeground = null mStopForeground } - } } try { mSetForeground = getClass.getMethod("setForeground", mSetForegroundSignature: _*) } catch { - case e: NoSuchMethodException => { + case e: NoSuchMethodException => throw new IllegalStateException( "OS doesn't have Service.startForeground OR Service.setForeground!") - } } - - // register close receiver - val filter = new IntentFilter() - filter.addAction(Intent.ACTION_SHUTDOWN) - filter.addAction(Action.CLOSE) - receiver = new BroadcastReceiver() { - def onReceive(p1: Context, p2: Intent) { - stopSelf() - } - } - registerReceiver(receiver, filter) - } - - /** Called when the activity is closed. */ - override def onDestroy() { - - // reset timer - if (timer != null) { - timer.cancel() - timer = null - } - - // clean up context - changeState(State.STOPPED) - - EasyTracker - .getInstance(this) - .send(MapBuilder - .createEvent(TAG, "stop", getVersionName, 0L) - .set(Fields.SESSION_CONTROL, "stop") - .build()) - - stopForegroundCompat(1) - if (receiver != null) { - unregisterReceiver(receiver) - receiver = null - } - - // reset NAT - killProcesses() - - super.onDestroy() } def killProcesses() { @@ -489,12 +308,7 @@ class ShadowsocksService extends Service { Utils.runCommand(sb.toString()) } - override def onStart(intent: Intent, startId: Int) { - handleCommand(intent) - } - override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = { - handleCommand(intent) Service.START_STICKY } @@ -593,12 +407,10 @@ class ShadowsocksService extends Service { try { mStopForeground.invoke(this, mStopForegroundArgs: _*) } catch { - case e: InvocationTargetException => { + case e: InvocationTargetException => Log.w(TAG, "Unable to invoke stopForeground", e) - } - case e: IllegalAccessException => { + case e: IllegalAccessException => Log.w(TAG, "Unable to invoke stopForeground", e) - } } return } @@ -606,4 +418,135 @@ class ShadowsocksService extends Service { mSetForegroundArgs(0) = boolean2Boolean(x = false) invokeMethod(mSetForeground, mSetForegroundArgs) } + + override def startRunner(c: Config) { + + var config = c + + // register close receiver + val filter = new IntentFilter() + filter.addAction(Intent.ACTION_SHUTDOWN) + filter.addAction(Action.CLOSE) + receiver = new BroadcastReceiver() { + def onReceive(p1: Context, p2: Intent) { + stopRunner() + } + } + registerReceiver(receiver, filter) + + // start tracker + EasyTracker + .getInstance(this) + .send(MapBuilder + .createEvent(TAG, "start", getVersionName, 0L) + .set(Fields.SESSION_CONTROL, "start") + .build()) + + changeState(State.CONNECTING) + + if (config.isTrafficStat) { + // initialize timer + val task = new TimerTask { + def run() { + val pm = getSystemService(Context.POWER_SERVICE).asInstanceOf[PowerManager] + val now = new + TrafficStat(TrafficStats.getUidTxBytes(uid), TrafficStats.getUidRxBytes(uid), + java.lang.System.currentTimeMillis()) + val txRate = ((now.tx - last.tx) / 1024 / TIMER_INTERVAL).toInt + val rxRate = ((now.rx - last.rx) / 1024 / TIMER_INTERVAL).toInt + last = now + if (lastTxRate == txRate && lastRxRate == rxRate) { + return + } else { + lastTxRate = txRate + lastRxRate = rxRate + } + if ((pm.isScreenOn && state == State.CONNECTED) || (txRate == 0 && rxRate == 0)) { + notifyForegroundAlert(getString(R.string.forward_success), + getString(R.string.service_status).format(math.max(txRate, rxRate)), + math.max(txRate, rxRate)) + } + } + } + last = new TrafficStat(TrafficStats.getUidTxBytes(uid), TrafficStats.getUidRxBytes(uid), + java.lang.System.currentTimeMillis()) + timer = new Timer(true) + timer.schedule(task, TIMER_INTERVAL * 1000, TIMER_INTERVAL * 1000) + } + + if (config.proxy == "198.199.101.152") { + val container = getApplication.asInstanceOf[ShadowsocksApplication].tagContainer + try { + config = ConfigUtils.getPublicConfig(getBaseContext, container, config) + } catch { + case ex: Exception => + notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed)) + stopRunner() + handler.sendEmptyMessageDelayed(Msg.CONNECT_FAIL, 500) + return + } + } + + killProcesses() + + var resolved: Boolean = false + if (!InetAddressUtils.isIPv4Address(config.proxy) && + !InetAddressUtils.isIPv6Address(config.proxy)) { + Utils.resolve(config.proxy, enableIPv6 = true) match { + case Some(a) => + config.proxy = a + resolved = true + case None => resolved = false + } + } else { + resolved = true + } + + hasRedirectSupport = Utils.getHasRedirectSupport + + if (resolved && handleConnection) { + notifyForegroundAlert(getString(R.string.forward_success), + getString(R.string.service_running).format(config.profileName)) + handler.sendEmptyMessageDelayed(Msg.CONNECT_SUCCESS, 500) + } else { + notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed)) + stopRunner() + handler.sendEmptyMessageDelayed(Msg.CONNECT_FAIL, 500) + } + handler.sendEmptyMessageDelayed(Msg.CONNECT_FINISH, 500) + } + + override def stopRunner() { + + // change the state + changeState(State.STOPPED) + + // stop the tracker + EasyTracker + .getInstance(this) + .send(MapBuilder + .createEvent(TAG, "stop", getVersionName, 0L) + .set(Fields.SESSION_CONTROL, "stop") + .build()) + + // reset timer + if (timer != null) { + timer.cancel() + timer = null + } + + // clean up context + stopForegroundCompat(1) + if (receiver != null) { + unregisterReceiver(receiver) + receiver = null + } + + // reset NAT + killProcesses() + } + + override def getTag = TAG + + override def getServiceMode = Mode.NAT } diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksReceiver.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksReceiver.scala index 9e743bde..841c406b 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowsocksReceiver.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksReceiver.scala @@ -54,33 +54,17 @@ class ShadowsocksReceiver extends BroadcastReceiver { val settings: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val status = context.getSharedPreferences(Key.status, Context.MODE_PRIVATE) - if (intent.getAction == Action.UPDATE_STATE) { - val state = intent.getIntExtra(Extra.STATE, State.INIT) - val running = state match { - case State.CONNECTING => true - case State.CONNECTED => true - case _ => false - } - status.edit.putBoolean(Key.isRunning, running).commit() - return - } - var versionName: String = null try { versionName = context.getPackageManager.getPackageInfo(context.getPackageName, 0).versionName } catch { - case e: PackageManager.NameNotFoundException => { + case e: PackageManager.NameNotFoundException => versionName = "NONE" - } } val isAutoConnect: Boolean = settings.getBoolean(Key.isAutoConnect, false) val isInstalled: Boolean = status.getBoolean(versionName, false) if (isAutoConnect && isInstalled) { if (Utils.getRoot) { - if (ShadowsocksService.isServiceStarted(context)) return - val intent: Intent = new Intent(context, classOf[ShadowsocksService]) - Extra.put(settings, intent) - context.startService(intent) } } } diff --git a/src/main/scala/com/github/shadowsocks/ShadowVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala similarity index 71% rename from src/main/scala/com/github/shadowsocks/ShadowVpnService.scala rename to src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala index 93d777e6..89271595 100644 --- a/src/main/scala/com/github/shadowsocks/ShadowVpnService.scala +++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala @@ -51,27 +51,15 @@ import java.io._ import android.net.VpnService import org.apache.http.conn.util.InetAddressUtils import android.os.Message -import scala.concurrent.ops._ import org.apache.commons.net.util.SubnetUtils import java.net.InetAddress import com.github.shadowsocks.utils._ import scala.Some +import com.github.shadowsocks.aidl.Config -object ShadowVpnService { - def isServiceStarted(context: Context): Boolean = { - Utils.isServiceStarted("com.github.shadowsocks.ShadowVpnService", context) - } -} +class ShadowsocksVpnService extends VpnService with BaseService { -class ShadowVpnService extends VpnService { - - val TAG = "ShadowVpnService" - - val MSG_CONNECT_FINISH = 1 - val MSG_CONNECT_SUCCESS = 2 - val MSG_CONNECT_FAIL = 3 - val MSG_STOP_SELF = 5 - val MSG_VPN_ERROR = 6 + val TAG = "ShadowsocksVpnService" val VPN_MTU = 1500 @@ -83,60 +71,21 @@ class ShadowVpnService extends VpnService { var apps: Array[ProxiedApp] = null var config: Config = null - private var state = State.INIT - private var message: String = null - - def changeState(s: Int) { - changeState(s, null) - } - - def changeState(s: Int, m: String) { - if (state != s) { - state = s - if (m != null) message = m - val intent = new Intent(Action.UPDATE_STATE) - intent.putExtra(Extra.STATE, state) - intent.putExtra(Extra.MESSAGE, message) - sendBroadcast(intent) - } - } - val handler: Handler = new Handler { override def handleMessage(msg: Message) { msg.what match { - case MSG_CONNECT_SUCCESS => + case Msg.CONNECT_SUCCESS => changeState(State.CONNECTED) - case MSG_CONNECT_FAIL => + case Msg.CONNECT_FAIL => changeState(State.STOPPED) - case MSG_VPN_ERROR => + case Msg.VPN_ERROR => if (msg.obj != null) changeState(State.STOPPED, msg.obj.asInstanceOf[String]) - case MSG_STOP_SELF => - stopSelf() case _ => } super.handleMessage(msg) } } - def getPid(name: String): Int = { - try { - val reader: BufferedReader = new BufferedReader(new FileReader(Path.BASE + name + ".pid")) - val line = reader.readLine - return Integer.valueOf(line) - } catch { - case e: FileNotFoundException => { - Log.e(TAG, "Cannot open pid file: " + name) - } - case e: IOException => { - Log.e(TAG, "Cannot read pid file: " + name) - } - case e: NumberFormatException => { - Log.e(TAG, "Invalid pid", e) - } - } - -1 - } - def startShadowsocksDaemon() { val cmd: String = (Path.BASE + "shadowsocks -b 127.0.0.1 -s \"%s\" -p \"%d\" -l \"%d\" -k \"%s\" -m \"%s\" -f " + @@ -148,8 +97,8 @@ class ShadowVpnService extends VpnService { def startDnsDaemon() { val cmd: String = Path.BASE + "pdnsd -c " + Path.BASE + "pdnsd.conf" - val conf: String = Config.PDNSD.format("0.0.0.0") - Config.printToFile(new File(Path.BASE + "pdnsd.conf"))(p => { + val conf: String = ConfigUtils.PDNSD.format("0.0.0.0") + ConfigUtils.printToFile(new File(Path.BASE + "pdnsd.conf"))(p => { p.println(conf) }) Utils.runCommand(cmd) @@ -161,74 +110,12 @@ class ShadowVpnService extends VpnService { val pi: PackageInfo = getPackageManager.getPackageInfo(getPackageName, 0) version = pi.versionName } catch { - case e: PackageManager.NameNotFoundException => { + case e: PackageManager.NameNotFoundException => version = "Package name not found" - } } version } - def handleCommand(intent: Intent) { - - if (intent == null) { - stopSelf() - return - } - - if (VpnService.prepare(this) != null) { - val i = new Intent(this, classOf[ShadowVpnActivity]) - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(i) - stopSelf() - return - } - - changeState(State.CONNECTING) - - config = Extra.get(intent) - - spawn { - if (config.proxy == "198.199.101.152") { - val container = getApplication.asInstanceOf[ShadowsocksApplication].tagContainer - try { - config = Config.getPublicConfig(getBaseContext, container, config) - } catch { - case ex: Exception => { - notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed)) - stopSelf() - handler.sendEmptyMessageDelayed(MSG_CONNECT_FAIL, 500) - return - } - } - } - - killProcesses() - - // Resolve server address - var resolved: Boolean = false - if (!InetAddressUtils.isIPv4Address(config.proxy) && - !InetAddressUtils.isIPv6Address(config.proxy)) { - Utils.resolve(config.proxy, enableIPv6 = true) match { - case Some(addr) => - config.proxy = addr - resolved = true - case None => resolved = false - } - } else { - resolved = true - } - - if (resolved && handleConnection) { - handler.sendEmptyMessageDelayed(MSG_CONNECT_SUCCESS, 300) - } else { - notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed)) - handler.sendEmptyMessageDelayed(MSG_CONNECT_FAIL, 300) - handler.sendEmptyMessageDelayed(MSG_STOP_SELF, 500) - } - handler.sendEmptyMessageDelayed(MSG_CONNECT_FINISH, 300) - } - } - def waitForProcess(name: String): Boolean = { val pid: Int = getPid(name) if (pid == -1) return false @@ -242,8 +129,7 @@ class ShadowVpnService extends VpnService { try { t.join(300) } catch { - case ignored: InterruptedException => { - } + case ignored: InterruptedException => } !t.isAlive } @@ -308,18 +194,17 @@ class ShadowVpnService extends VpnService { try { conn = builder.establish() } catch { - case ex: IllegalStateException => { + case ex: IllegalStateException => val msg = new Message() - msg.what = MSG_VPN_ERROR + msg.what = Msg.VPN_ERROR msg.obj = ex.getMessage handler.sendMessage(msg) conn = null - } case ex: Exception => conn = null } if (conn == null) { - stopSelf() + stopRunner() return } @@ -335,7 +220,7 @@ class ShadowVpnService extends VpnService { + "--loglevel 3 " + "--pid %stun2socks.pid") .format(PRIVATE_VLAN.format("2"), PRIVATE_VLAN.format("1"), config.localPort, fd, VPN_MTU, - Path.BASE) + Path.BASE) if (BuildConfig.DEBUG) Log.d(TAG, cmd) System.exec(cmd) } @@ -348,11 +233,6 @@ class ShadowVpnService extends VpnService { true } - def initSoundVibrateLights(notification: Notification) { - notification.sound = null - notification.defaults |= Notification.DEFAULT_LIGHTS - } - def notifyAlert(title: String, info: String) { val openIntent: Intent = new Intent(this, classOf[Shadowsocks]) openIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) @@ -373,6 +253,8 @@ class ShadowVpnService extends VpnService { val action = intent.getAction if (VpnService.SERVICE_INTERFACE == action) { return super.onBind(intent) + } else if (classOf[ShadowsocksVpnService].getName == action) { + return binder } null } @@ -380,55 +262,10 @@ class ShadowVpnService extends VpnService { override def onCreate() { super.onCreate() - Config.refresh(this) - - EasyTracker - .getInstance(this) - .send(MapBuilder - .createEvent(TAG, "start", getVersionName, 0L) - .set(Fields.SESSION_CONTROL, "start") - .build()) + ConfigUtils.refresh(this) notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) .asInstanceOf[NotificationManager] - - // register close receiver - val filter = new IntentFilter() - filter.addAction(Intent.ACTION_SHUTDOWN) - filter.addAction(Action.CLOSE) - receiver = new BroadcastReceiver { - def onReceive(p1: Context, p2: Intent) { - stopSelf() - } - } - registerReceiver(receiver, filter) - } - - override def onDestroy() { - killProcesses() - - changeState(State.STOPPED) - - EasyTracker - .getInstance(this) - .send(MapBuilder - .createEvent(TAG, "stop", getVersionName, 0L) - .set(Fields.SESSION_CONTROL, "stop") - .build()) - - if (receiver != null) { - unregisterReceiver(receiver) - receiver = null - } - - if (conn != null) { - conn.close() - conn = null - } - - notificationManager.cancel(1) - - super.onDestroy() } def killProcesses() { @@ -448,13 +285,115 @@ class ShadowVpnService extends VpnService { Utils.runCommand(sb.toString()) } - override def onStart(intent: Intent, startId: Int) { - handleCommand(intent) - } - override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = { - handleCommand(intent) Service.START_STICKY } + override def startRunner(c: Config) { + + var config = c + + // ensure the VPNService is prepared + if (VpnService.prepare(this) != null) { + val i = new Intent(this, classOf[ShadowVpnActivity]) + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(i) + return + } + + // start the tracker + EasyTracker + .getInstance(this) + .send(MapBuilder + .createEvent(TAG, "start", getVersionName, 0L) + .set(Fields.SESSION_CONTROL, "start") + .build()) + + // register close receiver + val filter = new IntentFilter() + filter.addAction(Intent.ACTION_SHUTDOWN) + receiver = new BroadcastReceiver { + def onReceive(p1: Context, p2: Intent) { + stopRunner() + } + } + registerReceiver(receiver, filter) + + changeState(State.CONNECTING) + + if (config.proxy == "198.199.101.152") { + val container = getApplication.asInstanceOf[ShadowsocksApplication].tagContainer + try { + config = ConfigUtils.getPublicConfig(getBaseContext, container, config) + } catch { + case ex: Exception => + notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed)) + stopRunner() + handler.sendEmptyMessageDelayed(Msg.CONNECT_FAIL, 500) + return + } + } + + // reset the context + killProcesses() + + // Resolve the server address + var resolved: Boolean = false + if (!InetAddressUtils.isIPv4Address(config.proxy) && + !InetAddressUtils.isIPv6Address(config.proxy)) { + Utils.resolve(config.proxy, enableIPv6 = true) match { + case Some(addr) => + config.proxy = addr + resolved = true + case None => resolved = false + } + } else { + resolved = true + } + + if (resolved && handleConnection) { + handler.sendEmptyMessageDelayed(Msg.CONNECT_SUCCESS, 300) + } else { + notifyAlert(getString(R.string.forward_fail), getString(R.string.service_failed)) + handler.sendEmptyMessageDelayed(Msg.CONNECT_FAIL, 300) + stopRunner() + } + handler.sendEmptyMessageDelayed(Msg.CONNECT_FINISH, 300) + } + + override def stopRunner() { + + // channge the state + changeState(State.STOPPED) + + // stop the tracker + EasyTracker + .getInstance(this) + .send(MapBuilder + .createEvent(TAG, "stop", getVersionName, 0L) + .set(Fields.SESSION_CONTROL, "stop") + .build()) + + // clean up the context + if (receiver != null) { + unregisterReceiver(receiver) + receiver = null + } + + // reset VPN + killProcesses() + + // close connections + if (conn != null) { + conn.close() + conn = null + } + + // reset notifications + notificationManager.cancel(1) + } + + override def getTag = TAG + + override def getServiceMode = Mode.VPN } diff --git a/src/main/scala/com/github/shadowsocks/utils/Config.scala b/src/main/scala/com/github/shadowsocks/utils/ConfigUtils.scala similarity index 64% rename from src/main/scala/com/github/shadowsocks/utils/Config.scala rename to src/main/scala/com/github/shadowsocks/utils/ConfigUtils.scala index c5d6ff2f..5090cd2f 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Config.scala +++ b/src/main/scala/com/github/shadowsocks/utils/ConfigUtils.scala @@ -1,11 +1,12 @@ package com.github.shadowsocks.utils -import android.content.Context +import android.content.{Intent, SharedPreferences, Context} import com.github.shadowsocks.ShadowsocksApplication import com.google.tagmanager.Container import scalaj.http.{HttpOptions, Http} +import com.github.shadowsocks.aidl.Config -object Config { +object ConfigUtils { val SHADOWSOCKS = "{\"server\": [%s], \"server_port\": %d, \"local_port\": %d, \"password\": %s, \"timeout\": %d}" val REDSOCKS = "base {" + " log_debug = off;" + @@ -83,10 +84,35 @@ object Config { val method = proxy(3).trim new Config(config.isGlobalProxy, config.isGFWList, config.isBypassApps, config.isTrafficStat, - config.profileName, host, password, method, port, config.localPort, config.proxiedAppString) + config.profileName, host, password, method, config.proxiedAppString, port, config.localPort) } -} -case class Config(isGlobalProxy: Boolean, isGFWList: Boolean, isBypassApps: Boolean, - isTrafficStat: Boolean, profileName: String, var proxy: String, sitekey: String, - encMethod: String, remotePort: Int, localPort: Int, proxiedAppString: String) + def load(settings: SharedPreferences): Config = { + val isGlobalProxy = settings.getBoolean(Key.isGlobalProxy, false) + val isGFWList = settings.getBoolean(Key.isGFWList, false) + val isBypassApps = settings.getBoolean(Key.isBypassApps, false) + val isTrafficStat = settings.getBoolean(Key.isTrafficStat, false) + + val profileName = settings.getString(Key.profileName, "default") + val proxy = settings.getString(Key.proxy, "127.0.0.1") + val sitekey = settings.getString(Key.sitekey, "default") + val encMethod = settings.getString(Key.encMethod, "table") + val remotePort: Int = try { + settings.getString(Key.remotePort, "1984").toInt + } catch { + case ex: NumberFormatException => + 1984 + } + val localPort: Int = try { + settings.getString(Key.localPort, "1984").toInt + } catch { + case ex: NumberFormatException => + 1984 + } + val proxiedAppString = settings.getString(Key.proxied, "") + + new Config(isGlobalProxy, isGFWList, isBypassApps, isTrafficStat, profileName, proxy, sitekey, + encMethod, proxiedAppString, remotePort, localPort) + + } +} \ No newline at end of file diff --git a/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/src/main/scala/com/github/shadowsocks/utils/Constants.scala index cf113971..2b4df64e 100644 --- a/src/main/scala/com/github/shadowsocks/utils/Constants.scala +++ b/src/main/scala/com/github/shadowsocks/utils/Constants.scala @@ -1,6 +1,14 @@ package com.github.shadowsocks.utils import android.content.{Intent, SharedPreferences} +import com.github.shadowsocks.aidl.Config + +object Msg { + val CONNECT_FINISH = 1 + val CONNECT_SUCCESS = 2 + val CONNECT_FAIL = 3 + val VPN_ERROR = 6 +} object Path { val BASE = "/data/data/com.github.shadowsocks/" @@ -37,100 +45,21 @@ object Scheme { val SS = "ss" } +object Mode { + val NAT = 0 + val VPN = 1 +} + object State { val INIT = 0 val CONNECTING = 1 val CONNECTED = 2 val STOPPED = 3 + def isAvailable(state: Int): Boolean = state != CONNECTED && state != CONNECTING } object Action { - val CLOSE = "com.github.shadowsocks.ACTION_SHUTDOWN" - val UPDATE_STATE = "com.github.shadowsocks.ACTION_UPDATE_STATE" + val CLOSE = "com.github.shadowsocks.CLOSE" val UPDATE_FRAGMENT = "com.github.shadowsocks.ACTION_UPDATE_FRAGMENT" val UPDATE_PREFS = "com.github.shadowsocks.ACTION_UPDATE_PREFS" -} - -object Extra { - val STATE = "state" - val MESSAGE = "message" - - def save(settings: SharedPreferences, config: Config) { - val edit = settings.edit() - - edit.putBoolean(Key.isGlobalProxy, config.isGlobalProxy) - edit.putBoolean(Key.isGFWList, config.isGFWList) - edit.putBoolean(Key.isBypassApps, config.isBypassApps) - edit.putBoolean(Key.isTrafficStat, config.isTrafficStat) - - edit.putString(Key.profileName, config.profileName) - edit.putString(Key.proxy, config.proxy) - edit.putString(Key.sitekey, config.sitekey) - edit.putString(Key.encMethod, config.encMethod) - edit.putString(Key.remotePort, config.remotePort.toString) - edit.putString(Key.localPort, config.localPort.toString) - - edit.apply() - } - - def get(intent: Intent): Config = { - val isGlobalProxy = intent.getBooleanExtra(Key.isGlobalProxy, false) - val isGFWList = intent.getBooleanExtra(Key.isGFWList, false) - val isBypassApps = intent.getBooleanExtra(Key.isBypassApps, false) - val isTrafficStat = intent.getBooleanExtra(Key.isTrafficStat, false) - - val profileName = intent.getStringExtra(Key.profileName) - val proxy = intent.getStringExtra(Key.proxy) - val sitekey = intent.getStringExtra(Key.sitekey) - val encMethod = intent.getStringExtra(Key.encMethod) - val remotePort = intent.getIntExtra(Key.remotePort, 1984) - val localPort = intent.getIntExtra(Key.localPort, 1984) - val proxiedString = intent.getStringExtra(Key.proxied) - - new Config(isGlobalProxy, isGFWList, isBypassApps, isTrafficStat, profileName, proxy, sitekey, - encMethod, remotePort, localPort, proxiedString) - } - - def put(settings: SharedPreferences, intent: Intent) { - val isGlobalProxy = settings.getBoolean(Key.isGlobalProxy, false) - val isGFWList = settings.getBoolean(Key.isGFWList, false) - val isBypassApps = settings.getBoolean(Key.isBypassApps, false) - val isTrafficStat = settings.getBoolean(Key.isTrafficStat, false) - - - val profileName = settings.getString(Key.profileName, "default") - val proxy = settings.getString(Key.proxy, "127.0.0.1") - val sitekey = settings.getString(Key.sitekey, "default") - val encMethod = settings.getString(Key.encMethod, "table") - val remotePort: Int = try { - settings.getString(Key.remotePort, "1984").toInt - } catch { - case ex: NumberFormatException => { - 1984 - } - } - val localProt: Int = try { - settings.getString(Key.localPort, "1984").toInt - } catch { - case ex: NumberFormatException => { - 1984 - } - } - val proxiedAppString = settings.getString(Key.proxied, "") - - intent.putExtra(Key.isGlobalProxy, isGlobalProxy) - intent.putExtra(Key.isGFWList, isGFWList) - intent.putExtra(Key.isBypassApps, isBypassApps) - intent.putExtra(Key.isTrafficStat, isTrafficStat) - - intent.putExtra(Key.profileName, profileName) - intent.putExtra(Key.proxy, proxy) - intent.putExtra(Key.sitekey, sitekey) - intent.putExtra(Key.encMethod, encMethod) - intent.putExtra(Key.remotePort, remotePort) - intent.putExtra(Key.localPort, localProt) - - intent.putExtra(Key.proxied, proxiedAppString) - } -} - +} \ No newline at end of file