Commit c480137d authored by Mygod's avatar Mygod Committed by Max Lv

Remove .aidl.Config and persisting SharedPreferences (#742)

Previously a profile can exist in the following forms:

* `.database.Profile` used for everything backend;
* `.ShadowsocksSettings` which uses Preferences to provide UI to tweak settings;
* `.aidl.Config` used for IPC;
* `<data dir>/shared_pref/com.github.shadowsocks_preferences.xml` which exists for no apparent reason.

Thus long code block can be seen whose purpose is simply converting the data from one form to another. This commit intends to remove the latter two by:

1. Using profile ID and take advantage of fs r/w lock SQLite uses for IPC and better extensibility;
2. Stop persisting redundant fields in the preferences file and update database directly.
parent 8515edff
package com.github.shadowsocks.aidl;
parcelable Config;
\ No newline at end of file
package com.github.shadowsocks.aidl;
import android.os.Parcel;
import android.os.Parcelable;
public class Config implements Parcelable {
public boolean isProxyApps = false;
public boolean isBypassApps = false;
public boolean isUdpDns = false;
public boolean isAuth = false;
public boolean isIpv6 = false;
public String profileName = "Untitled";
public String proxy = "127.0.0.1";
public String sitekey = "null";
public String route = "all";
public String encMethod = "rc4";
public String proxiedAppString = "";
public int remotePort = 1984;
public int localPort = 1080;
public int profileId = 0;
public static final Parcelable.Creator<Config> CREATOR = new Parcelable.Creator<Config>() {
public Config createFromParcel(Parcel in) {
return new Config(in);
}
public Config[] newArray(int size) {
return new Config[size];
}
};
public Config(boolean isProxyApps, boolean isBypassApps,
boolean isUdpDns, boolean isAuth, boolean isIpv6, String profileName, String proxy, String sitekey,
String encMethod, String proxiedAppString, String route, int remotePort, int localPort, int profileId) {
this.isProxyApps = isProxyApps;
this.isBypassApps = isBypassApps;
this.isUdpDns = isUdpDns;
this.isAuth = isAuth;
this.isIpv6 = isIpv6;
this.profileName = profileName;
this.proxy = proxy;
this.sitekey = sitekey;
this.encMethod = encMethod;
this.proxiedAppString = proxiedAppString;
this.route = route;
this.remotePort = remotePort;
this.localPort = localPort;
this.profileId = profileId;
}
private Config(Parcel in) {
readFromParcel(in);
}
public void readFromParcel(Parcel in) {
isProxyApps = in.readInt() == 1;
isBypassApps = in.readInt() == 1;
isUdpDns = in.readInt() == 1;
isAuth = in.readInt() == 1;
isIpv6 = in.readInt() == 1;
profileName = in.readString();
proxy = in.readString();
sitekey = in.readString();
encMethod = in.readString();
proxiedAppString = in.readString();
route = in.readString();
remotePort = in.readInt();
localPort = in.readInt();
profileId = in.readInt();
}
@Override public int describeContents() {
return 0;
}
@Override public void writeToParcel(Parcel out, int flags) {
out.writeInt(isProxyApps ? 1 : 0);
out.writeInt(isBypassApps ? 1 : 0);
out.writeInt(isUdpDns ? 1 : 0);
out.writeInt(isAuth ? 1 : 0);
out.writeInt(isIpv6 ? 1 : 0);
out.writeString(profileName);
out.writeString(proxy);
out.writeString(sitekey);
out.writeString(encMethod);
out.writeString(proxiedAppString);
out.writeString(route);
out.writeInt(remotePort);
out.writeInt(localPort);
out.writeInt(profileId);
}
}
package com.github.shadowsocks.aidl;
import com.github.shadowsocks.aidl.Config;
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback;
interface IShadowsocksService {
......@@ -9,5 +8,5 @@ interface IShadowsocksService {
oneway void registerCallback(IShadowsocksServiceCallback cb);
oneway void unregisterCallback(IShadowsocksServiceCallback cb);
oneway void use(in Config config);
oneway void use(in int profileId);
}
......@@ -7,36 +7,43 @@
<com.github.shadowsocks.preferences.SummaryEditTextPreference
android:key="profileName"
android:persistent="false"
android:title="@string/profile_name"/>
<com.github.shadowsocks.preferences.SummaryEditTextPreference
android:key="proxy"
android:persistent="false"
android:summary="@string/proxy_summary"
android:title="@string/proxy"/>
<com.github.shadowsocks.preferences.NumberPickerPreference
app:min="1"
app:max="65535"
android:key="remotePortNum"
android:persistent="false"
android:summary="@string/remote_port_summary"
android:title="@string/remote_port"/>
<com.github.shadowsocks.preferences.NumberPickerPreference
app:min="1025"
app:max="65535"
android:key="localPortNum"
android:persistent="false"
android:summary="@string/port_summary"
android:title="@string/port"/>
<com.github.shadowsocks.preferences.PasswordEditTextPreference
android:inputType="textPassword"
android:key="sitekey"
android:persistent="false"
android:summary="@string/sitekey_summary"
android:title="@string/sitekey"/>
<com.github.shadowsocks.preferences.DropDownPreference
android:key="encMethod"
android:persistent="false"
app:entries="@array/enc_method_entry"
app:entryValues="@array/enc_method_value"
android:summary="%s"
android:title="@string/enc_method"/>
<SwitchPreference
android:key="isAuth"
android:persistent="false"
android:summary="@string/onetime_auth_summary"
android:title="@string/onetime_auth"/>
......@@ -47,20 +54,24 @@
<com.github.shadowsocks.preferences.DropDownPreference
android:key="route"
android:persistent="false"
app:entries="@array/route_entry"
app:entryValues="@array/route_value"
android:summary="%s"
android:title="@string/route_list"/>
<SwitchPreference
android:key="isIpv6"
android:persistent="false"
android:summary="@string/ipv6_summary"
android:title="@string/ipv6"/>
<SwitchPreference
android:key="isProxyApps"
android:persistent="false"
android:summary="@string/proxied_apps_summary"
android:title="@string/proxied_apps"/>
<SwitchPreference
android:key="isUdpDns"
android:persistent="false"
android:summary="@string/udp_dns_summary"
android:title="@string/udp_dns"/>
......
......@@ -117,8 +117,10 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
proxiedApps.add(item.packageName)
check.setChecked(true)
}
if (!appsLoading.get)
app.editor.putString(Key.proxied, proxiedApps.mkString("\n")).apply
if (!appsLoading.get) {
profile.individual = proxiedApps.mkString("\n")
app.profileManager.updateProfile(profile)
}
}
}
......@@ -141,9 +143,9 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
private var loadingView: View = _
private val appsLoading = new AtomicBoolean
private var handler: Handler = _
private val profile = app.currentProfile.get
private def initProxiedApps(str: String = app.settings.getString(Key.proxied, "")) =
proxiedApps = str.split('\n').to[mutable.HashSet]
private def initProxiedApps(str: String = profile.individual) = proxiedApps = str.split('\n').to[mutable.HashSet]
override def onDestroy() {
instance = null
......@@ -225,17 +227,23 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
toolbar.inflateMenu(R.menu.app_manager_menu)
toolbar.setOnMenuItemClickListener(this)
app.editor.putBoolean(Key.isProxyApps, true).apply
if (!profile.proxyApps) {
profile.proxyApps = true
app.profileManager.updateProfile(profile)
}
findViewById(R.id.onSwitch).asInstanceOf[Switch]
.setOnCheckedChangeListener((_, checked) => {
app.editor.putBoolean(Key.isProxyApps, checked).apply
profile.proxyApps = checked
app.profileManager.updateProfile(profile)
finish()
})
bypassSwitch = findViewById(R.id.bypassSwitch).asInstanceOf[Switch]
bypassSwitch.setChecked(app.settings.getBoolean(Key.isBypassApps, false))
bypassSwitch.setOnCheckedChangeListener((_, checked) =>
app.editor.putBoolean(Key.isBypassApps, checked).apply())
bypassSwitch.setChecked(profile.bypass)
bypassSwitch.setOnCheckedChangeListener((_, checked) => {
profile.bypass = checked
app.profileManager.updateProfile(profile)
})
initProxiedApps()
loadingView = findViewById(R.id.loading)
......
......@@ -47,17 +47,19 @@ import android.os.{Handler, RemoteCallbackList}
import android.text.TextUtils
import android.util.Log
import android.widget.Toast
import com.github.shadowsocks.aidl.{Config, IShadowsocksService, IShadowsocksServiceCallback}
import com.github.kevinsawicki.http.HttpRequest
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.utils._
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
trait BaseService extends Service {
@volatile private var state = State.STOPPED
@volatile protected var config: Config = null
@volatile protected var profile: Profile = _
var timer: Timer = null
var trafficMonitorThread: TrafficMonitorThread = null
var timer: Timer = _
var trafficMonitorThread: TrafficMonitorThread = _
final val callbacks = new RemoteCallbackList[IShadowsocksServiceCallback]
var callbacksCount: Int = _
......@@ -101,26 +103,50 @@ trait BaseService extends Service {
}
}
override def use(config: Config) = synchronized(state match {
case State.STOPPED => if (config != null && checkConfig(config)) startRunner(config)
case State.CONNECTED =>
if (config == null) stopRunner(true)
else if (config.profileId != BaseService.this.config.profileId && checkConfig(config)) {
override def use(profileId: Int) = synchronized(if (profileId < 0) stopRunner(true) else {
val profile = app.profileManager.getProfile(profileId).orNull
if (profile == null) stopRunner(true) else state match {
case State.STOPPED => if (checkProfile(profile)) startRunner(profile)
case State.CONNECTED => if (profileId != BaseService.this.profile.id && checkProfile(profile)) {
stopRunner(false)
startRunner(config)
startRunner(profile)
}
case _ => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + state)
case _ => Log.w(BaseService.this.getClass.getSimpleName, "Illegal state when invoking use: " + state)
}
})
}
def checkConfig(config: Config) = if (TextUtils.isEmpty(config.proxy) || TextUtils.isEmpty(config.sitekey)) {
def checkProfile(profile: Profile) = if (TextUtils.isEmpty(profile.host) || TextUtils.isEmpty(profile.password)) {
changeState(State.STOPPED)
stopRunner(true)
false
} else true
def startRunner(config: Config) {
this.config = config
def connect() = if (profile.host == "198.199.101.152") try {
val holder = app.containerHolder
val container = holder.getContainer
val url = container.getString("proxy_url")
val sig = Utils.getSignature(this)
val list = HttpRequest
.post(url)
.connectTimeout(2000)
.readTimeout(2000)
.send("sig="+sig)
.body
val proxies = util.Random.shuffle(list.split('|').toSeq)
val proxy = proxies.head.split(':')
profile.host = proxy(0).trim
profile.remotePort = proxy(1).trim.toInt
profile.password = proxy(2).trim
profile.method = proxy(3).trim
} catch {
case ex: Exception =>
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner(true)
}
def startRunner(profile: Profile) {
this.profile = profile
startService(new Intent(this, getClass))
TrafficMonitor.reset()
......@@ -135,6 +161,12 @@ trait BaseService extends Service {
registerReceiver(closeReceiver, filter)
closeReceiverRegistered = true
}
app.track(getClass.getSimpleName, "start")
changeState(State.CONNECTING)
Utils.ThrowableFuture(connect)
}
def stopRunner(stopService: Boolean) {
......@@ -158,17 +190,19 @@ trait BaseService extends Service {
// stop the service if nothing has bound to it
if (stopService) stopSelf()
profile = null
}
def updateTrafficTotal(tx: Long, rx: Long) {
val config = this.config // avoid race conditions without locking
if (config != null) {
app.profileManager.getProfile(config.profileId) match {
case Some(profile) =>
profile.tx += tx
profile.rx += rx
app.profileManager.updateProfile(profile)
case None => // Ignore
val profile = this.profile // avoid race conditions without locking
if (profile != null) {
app.profileManager.getProfile(profile.id) match {
case Some(p) => // default profile may have host, etc. modified
p.tx += tx
p.rx += rx
app.profileManager.updateProfile(p)
case None =>
}
}
}
......@@ -197,6 +231,12 @@ trait BaseService extends Service {
})
}
override def onCreate() {
super.onCreate()
app.refreshContainerHolder
}
// Service of shadowsocks should always be started explicitly
override def onStartCommand(intent: Intent, flags: Int, startId: Int): Int = Service.START_NOT_STICKY
......@@ -217,4 +257,15 @@ trait BaseService extends Service {
state = s
})
}
def getBlackList = {
val default = getString(R.string.black_list)
try {
val container = app.containerHolder.getContainer
val update = container.getString("black_list")
if (update == null || update.isEmpty) default else update
} catch {
case ex: Exception => default
}
}
}
......@@ -11,20 +11,19 @@ import android.provider.Settings
import android.support.v7.app.{AlertDialog, AppCompatActivity}
import android.support.v7.widget.RecyclerView.ViewHolder
import android.support.v7.widget.Toolbar.OnMenuItemClickListener
import android.support.v7.widget._
import android.support.v7.widget.helper.ItemTouchHelper
import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
import android.support.v7.widget._
import android.text.style.TextAppearanceSpan
import android.text.{SpannableStringBuilder, Spanned, TextUtils}
import android.util.Log
import android.view._
import android.widget.{CheckedTextView, ImageView, LinearLayout, Toast}
import com.github.clans.fab.{FloatingActionButton, FloatingActionMenu}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils.{Key, Parser, TrafficMonitor, Utils}
import com.github.shadowsocks.widget.UndoSnackbarManager
import com.github.shadowsocks.ShadowsocksApplication.app
import net.glxn.qrgen.android.QRCode
import scala.collection.mutable.ArrayBuffer
......@@ -281,8 +280,9 @@ final class ProfileManagerActivity extends AppCompatActivity with OnMenuItemClic
v.getId match {
case R.id.fab_manual_add =>
menu.toggle(true)
app.profileManager.reload(-1)
app.switchProfile(app.profileManager.save.id)
val profile = app.profileManager.createProfile()
app.profileManager.updateProfile(profile)
app.switchProfile(profile.id)
finish
case R.id.fab_qrcode_add =>
menu.toggle(false)
......
......@@ -100,7 +100,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
import Shadowsocks._
// Variables
var serviceStarted = false
var serviceStarted: Boolean = _
var fab: FloatingActionButton = _
var fabProgressCircle: FABProgressCircle = _
var progressDialog: ProgressDialog = _
......@@ -186,23 +186,6 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
if (!app.settings.getBoolean(app.getVersionName, false)) {
app.editor.putBoolean(app.getVersionName, true).apply()
try {
// Workaround that convert port(String) to port(Int)
val oldLocalPort = app.settings.getString(Key.localPort, "")
val oldRemotePort = app.settings.getString(Key.remotePort, "")
if (oldLocalPort != "") {
app.editor.putInt(Key.localPort, oldLocalPort.toInt).apply()
}
if (oldRemotePort != "") {
app.editor.putInt(Key.remotePort, oldRemotePort.toInt).apply()
}
} catch {
case ex: Exception => // Ignore
}
val oldProxiedApps = app.settings.getString(Key.proxied, "")
if (oldProxiedApps.contains('|'))
app.editor.putString(Key.proxied, DBHelper.updateProxiedApps(this, oldProxiedApps)).apply()
recovery()
......@@ -364,7 +347,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
val title = field.get(toolbar).asInstanceOf[TextView]
title.setFocusable(true)
title.setGravity(0x10)
title.getLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
title.getLayoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
title.setOnClickListener(_ => startActivity(new Intent(this, classOf[ProfileManagerActivity])))
val typedArray = obtainStyledAttributes(Array(R.attr.selectableItemBackgroundBorderless))
title.setBackgroundResource(typedArray.getResourceId(0, 0))
......@@ -434,11 +417,6 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
handler.post(() => attachService)
}
protected override def onPause() {
super.onPause()
app.profileManager.save
}
private def hideCircle() {
try {
fabProgressCircle.hide()
......@@ -489,34 +467,34 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
private def updateCurrentProfile() = {
// Check if current profile changed
if (app.profileId != currentProfile.id) {
currentProfile = app.currentProfile match {
if (preferences.profile == null || app.profileId != preferences.profile.id) {
updatePreferenceScreen(app.currentProfile match {
case Some(profile) => profile // updated
case None => // removed
app.switchProfile((app.profileManager.getFirstProfile match {
case Some(first) => first
case None => app.profileManager.createDefault()
}).id)
}
updatePreferenceScreen()
})
if (serviceStarted) serviceLoad()
true
} else false
} else {
preferences.refreshProfile()
false
}
}
protected override def onResume() {
super.onResume()
ConfigUtils.refresh(this)
app.refreshContainerHolder
updateState(updateCurrentProfile())
}
private def updatePreferenceScreen() {
val profile = currentProfile
private def updatePreferenceScreen(profile: Profile) {
if (profile.host == "198.199.101.152") if (adView == null) {
adView = new AdView(this)
adView.setAdUnitId("ca-app-pub-9097031975646651/7760346322")
......@@ -525,7 +503,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
adView.loadAd(new AdRequest.Builder().build())
} else adView.setVisibility(View.VISIBLE) else if (adView != null) adView.setVisibility(View.GONE)
preferences.update(profile)
preferences.setProfile(profile)
}
override def onStart() {
......@@ -587,7 +565,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
}
def serviceStop() {
if (bgService != null) bgService.use(null)
if (bgService != null) bgService.use(-1)
}
def checkText(key: String): Boolean = {
......@@ -599,7 +577,7 @@ class Shadowsocks extends AppCompatActivity with ServiceBoundContext {
/** Called when connect button is clicked. */
def serviceLoad() {
bgService.use(ConfigUtils.loadFromSharedPreferences)
bgService.use(app.profileId)
if (app.isVpnEnabled) {
changeSwitch(checked = false)
......
......@@ -60,13 +60,12 @@ object ShadowsocksApplication {
class ShadowsocksApplication extends Application {
import ShadowsocksApplication._
lazy val dbHelper = new DBHelper(this)
final val SIG_FUNC = "getSignature"
var containerHolder: ContainerHolder = _
lazy val tracker = GoogleAnalytics.getInstance(this).newTracker(R.xml.tracker)
lazy val settings = PreferenceManager.getDefaultSharedPreferences(this)
lazy val editor = settings.edit
lazy val profileManager = new ProfileManager(settings, dbHelper)
lazy val profileManager = new ProfileManager(new DBHelper(this))
def isNatEnabled = settings.getBoolean(Key.isNAT, false)
def isVpnEnabled = !isNatEnabled
......@@ -85,12 +84,12 @@ class ShadowsocksApplication extends Application {
.build())
def profileId = settings.getInt(Key.profileId, -1)
def profileId(i: Int) = settings.edit.putInt(Key.profileId, i).apply
def profileId(i: Int) = editor.putInt(Key.profileId, i).apply
def currentProfile = profileManager.getProfile(profileId)
def switchProfile(id: Int) = {
profileId(id)
profileManager.load(id)
profileManager.getProfile(id) getOrElse profileManager.createProfile()
}
override def onCreate() {
......@@ -117,4 +116,9 @@ class ShadowsocksApplication extends Application {
}
pending.setResultCallback(callback, 2, TimeUnit.SECONDS)
}
def refreshContainerHolder {
val holder = app.containerHolder
if (holder != null) holder.refresh()
}
}
......@@ -66,7 +66,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
def startBackgroundService() {
if (app.isNatEnabled) {
bgService.use(ConfigUtils.loadFromSharedPreferences)
bgService.use(app.profileId)
finish()
} else {
val intent = VpnService.prepare(ShadowsocksRunnerActivity.this)
......@@ -109,7 +109,7 @@ class ShadowsocksRunnerActivity extends Activity with ServiceBoundContext {
resultCode match {
case Activity.RESULT_OK =>
if (bgService != null) {
bgService.use(ConfigUtils.loadFromSharedPreferences)
bgService.use(app.profileId)
}
case _ =>
Log.e(TAG, "Failed to start VpnService")
......
......@@ -58,11 +58,11 @@ class ShadowsocksRunnerService extends Service with ServiceBoundContext {
}
def startBackgroundService() {
if (app.isNatEnabled) bgService.use(ConfigUtils.loadFromSharedPreferences) else {
if (app.isNatEnabled) bgService.use(app.profileId) else {
val intent = VpnService.prepare(ShadowsocksRunnerService.this)
if (intent == null) {
if (bgService != null) {
bgService.use(ConfigUtils.loadFromSharedPreferences)
bgService.use(app.profileId)
}
}
}
......
......@@ -9,10 +9,10 @@ import android.os.{Build, Bundle}
import android.preference.{Preference, PreferenceFragment, SwitchPreference}
import android.support.v7.app.AlertDialog
import android.webkit.{WebView, WebViewClient}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.preferences._
import com.github.shadowsocks.utils.{Key, Utils}
import com.github.shadowsocks.ShadowsocksApplication.app
object ShadowsocksSettings {
// Constants
......@@ -74,12 +74,58 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
addPreferencesFromResource(R.xml.pref_all)
getPreferenceManager.getSharedPreferences.registerOnSharedPreferenceChangeListener(this)
findPreference(Key.profileName).setOnPreferenceChangeListener((_, value) => {
profile.name = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.proxy).setOnPreferenceChangeListener((_, value) => {
profile.host = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.remotePort).setOnPreferenceChangeListener((_, value) => {
profile.remotePort = value.asInstanceOf[Int]
app.profileManager.updateProfile(profile)
})
findPreference(Key.localPort).setOnPreferenceChangeListener((_, value) => {
profile.localPort = value.asInstanceOf[Int]
app.profileManager.updateProfile(profile)
})
findPreference(Key.sitekey).setOnPreferenceChangeListener((_, value) => {
profile.password = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.encMethod).setOnPreferenceChangeListener((_, value) => {
profile.method = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
findPreference(Key.route).setOnPreferenceChangeListener((_, value) => {
profile.route = value.asInstanceOf[String]
app.profileManager.updateProfile(profile)
})
isProxyApps = findPreference(Key.isProxyApps).asInstanceOf[SwitchPreference]
isProxyApps.setOnPreferenceClickListener((preference: Preference) => {
isProxyApps.setOnPreferenceClickListener(_ => {
startActivity(new Intent(activity, classOf[AppManager]))
isProxyApps.setChecked(true)
false
})
isProxyApps.setOnPreferenceChangeListener((_, value) => {
profile.proxyApps = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.isUdpDns).setOnPreferenceChangeListener((_, value) => {
profile.udpdns = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.isAuth).setOnPreferenceChangeListener((_, value) => {
profile.auth = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
findPreference(Key.isIpv6).setOnPreferenceChangeListener((_, value) => {
profile.ipv6 = value.asInstanceOf[Boolean]
app.profileManager.updateProfile(profile)
})
val switch = findPreference(Key.isAutoConnect).asInstanceOf[SwitchPreference]
switch.setOnPreferenceChangeListener((_, value) => {
......@@ -131,9 +177,9 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
})
}
override def onResume {
super.onResume()
isProxyApps.setChecked(app.settings.getBoolean(Key.isProxyApps, false)) // update
def refreshProfile() {
profile = app.currentProfile.get
isProxyApps.setChecked(profile.proxyApps)
}
override def onDestroy {
......@@ -160,14 +206,9 @@ class ShadowsocksSettings extends PreferenceFragment with OnSharedPreferenceChan
}
}
def update(profile: Profile) {
for (name <- PROXY_PREFS) {
val pref = findPreference(name)
updatePreference(pref, name, profile)
}
for (name <- FEATURE_PREFS) {
val pref = findPreference(name)
updatePreference(pref, name, profile)
}
var profile: Profile = _
def setProfile(profile: Profile) {
this.profile = profile
for (name <- Array(PROXY_PREFS, FEATURE_PREFS).flatten) updatePreference(findPreference(name), name, profile)
}
}
......@@ -48,9 +48,9 @@ import android.content.pm.PackageManager.NameNotFoundException
import android.net.VpnService
import android.os._
import android.util.Log
import com.github.shadowsocks.aidl.Config
import com.github.shadowsocks.utils._
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.database.Profile
import scala.collection.mutable.ArrayBuffer
......@@ -78,11 +78,6 @@ class ShadowsocksVpnService extends VpnService with BaseService {
null
}
override def onCreate() {
super.onCreate()
ConfigUtils.refresh(this)
}
override def onRevoke() {
stopRunner(true)
}
......@@ -132,66 +127,53 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
}
override def startRunner(config: Config) {
super.startRunner(config)
vpnThread = new ShadowsocksVpnThread(this)
vpnThread.start()
override def startRunner(profile: Profile) {
// ensure the VPNService is prepared
if (VpnService.prepare(this) != null) {
val i = new Intent(this, classOf[ShadowsocksRunnerActivity])
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(i)
stopRunner(true)
return
}
app.track(TAG, "start")
super.startRunner(profile)
}
changeState(State.CONNECTING)
override def connect() = {
super.connect()
Utils.ThrowableFuture {
if (config.proxy == "198.199.101.152") {
val holder = app.containerHolder
try {
this.config = ConfigUtils.getPublicConfig(getBaseContext, holder.getContainer, config)
} catch {
case ex: Exception =>
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner(true)
this.config = null
}
}
if (profile != null) {
if (config != null) {
vpnThread = new ShadowsocksVpnThread(this)
vpnThread.start()
// reset the context
killProcesses()
// reset the context
killProcesses()
// Resolve the server address
var resolved: Boolean = false
if (!Utils.isNumeric(config.proxy)) {
Utils.resolve(config.proxy, enableIPv6 = true) match {
case Some(addr) =>
config.proxy = addr
resolved = true
case None => resolved = false
}
} else {
resolved = true
// Resolve the server address
var resolved: Boolean = false
if (!Utils.isNumeric(profile.host)) {
Utils.resolve(profile.host, enableIPv6 = true) match {
case Some(addr) =>
profile.host = addr
resolved = true
case None => resolved = false
}
} else {
resolved = true
}
if (!resolved) {
changeState(State.STOPPED, getString(R.string.invalid_server))
stopRunner(true)
} else if (handleConnection) {
changeState(State.CONNECTED)
notification = new ShadowsocksNotification(this, config.profileName)
} else {
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner(true)
}
if (!resolved) {
changeState(State.STOPPED, getString(R.string.invalid_server))
stopRunner(true)
} else if (handleConnection) {
changeState(State.CONNECTED)
notification = new ShadowsocksNotification(this, profile.name)
} else {
changeState(State.STOPPED, getString(R.string.service_failed))
stopRunner(true)
}
}
}
......@@ -199,7 +181,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
/** Called when the activity is first created. */
def handleConnection: Boolean = {
startShadowsocksDaemon()
if (!config.isUdpDns) {
if (!profile.udpdns) {
startDnsDaemon()
startDnsTunnel()
}
......@@ -210,22 +192,22 @@ class ShadowsocksVpnService extends VpnService with BaseService {
def startShadowsocksDaemon() {
if (config.route != Route.ALL) {
val acl: Array[Array[String]] = config.route match {
if (profile.route != Route.ALL) {
val acl: Array[Array[String]] = profile.route match {
case Route.BYPASS_LAN => Array(getResources.getStringArray(R.array.private_route))
case Route.BYPASS_CHN => Array(getResources.getStringArray(R.array.chn_route))
case Route.BYPASS_LAN_CHN =>
Array(getResources.getStringArray(R.array.private_route), getResources.getStringArray(R.array.chn_route))
}
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/acl.list"))(p => {
Utils.printToFile(new File(getApplicationInfo.dataDir + "/acl.list"))(p => {
acl.flatten.foreach(p.println)
})
}
val conf = ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, config.proxy, config.remotePort, config.localPort,
config.sitekey, config.encMethod, 600)
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-vpn.conf"))(p => {
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, profile.localPort,
profile.password, profile.method, 600)
Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-local-vpn.conf"))(p => {
p.println(conf)
})
......@@ -236,9 +218,9 @@ class ShadowsocksVpnService extends VpnService with BaseService {
, "-P", getApplicationInfo.dataDir
, "-c", getApplicationInfo.dataDir + "/ss-local-vpn.conf")
if (config.isAuth) cmd += "-A"
if (profile.auth) cmd += "-A"
if (config.route != Route.ALL) {
if (profile.route != Route.ALL) {
cmd += "--acl"
cmd += (getApplicationInfo.dataDir + "/acl.list")
}
......@@ -250,9 +232,9 @@ class ShadowsocksVpnService extends VpnService with BaseService {
def startDnsTunnel() = {
val conf = ConfigUtils
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, config.proxy, config.remotePort, 8163,
config.sitekey, config.encMethod, 10)
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf"))(p => {
.SHADOWSOCKS.formatLocal(Locale.ENGLISH, profile.host, profile.remotePort, 8163,
profile.password, profile.method, 10)
Utils.printToFile(new File(getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf"))(p => {
p.println(conf)
})
val cmd = new ArrayBuffer[String]
......@@ -266,7 +248,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
, "-P", getApplicationInfo.dataDir
, "-c", getApplicationInfo.dataDir + "/ss-tunnel-vpn.conf")
if (config.isAuth) cmd += "-A"
if (profile.auth) cmd += "-A"
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
......@@ -274,18 +256,17 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
def startDnsDaemon() {
val ipv6 = if (config.isIpv6) "" else "reject = ::/0;"
val ipv6 = if (profile.ipv6) "" else "reject = ::/0;"
val conf = {
if (config.route == Route.BYPASS_CHN || config.route == Route.BYPASS_LAN_CHN) {
val blackList = ConfigUtils.getBlackList(this)
if (profile.route == Route.BYPASS_CHN || profile.route == Route.BYPASS_LAN_CHN) {
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
"0.0.0.0", 8153, blackList, 8163, ipv6)
"0.0.0.0", 8153, getBlackList, 8163, ipv6)
} else {
ConfigUtils.PDNSD_LOCAL.formatLocal(Locale.ENGLISH, getApplicationInfo.dataDir,
"0.0.0.0", 8153, 8163, ipv6)
}
}
ConfigUtils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-vpn.conf"))(p => {
Utils.printToFile(new File(getApplicationInfo.dataDir + "/pdnsd-vpn.conf"))(p => {
p.println(conf)
})
val cmd = getApplicationInfo.dataDir + "/pdnsd -c " + getApplicationInfo.dataDir + "/pdnsd-vpn.conf"
......@@ -299,22 +280,22 @@ class ShadowsocksVpnService extends VpnService with BaseService {
val builder = new Builder()
builder
.setSession(config.profileName)
.setSession(profile.name)
.setMtu(VPN_MTU)
.addAddress(PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"), 24)
.addDnsServer("8.8.8.8")
if (config.isIpv6) {
if (profile.ipv6) {
builder.addAddress(PRIVATE_VLAN6.formatLocal(Locale.ENGLISH, "1"), 126)
builder.addRoute("::", 0)
}
if (Utils.isLollipopOrAbove) {
if (config.isProxyApps) {
for (pkg <- config.proxiedAppString.split('\n')) {
if (profile.proxyApps) {
for (pkg <- profile.individual.split('\n')) {
try {
if (!config.isBypassApps) {
if (!profile.bypass) {
builder.addAllowedApplication(pkg)
} else {
builder.addDisallowedApplication(pkg)
......@@ -327,7 +308,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
}
if (config.route == Route.ALL || config.route == Route.BYPASS_CHN) {
if (profile.route == Route.ALL || profile.route == Route.BYPASS_CHN) {
builder.addRoute("0.0.0.0", 0)
} else {
val privateList = getResources.getStringArray(R.array.bypass_private_route)
......@@ -368,12 +349,12 @@ class ShadowsocksVpnService extends VpnService with BaseService {
+ "--loglevel 3")
.formatLocal(Locale.ENGLISH,
PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "2"),
config.localPort, fd, VPN_MTU, getApplicationInfo.dataDir + "/sock_path")
profile.localPort, fd, VPN_MTU, getApplicationInfo.dataDir + "/sock_path")
if (config.isIpv6)
if (profile.ipv6)
cmd += " --netif-ip6addr " + PRIVATE_VLAN6.formatLocal(Locale.ENGLISH, "2")
if (config.isUdpDns)
if (profile.udpdns)
cmd += " --enable-udprelay"
else
cmd += " --dnsgw %s:8153".formatLocal(Locale.ENGLISH, PRIVATE_VLAN.formatLocal(Locale.ENGLISH, "1"))
......
......@@ -39,16 +39,14 @@
package com.github.shadowsocks.database
import android.content.SharedPreferences
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.Key
object ProfileManager {
private final val TAG = "ProfileManager"
}
class ProfileManager(settings: SharedPreferences, dbHelper: DBHelper) {
class ProfileManager(dbHelper: DBHelper) {
import ProfileManager._
var profileAddedListener: Profile => Any = _
......@@ -139,67 +137,6 @@ class ProfileManager(settings: SharedPreferences, dbHelper: DBHelper) {
}
}
def reload(id: Int): Profile = {
save()
load(id)
}
def load(id: Int): Profile = {
val profile = getProfile(id) getOrElse createProfile()
val edit = settings.edit()
edit.putBoolean(Key.isProxyApps, profile.proxyApps)
edit.putBoolean(Key.isBypassApps, profile.bypass)
edit.putBoolean(Key.isUdpDns, profile.udpdns)
edit.putBoolean(Key.isAuth, profile.auth)
edit.putBoolean(Key.isIpv6, profile.ipv6)
edit.putString(Key.profileName, profile.name)
edit.putString(Key.proxy, profile.host)
edit.putString(Key.sitekey, profile.password)
edit.putString(Key.encMethod, profile.method)
edit.putInt(Key.remotePort, profile.remotePort)
edit.putInt(Key.localPort, profile.localPort)
edit.putString(Key.proxied, profile.individual)
edit.putInt(Key.profileId, profile.id)
edit.putString(Key.route, profile.route)
edit.apply()
profile
}
private def loadFromPreferences: Profile = {
val id = settings.getInt(Key.profileId, -1)
val profile: Profile = getProfile(id) match {
case Some(p) => p
case _ => new Profile()
}
profile.proxyApps = settings.getBoolean(Key.isProxyApps, false)
profile.bypass = settings.getBoolean(Key.isBypassApps, false)
profile.udpdns = settings.getBoolean(Key.isUdpDns, false)
profile.auth = settings.getBoolean(Key.isAuth, false)
profile.ipv6 = settings.getBoolean(Key.isIpv6, false)
profile.name = settings.getString(Key.profileName, "default")
profile.host = settings.getString(Key.proxy, "127.0.0.1")
profile.password = settings.getString(Key.sitekey, "default")
profile.method = settings.getString(Key.encMethod, "table")
profile.route = settings.getString(Key.route, "all")
profile.remotePort = settings.getInt(Key.remotePort, 1984)
profile.localPort = settings.getInt(Key.localPort, 1984)
profile.individual = settings.getString(Key.proxied, "")
profile
}
def save(): Profile = {
val profile = loadFromPreferences
updateProfile(profile)
profile
}
def createDefault(): Profile = {
val profile = new Profile {
name = "Default"
......
/*
* Shadowsocks - A shadowsocks client for Android
* Copyright (C) 2014 <max.c.lv@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*
* ___====-_ _-====___
* _--^^^#####// \\#####^^^--_
* _-^##########// ( ) \\##########^-_
* -############// |\^^/| \\############-
* _/############// (@::@) \\############\_
* /#############(( \\// ))#############\
* -###############\\ (oo) //###############-
* -#################\\ / VV \ //#################-
* -###################\\/ \//###################-
* _#/|##########/\######( /\ )######/\##########|\#_
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
* ` ` ` ` / | | | | \ ' ' ' '
* ( | | | | )
* __\ | | | | /__
* (vvv(VVV)(VVV)vvv)
*
* HERE BE DRAGONS
*
*/
package com.github.shadowsocks.utils
import android.content.Context
import com.github.kevinsawicki.http.HttpRequest
import com.github.shadowsocks.R
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.aidl.Config
import com.google.android.gms.tagmanager.Container
object ConfigUtils {
val SHADOWSOCKS = "{\"server\": \"%s\", \"server_port\": %d, \"local_port\": %d, \"password\": \"%s\", \"method\":\"%s\", \"timeout\": %d}"
val REDSOCKS = "base {\n" +
" log_debug = off;\n" +
" log_info = off;\n" +
" log = stderr;\n" +
" daemon = off;\n" +
" redirector = iptables;\n" +
"}\n" +
"redsocks {\n" +
" local_ip = 127.0.0.1;\n" +
" local_port = 8123;\n" +
" ip = 127.0.0.1;\n" +
" port = %d;\n" +
" type = socks5;\n" +
"}\n"
val PDNSD_LOCAL =
"""
|global {
| perm_cache = 2048;
| cache_dir = "%s";
| server_ip = %s;
| server_port = %d;
| query_method = tcp_only;
| run_ipv4 = on;
| min_ttl = 15m;
| max_ttl = 1w;
| timeout = 10;
| daemon = off;
|}
|
|server {
| label = "local";
| ip = 127.0.0.1;
| port = %d;
| %s
| reject_policy = negate;
| reject_recursively = on;
| timeout = 5;
|}
|
|rr {
| name=localhost;
| reverse=on;
| a=127.0.0.1;
| owner=localhost;
| soa=localhost,root.localhost,42,86400,900,86400,86400;
|}
""".stripMargin
val PDNSD_DIRECT =
"""
|global {
| perm_cache = 2048;
| cache_dir = "%s";
| server_ip = %s;
| server_port = %d;
| query_method = tcp_only;
| run_ipv4 = on;
| min_ttl = 15m;
| max_ttl = 1w;
| timeout = 10;
| daemon = off;
|}
|
|server {
| label = "china-servers";
| ip = 114.114.114.114, 112.124.47.27;
| timeout = 4;
| exclude = %s;
| policy = included;
| uptest = none;
| preset = on;
|}
|
|server {
| label = "local-server";
| ip = 127.0.0.1;
| port = %d;
| %s
| reject_policy = negate;
| reject_recursively = on;
|}
|
|rr {
| name=localhost;
| reverse=on;
| a=127.0.0.1;
| owner=localhost;
| soa=localhost,root.localhost,42,86400,900,86400,86400;
|}
""".stripMargin
def printToFile(f: java.io.File)(op: java.io.PrintWriter => Unit) {
val p = new java.io.PrintWriter(f)
try {
op(p)
} finally {
p.close()
}
}
def refresh(context: Context) {
val holder = app.containerHolder
if (holder != null) holder.refresh()
}
def getBlackList(context: Context): String = {
val default = context.getString(R.string.black_list)
try {
val container = app.containerHolder.getContainer
val update = container.getString("black_list")
if (update == null || update.isEmpty) default else update
} catch {
case ex: Exception => default
}
}
def getPublicConfig(context: Context, container: Container, config: Config): Config = {
val url = container.getString("proxy_url")
val sig = Utils.getSignature(context)
val list = HttpRequest
.post(url)
.connectTimeout(2000)
.readTimeout(2000)
.send("sig="+sig)
.body
val proxies = util.Random.shuffle(list.split('|').toSeq)
val proxy = proxies.head.split(':')
val host = proxy(0).trim
val port = proxy(1).trim.toInt
val password = proxy(2).trim
val method = proxy(3).trim
new Config(config.isProxyApps, config.isBypassApps, config.isUdpDns, config.isAuth, config.isIpv6,
config.profileName, host, password, method, config.proxiedAppString, config.route, port, config.localPort, 0)
}
def loadFromSharedPreferences = {
val isProxyApps = app.settings.getBoolean(Key.isProxyApps, false)
val isBypassApps = app.settings.getBoolean(Key.isBypassApps, false)
val isUdpDns = app.settings.getBoolean(Key.isUdpDns, false)
val isAuth = app.settings.getBoolean(Key.isAuth, false)
val isIpv6 = app.settings.getBoolean(Key.isIpv6, false)
val profileName = app.settings.getString(Key.profileName, "default")
val proxy = app.settings.getString(Key.proxy, "127.0.0.1")
val sitekey = app.settings.getString(Key.sitekey, "default")
val encMethod = app.settings.getString(Key.encMethod, "table")
val route = app.settings.getString(Key.route, "all")
val remotePort = app.settings.getInt(Key.remotePort, 1984)
val localPort = app.settings.getInt(Key.localPort, 1984)
val proxiedAppString = app.settings.getString(Key.proxied, "")
val profileId = app.settings.getInt(Key.profileId, -1)
new Config(isProxyApps, isBypassApps, isUdpDns, isAuth, isIpv6, profileName, proxy, sitekey, encMethod,
proxiedAppString, route, remotePort, localPort, profileId)
}
}
......@@ -47,6 +47,101 @@ object Executable {
val TUN2SOCKS = "tun2socks"
}
object ConfigUtils {
val SHADOWSOCKS = "{\"server\": \"%s\", \"server_port\": %d, \"local_port\": %d, \"password\": \"%s\", \"method\":\"%s\", \"timeout\": %d}"
val REDSOCKS = "base {\n" +
" log_debug = off;\n" +
" log_info = off;\n" +
" log = stderr;\n" +
" daemon = off;\n" +
" redirector = iptables;\n" +
"}\n" +
"redsocks {\n" +
" local_ip = 127.0.0.1;\n" +
" local_port = 8123;\n" +
" ip = 127.0.0.1;\n" +
" port = %d;\n" +
" type = socks5;\n" +
"}\n"
val PDNSD_LOCAL =
"""
|global {
| perm_cache = 2048;
| cache_dir = "%s";
| server_ip = %s;
| server_port = %d;
| query_method = tcp_only;
| run_ipv4 = on;
| min_ttl = 15m;
| max_ttl = 1w;
| timeout = 10;
| daemon = off;
|}
|
|server {
| label = "local";
| ip = 127.0.0.1;
| port = %d;
| %s
| reject_policy = negate;
| reject_recursively = on;
| timeout = 5;
|}
|
|rr {
| name=localhost;
| reverse=on;
| a=127.0.0.1;
| owner=localhost;
| soa=localhost,root.localhost,42,86400,900,86400,86400;
|}
""".stripMargin
val PDNSD_DIRECT =
"""
|global {
| perm_cache = 2048;
| cache_dir = "%s";
| server_ip = %s;
| server_port = %d;
| query_method = tcp_only;
| run_ipv4 = on;
| min_ttl = 15m;
| max_ttl = 1w;
| timeout = 10;
| daemon = off;
|}
|
|server {
| label = "china-servers";
| ip = 114.114.114.114, 112.124.47.27;
| timeout = 4;
| exclude = %s;
| policy = included;
| uptest = none;
| preset = on;
|}
|
|server {
| label = "local-server";
| ip = 127.0.0.1;
| port = %d;
| %s
| reject_policy = negate;
| reject_recursively = on;
|}
|
|rr {
| name=localhost;
| reverse=on;
| a=127.0.0.1;
| owner=localhost;
| soa=localhost,root.localhost,42,86400,900,86400,86400;
|}
""".stripMargin
}
object Key {
val profileId = "profileId"
val profileName = "profileName"
......
......@@ -120,6 +120,15 @@ object Utils {
})
}
def printToFile(f: java.io.File)(op: java.io.PrintWriter => Unit) {
val p = new java.io.PrintWriter(f)
try {
op(p)
} finally {
p.close()
}
}
// Blocked > 3 seconds
def toggleAirplaneMode(context: Context) = {
if (Console.isRoot) {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment