diff --git a/src/main/res/drawable-v21/background_selectable.xml b/src/main/res/drawable-v21/background_selectable.xml
new file mode 100644
index 00000000..caca653d
--- /dev/null
+++ b/src/main/res/drawable-v21/background_selectable.xml
@@ -0,0 +1,11 @@
+
+
+ -
+
+
-
+
+
+
+
+
diff --git a/src/main/res/drawable/background_selectable.xml b/src/main/res/drawable/background_selectable.xml
new file mode 100644
index 00000000..e4b7a428
--- /dev/null
+++ b/src/main/res/drawable/background_selectable.xml
@@ -0,0 +1,8 @@
+
+
+ -
+
+
+
diff --git a/src/main/res/drawable/ic_action_assignment.xml b/src/main/res/drawable/ic_action_assignment.xml
new file mode 100644
index 00000000..4a3eb4fe
--- /dev/null
+++ b/src/main/res/drawable/ic_action_assignment.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/main/res/drawable/ic_av_playlist_add.xml b/src/main/res/drawable/ic_av_playlist_add.xml
new file mode 100644
index 00000000..37a0ca08
--- /dev/null
+++ b/src/main/res/drawable/ic_av_playlist_add.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/main/res/layout/dialog_acl_rule.xml b/src/main/res/layout/dialog_acl_rule.xml
new file mode 100644
index 00000000..68e30199
--- /dev/null
+++ b/src/main/res/layout/dialog_acl_rule.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
diff --git a/src/main/res/layout/layout_apps.xml b/src/main/res/layout/layout_apps.xml
index 7b89ca48..ec8d350e 100644
--- a/src/main/res/layout/layout_apps.xml
+++ b/src/main/res/layout/layout_apps.xml
@@ -47,6 +47,7 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/applistview"
+ android:scrollbars="vertical"
android:visibility="gone"/>
diff --git a/src/main/res/layout/layout_profiles.xml b/src/main/res/layout/layout_list.xml
similarity index 86%
rename from src/main/res/layout/layout_profiles.xml
rename to src/main/res/layout/layout_list.xml
index 4e90d3bb..bfe42dca 100644
--- a/src/main/res/layout/layout_profiles.xml
+++ b/src/main/res/layout/layout_list.xml
@@ -5,11 +5,12 @@
android:orientation="vertical">
diff --git a/src/main/res/layout/layout_tasker.xml b/src/main/res/layout/layout_tasker.xml
index c33cedeb..02662ed8 100644
--- a/src/main/res/layout/layout_tasker.xml
+++ b/src/main/res/layout/layout_tasker.xml
@@ -15,7 +15,8 @@
android:textSize="18sp"
android:padding="16dp"/>
-
+ android:layout_height="match_parent"
+ android:scrollbars="vertical"/>
diff --git a/src/main/res/menu/custom_rules_menu.xml b/src/main/res/menu/custom_rules_menu.xml
new file mode 100644
index 00000000..380c254f
--- /dev/null
+++ b/src/main/res/menu/custom_rules_menu.xml
@@ -0,0 +1,54 @@
+
+
diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml
index 2ac3360a..e43c0af1 100644
--- a/src/main/res/values-ja/strings.xml
+++ b/src/main/res/values-ja/strings.xml
@@ -88,13 +88,15 @@
QRコードを読み取る
手動でセットアップ
+ 中国本土からアクセス不可なアドレス以外を迂回する
- すべて
- LANアドレスを迂回する
- 中国本土のアドレスを迂回する
- LAN及び中国本土のアドレスを迂回する
- - 中国本土からアクセス不可なアドレス以外を迂回する
+ - @string/route_entry_gfwlist
- 中国本土のアドレス以外を迂回する
+ - @string/custom_rules
設定をすべてのプロファイルに適応する
diff --git a/src/main/res/values-ru/strings.xml b/src/main/res/values-ru/strings.xml
index 1c63b9b5..be04a638 100644
--- a/src/main/res/values-ru/strings.xml
+++ b/src/main/res/values-ru/strings.xml
@@ -41,13 +41,15 @@
CHN Маршрут
Игнорировать все сайты находящиеся в Китае
Маршрут
+ Список GFW
- Все
- Обход LAN
- Обходной Китай
- Обход LAN и Китай
- - Список GFW
+ - @string/route_entry_gfwlist
- Список Китай
+ - @string/custom_rules
Прокси для выбранных приложений
Установить прокси для выбранных приложений, требует режим NAT под Android 4.x
diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml
index 5047b896..171b0eea 100644
--- a/src/main/res/values-zh-rCN/strings.xml
+++ b/src/main/res/values-zh-rCN/strings.xml
@@ -100,13 +100,15 @@
手动设置
请安装任意兼容 ZXing 的二维码扫描应用。
+ 仅代理中国大陆无法访问的地址
- 全局
- 绕过局域网地址
- 绕过中国大陆地址
- 绕过局域网及中国大陆地址
- - 仅代理中国大陆无法访问的地址
+ - @string/route_entry_gfwlist
- 仅代理中国大陆地址
+ - @string/custom_rules
应用设置到所有配置文件
@@ -142,4 +144,14 @@
%d(远端 KCPTUN 服务器的端口号)
KCP 参数
+
+ 自定义规则
+ 选择…
+ 添加规则…
+
+ - 子网/域名 PCRE 正则表达式
+ - 域名及其子域名
+
+ 编辑规则
+
diff --git a/src/main/res/values-zh-rTW/strings.xml b/src/main/res/values-zh-rTW/strings.xml
index 9d97cb2c..4a5e8614 100644
--- a/src/main/res/values-zh-rTW/strings.xml
+++ b/src/main/res/values-zh-rTW/strings.xml
@@ -101,13 +101,15 @@
手動設定
請安裝任何兼容 ZXing 的 QR 碼掃描應用程式。
+ GFW List
- 全域
- 略過區域網路位址
- 略過中國大陸位址
- 略過區域網路及中國大陸位址
- - 僅代理大陸屏蔽位址
+ - @string/route_entry_gfwlist
- 僅代理大陸位址
+ - @string/custom_rules
應用設置到所有配置文件
@@ -143,4 +145,14 @@
%d(遠端 KCPTUN 伺服器的連接埠號碼)
KCP 參數
+
+ 自定義規則
+ 選擇…
+ 添加規則…
+
+ - 子網/主機名 PCRE 正則表達式
+ - 域名及其所有子域名
+
+ 編輯規則
+
diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml
index 7d2b4273..7fd25301 100644
--- a/src/main/res/values/arrays.xml
+++ b/src/main/res/values/arrays.xml
@@ -45,6 +45,7 @@
- bypass-lan-china
- gfwlist
- china-list
+ - custom-rules
diff --git a/src/main/res/values/colors.xml b/src/main/res/values/colors.xml
index d0374a8a..549c9a08 100644
--- a/src/main/res/values/colors.xml
+++ b/src/main/res/values/colors.xml
@@ -4,9 +4,11 @@
#388E3C
#00C853
+ #CFD8DC
#90A4AE
#607D8B
#455A64
+ @color/material_blue_grey_100
@color/material_blue_grey_300
@color/material_blue_grey_500
@color/material_blue_grey_700
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 3688f22e..e0310463 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -48,13 +48,15 @@
Bypass all sites located in China
Route
+ GFW List
- All
- Bypass LAN
- Bypass China
- Bypass LAN & China
- - GFW List
+ - @string/route_entry_gfwlist
- China List
+ - @string/custom_rules
Per-App Proxy
Set proxy for selected apps, requires NAT mode under Android 4.x
@@ -156,4 +158,14 @@
KCP Port
%d (port number of the remote KCPTUN server)
KCP Parameters (CLI)
+
+
+ Custom rules
+ Selection…
+ Add rule(s)…
+
+ - Subnet/Hostname PCRE pattern
+ - Domain name and all its subdomain names
+
+ Edit rule
diff --git a/src/main/scala/com/github/shadowsocks/AppManager.scala b/src/main/scala/com/github/shadowsocks/AppManager.scala
index e2cf7bb7..c3818e17 100644
--- a/src/main/scala/com/github/shadowsocks/AppManager.scala
+++ b/src/main/scala/com/github/shadowsocks/AppManager.scala
@@ -221,7 +221,7 @@ class AppManager extends AppCompatActivity with OnMenuItemClickListener {
initProxiedApps()
loadingView = findViewById(R.id.loading)
appListView = findViewById(R.id.applistview).asInstanceOf[RecyclerView]
- appListView.setLayoutManager(new LinearLayoutManager(this))
+ appListView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false))
appListView.setItemAnimator(new DefaultItemAnimator)
instance = this
diff --git a/src/main/scala/com/github/shadowsocks/BaseService.scala b/src/main/scala/com/github/shadowsocks/BaseService.scala
index f4ccc635..204c3df1 100644
--- a/src/main/scala/com/github/shadowsocks/BaseService.scala
+++ b/src/main/scala/com/github/shadowsocks/BaseService.scala
@@ -34,6 +34,7 @@ import android.text.TextUtils
import android.util.Log
import android.widget.Toast
import com.github.shadowsocks.ShadowsocksApplication.app
+import com.github.shadowsocks.acl.Acl
import com.github.shadowsocks.aidl.{IShadowsocksService, IShadowsocksServiceCallback}
import com.github.shadowsocks.database.Profile
import com.github.shadowsocks.utils._
@@ -132,38 +133,39 @@ trait BaseService extends Service {
false
} else true
- def connect(): Unit = if (profile.host == "198.199.101.152") {
- val holder = app.containerHolder
- val container = holder.getContainer
- val url = container.getString("proxy_url")
- val sig = Utils.getSignature(this)
+ def connect() {
+ if (profile.host == "198.199.101.152") {
+ val holder = app.containerHolder
+ val container = holder.getContainer
+ val url = container.getString("proxy_url")
+ val sig = Utils.getSignature(this)
- val client = new OkHttpClient.Builder()
- .dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
- case Some(ip) => util.Arrays.asList(InetAddress.getByName(ip))
- case _ => Dns.SYSTEM.lookup(hostname)
- })
- .connectTimeout(10, TimeUnit.SECONDS)
- .writeTimeout(10, TimeUnit.SECONDS)
- .readTimeout(30, TimeUnit.SECONDS)
- .build()
- val requestBody = new FormBody.Builder()
- .add("sig", sig)
- .build()
- val request = new Request.Builder()
- .url(url)
- .post(requestBody)
- .build()
+ val client = new OkHttpClient.Builder()
+ .dns(hostname => Utils.resolve(hostname, enableIPv6 = false) match {
+ case Some(ip) => util.Arrays.asList(InetAddress.getByName(ip))
+ case _ => Dns.SYSTEM.lookup(hostname)
+ })
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .writeTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(30, TimeUnit.SECONDS)
+ .build()
+ val requestBody = new FormBody.Builder()
+ .add("sig", sig)
+ .build()
+ val request = new Request.Builder()
+ .url(url)
+ .post(requestBody)
+ .build()
- val resposne = client.newCall(request).execute()
- val list = resposne.body.string
-
- val proxies = 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
+ val proxies = Random.shuffle(client.newCall(request).execute().body.string.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
+ }
+ if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules
+ Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES))
}
def startRunner(profile: Profile) {
diff --git a/src/main/scala/com/github/shadowsocks/MainActivity.scala b/src/main/scala/com/github/shadowsocks/MainActivity.scala
index 76222fe1..8aba2462 100644
--- a/src/main/scala/com/github/shadowsocks/MainActivity.scala
+++ b/src/main/scala/com/github/shadowsocks/MainActivity.scala
@@ -43,6 +43,7 @@ import android.view.View
import android.widget.{TextView, Toast}
import com.github.jorgecastilloprz.FABProgressCircle
import com.github.shadowsocks.ShadowsocksApplication.app
+import com.github.shadowsocks.acl.CustomRulesFragment
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
import com.github.shadowsocks.utils.CloseUtils.autoDisconnect
import com.github.shadowsocks.utils._
@@ -62,6 +63,7 @@ object MainActivity {
private final val DRAWER_RECOVERY = 2L
private final val DRAWER_ABOUT = 3L
private final val DRAWER_FAQ = 4L
+ private final val DRAWER_CUSTOM_RULES = 5L
}
class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawerItemClickListener
@@ -84,6 +86,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
private var currentFragment: ToolbarFragment = _
private lazy val profilesFragment = new ProfilesFragment()
+ private lazy val customRulesFragment = new CustomRulesFragment()
private lazy val globalSettingsFragment = new GlobalSettingsFragment()
private lazy val aboutFragment = new AboutFragment()
private lazy val customTabsIntent = new CustomTabsIntent.Builder()
@@ -193,6 +196,11 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
.withName(R.string.profiles)
.withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_description))
.withIconTintingEnabled(true),
+ new PrimaryDrawerItem()
+ .withIdentifier(DRAWER_CUSTOM_RULES)
+ .withName(R.string.custom_rules)
+ .withIcon(AppCompatResources.getDrawable(this, R.drawable.ic_action_assignment))
+ .withIconTintingEnabled(true),
new PrimaryDrawerItem()
.withIdentifier(DRAWER_GLOBAL_SETTINGS)
.withName(R.string.settings)
@@ -377,6 +385,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
app.track(TAG, "about")
displayFragment(aboutFragment)
case DRAWER_FAQ => launchUrl(getString(R.string.faq_url))
+ case DRAWER_CUSTOM_RULES => displayFragment(customRulesFragment)
}
true // unexpected cases will throw exception
}
diff --git a/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala b/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala
index 87c467eb..6a706f8b 100644
--- a/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala
+++ b/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala
@@ -71,7 +71,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
case _ => false
}
- final class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view)
+ final class ProfileViewHolder(view: View) extends RecyclerView.ViewHolder(view)
with View.OnClickListener with PopupMenu.OnMenuItemClickListener {
var item: Profile = _
@@ -256,7 +256,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
.putExtra(Action.EXTRA_PROFILE_ID, id))
override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
- inflater.inflate(R.layout.layout_profiles, container, false)
+ inflater.inflate(R.layout.layout_list, container, false)
override def onViewCreated(view: View, savedInstanceState: Bundle) {
super.onViewCreated(view, savedInstanceState)
@@ -265,18 +265,19 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
toolbar.setOnMenuItemClickListener(this)
if (app.profileManager.getFirstProfile.isEmpty) app.profileId(app.profileManager.createProfile().id)
- val profilesList = view.findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
- val layoutManager = new LinearLayoutManager(getActivity)
+ val profilesList = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
+ val layoutManager = new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false)
profilesList.setLayoutManager(layoutManager)
+ layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
+ case (profile, i) if profile.id == app.profileId => i
+ }.getOrElse(-1))
val animator = new DefaultItemAnimator()
animator.setSupportsChangeAnimations(false) // prevent fading-in/out when rebinding
profilesList.setItemAnimator(animator)
profilesList.setAdapter(profilesAdapter)
instance = this
- layoutManager.scrollToPosition(profilesAdapter.profiles.zipWithIndex.collectFirst {
- case (profile, i) if profile.id == app.profileId => i
- }.getOrElse(-1))
- undoManager = new UndoSnackbarManager[Profile](getActivity.findViewById(R.id.snackbar), profilesAdapter.undo, profilesAdapter.commit)
+ undoManager = new UndoSnackbarManager[Profile](getActivity.findViewById(R.id.snackbar),
+ profilesAdapter.undo, profilesAdapter.commit)
new ItemTouchHelper(new SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.START | ItemTouchHelper.END) {
override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
@@ -288,7 +289,7 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
def onSwiped(viewHolder: ViewHolder, direction: Int) {
val index = viewHolder.getAdapterPosition
profilesAdapter.remove(index)
- undoManager.remove(index, viewHolder.asInstanceOf[ProfileViewHolder].item)
+ undoManager.remove((index, viewHolder.asInstanceOf[ProfileViewHolder].item))
}
def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean = {
profilesAdapter.move(viewHolder.getAdapterPosition, target.getAdapterPosition)
diff --git a/src/main/scala/com/github/shadowsocks/QRCodeDialog.scala b/src/main/scala/com/github/shadowsocks/QRCodeDialog.scala
index a6da1fd0..fe5f6101 100644
--- a/src/main/scala/com/github/shadowsocks/QRCodeDialog.scala
+++ b/src/main/scala/com/github/shadowsocks/QRCodeDialog.scala
@@ -30,7 +30,18 @@ import android.widget.{ImageView, LinearLayout}
import com.github.shadowsocks.utils.Utils
import net.glxn.qrgen.android.QRCode
-final class QRCodeDialog(url: String) extends DialogFragment {
+object QRCodeDialog {
+ private final val KEY_URL = "com.github.shadowsocks.QRCodeDialog.KEY_URL"
+}
+
+final class QRCodeDialog extends DialogFragment {
+ import QRCodeDialog._
+ def this(url: String) {
+ this()
+ getArguments.putString(KEY_URL, url)
+ }
+ private def url = getArguments.getString(KEY_URL)
+
private lazy val nfcShareItem = url.getBytes(Charset.forName("UTF-8"))
private var adapter: NfcAdapter = _
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
index 2bd45095..4933b3a7 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksApplication.scala
@@ -34,8 +34,8 @@ import android.preference.PreferenceManager
import android.support.v7.app.AppCompatDelegate
import android.util.Log
import com.evernote.android.job.JobManager
+import com.github.shadowsocks.acl.DonaldTrump
import com.github.shadowsocks.database.{DBHelper, Profile, ProfileManager}
-import com.github.shadowsocks.job.DonaldTrump
import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils._
import com.google.android.gms.analytics.{GoogleAnalytics, HitBuilders, StandardExceptionParser, Tracker}
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
index dc554c00..1f62ff1e 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
@@ -28,8 +28,8 @@ import android.content._
import android.os._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
+import com.github.shadowsocks.acl.{AclSyncJob, Acl}
import com.github.shadowsocks.database.Profile
-import com.github.shadowsocks.job.AclSyncJob
import com.github.shadowsocks.utils._
import eu.chainfire.libsuperuser.Shell
@@ -76,9 +76,9 @@ class ShadowsocksNatService extends BaseService {
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
- if (profile.route != Route.ALL) {
+ if (profile.route != Acl.ALL) {
cmd += "--acl"
- cmd += getApplicationInfo.dataDir + '/' + profile.route + ".acl"
+ cmd += Acl.getPath(profile.route)
}
if (BuildConfig.DEBUG) Log.d(TAG, cmd.mkString(" "))
@@ -169,11 +169,11 @@ class ShadowsocksNatService extends BaseService {
val reject = if (profile.ipv6) "224.0.0.0/3" else "224.0.0.0/3, ::/0"
val conf = profile.route match {
- case Route.BYPASS_CHN | Route.BYPASS_LAN_CHN | Route.GFWLIST =>
+ case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
"127.0.0.1", profile.localPort + 53, "114.114.114.114, 223.5.5.5, 1.2.4.8",
getBlackList, reject, profile.localPort + 63, reject)
- case Route.CHINALIST =>
+ case Acl.CHINALIST =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, "", getApplicationInfo.dataDir,
"127.0.0.1", profile.localPort + 53, "8.8.8.8, 8.8.4.4, 208.67.222.222",
"", reject, profile.localPort + 63, reject)
@@ -311,7 +311,7 @@ class ShadowsocksNatService extends BaseService {
handleConnection()
- if (profile.route != Route.ALL)
+ if (profile.route != Acl.ALL)
AclSyncJob.schedule(profile.route)
changeState(State.CONNECTED)
diff --git a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
index db3b65fd..08c5cd64 100644
--- a/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
+++ b/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
@@ -30,8 +30,8 @@ import android.net.VpnService
import android.os._
import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
+import com.github.shadowsocks.acl.{AclSyncJob, Acl}
import com.github.shadowsocks.database.Profile
-import com.github.shadowsocks.job.AclSyncJob
import com.github.shadowsocks.utils._
import scala.collection.mutable.ArrayBuffer
@@ -145,7 +145,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
handleConnection()
changeState(State.CONNECTED)
- if (profile.route != Route.ALL)
+ if (profile.route != Acl.ALL)
AclSyncJob.schedule(profile.route)
notification = new ShadowsocksNotification(this, profile.getName)
@@ -241,9 +241,9 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.udpdns && !profile.kcp) cmd += "-u"
- if (profile.route != Route.ALL) {
+ if (profile.route != Acl.ALL) {
cmd += "--acl"
- cmd += getApplicationInfo.dataDir + '/' + profile.route + ".acl"
+ cmd += Acl.getPath(profile.route)
}
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
@@ -291,11 +291,11 @@ class ShadowsocksVpnService extends VpnService with BaseService {
val reject = if (profile.ipv6) "224.0.0.0/3" else "224.0.0.0/3, ::/0"
val protect = "protect = \"" + protectPath +"\";"
val conf = profile.route match {
- case Route.BYPASS_CHN | Route.BYPASS_LAN_CHN | Route.GFWLIST =>
+ case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
"0.0.0.0", profile.localPort + 53, "114.114.114.114, 223.5.5.5, 1.2.4.8",
getBlackList, reject, profile.localPort + 63, reject)
- case Route.CHINALIST =>
+ case Acl.CHINALIST =>
ConfigUtils.PDNSD_DIRECT.formatLocal(Locale.ENGLISH, protect, getApplicationInfo.dataDir,
"0.0.0.0", profile.localPort + 53, "8.8.8.8, 8.8.4.4, 208.67.222.222",
"", reject, profile.localPort + 63, reject)
@@ -347,7 +347,7 @@ class ShadowsocksVpnService extends VpnService with BaseService {
}
}
- if (profile.route == Route.ALL || profile.route == Route.BYPASS_CHN) {
+ if (profile.route == Acl.ALL || profile.route == Acl.BYPASS_CHN) {
builder.addRoute("0.0.0.0", 0)
} else {
val privateList = getResources.getStringArray(R.array.bypass_private_route)
diff --git a/src/main/scala/com/github/shadowsocks/TaskerActivity.scala b/src/main/scala/com/github/shadowsocks/TaskerActivity.scala
index c976287c..cb16cc48 100644
--- a/src/main/scala/com/github/shadowsocks/TaskerActivity.scala
+++ b/src/main/scala/com/github/shadowsocks/TaskerActivity.scala
@@ -35,7 +35,7 @@ import com.github.shadowsocks.ShadowsocksApplication.app
* @author CzBiX
*/
class TaskerActivity extends AppCompatActivity {
- private class ProfileViewHolder(val view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
+ private class ProfileViewHolder(view: View) extends RecyclerView.ViewHolder(view) with View.OnClickListener {
{
val typedArray = obtainStyledAttributes(Array(android.R.attr.selectableItemBackground))
view.setBackgroundResource(typedArray.getResourceId(0, 0))
@@ -92,8 +92,8 @@ class TaskerActivity extends AppCompatActivity {
taskerOption = TaskerSettings.fromIntent(getIntent)
switch = findViewById(R.id.serviceSwitch).asInstanceOf[Switch]
switch.setChecked(taskerOption.switchOn)
- val profilesList = findViewById(R.id.profilesList).asInstanceOf[RecyclerView]
- val lm = new LinearLayoutManager(this)
+ val profilesList = findViewById(R.id.list).asInstanceOf[RecyclerView]
+ val lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
profilesList.setLayoutManager(lm)
profilesList.setItemAnimator(new DefaultItemAnimator)
profilesList.setAdapter(profilesAdapter)
diff --git a/src/main/scala/com/github/shadowsocks/acl/Acl.scala b/src/main/scala/com/github/shadowsocks/acl/Acl.scala
new file mode 100644
index 00000000..6868f449
--- /dev/null
+++ b/src/main/scala/com/github/shadowsocks/acl/Acl.scala
@@ -0,0 +1,143 @@
+package com.github.shadowsocks.acl
+
+import java.io.FileNotFoundException
+
+import com.github.shadowsocks.ShadowsocksApplication.app
+import com.github.shadowsocks.utils.IOUtils
+import com.j256.ormlite.field.DatabaseField
+
+import scala.collection.mutable
+import scala.io.Source
+
+/**
+ * ACL handler compliant with: src/main/jni/shadowsocks-libev/src/acl.c
+ *
+ * OrmLite integration is unused for now.
+ *
+ * @author Mygod
+ */
+class Acl {
+ @DatabaseField(generatedId = true)
+ var id: Int = _
+ val bypassHostnames = new mutable.SortedList[String]()
+ val proxyHostnames = new mutable.SortedList[String]()
+ val subnets = new mutable.SortedList[Subnet]()
+ @DatabaseField
+ var bypass: Boolean = _
+
+ def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
+ def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
+ def getSubnetsString: String = subnets.mkString("\n")
+ def setBypassHostnamesString(value: String) {
+ bypassHostnames.clear()
+ bypassHostnames ++= value.split("\n")
+ }
+ def setProxyHostnamesString(value: String) {
+ proxyHostnames.clear()
+ proxyHostnames ++= value.split("\n")
+ }
+ def setSubnetsString(value: String) {
+ subnets.clear()
+ subnets ++= value.split("\n").map(Subnet.fromString)
+ }
+
+ def fromAcl(other: Acl): Acl = {
+ bypassHostnames.clear()
+ bypassHostnames ++= other.bypassHostnames
+ proxyHostnames.clear()
+ proxyHostnames ++= other.proxyHostnames
+ subnets.clear()
+ subnets ++= other.subnets
+ bypass = other.bypass
+ this
+ }
+ def fromSource(value: Source): Acl = {
+ bypassHostnames.clear()
+ proxyHostnames.clear()
+ this.subnets.clear()
+ bypass = false
+ lazy val bypassSubnets = new mutable.SortedList[Subnet]()
+ lazy val proxySubnets = new mutable.SortedList[Subnet]()
+ var hostnames: mutable.SortedList[String] = proxyHostnames
+ var subnets: mutable.SortedList[Subnet] = proxySubnets
+ for (line <- value.getLines()) (line.indexOf('#') match {
+ case -1 => line
+ case index => line.substring(0, index) // trim comments
+ }).trim match {
+ case "[outbound_block_list]" =>
+ hostnames = null
+ subnets = null
+ case "[black_list]" | "[bypass_list]" =>
+ hostnames = bypassHostnames
+ subnets = bypassSubnets
+ case "[white_list]" | "[proxy_list]" =>
+ hostnames = proxyHostnames
+ subnets = proxySubnets
+ case "[reject_all]" | "[bypass_all]" => bypass = true
+ case "[accept_all]" | "[proxy_all]" => bypass = false
+ case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
+ case _: IllegalArgumentException => hostnames += input
+ }
+ case _ =>
+ }
+ this.subnets ++= (if (bypass) bypassSubnets else proxySubnets)
+ this
+ }
+ final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getPath(id)))
+
+ override def toString: String = {
+ val result = new StringBuilder()
+ result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
+ var bypassList = bypassHostnames.toStream
+ var proxyList = proxyHostnames.toStream
+ if (bypass) bypassList = subnets.toStream.map(_.toString) #::: bypassList
+ else proxyList = subnets.toStream.map(_.toString) #::: proxyList
+ if (bypassList.nonEmpty) {
+ result.append("[bypass_list]\n")
+ result.append(bypassList.mkString("\n"))
+ result.append('\n')
+ }
+ if (proxyList.nonEmpty) {
+ result.append("[proxy_list]\n")
+ result.append(proxyList.mkString("\n"))
+ result.append('\n')
+ }
+ result.toString
+ }
+
+ def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
+
+ // Don't change: dummy fields for OrmLite interaction
+
+ // noinspection ScalaUnusedSymbol
+ @DatabaseField(useGetSet = true)
+ private val bypassHostnamesString: String = null
+ // noinspection ScalaUnusedSymbol
+ @DatabaseField(useGetSet = true)
+ private val proxyHostnamesString: String = null
+ // noinspection ScalaUnusedSymbol
+ @DatabaseField(useGetSet = true)
+ private val subnetsString: String = null
+}
+
+object Acl {
+ final val ALL = "all"
+ final val BYPASS_LAN = "bypass-lan"
+ final val BYPASS_CHN = "bypass-china"
+ final val BYPASS_LAN_CHN = "bypass-lan-china"
+ final val GFWLIST = "gfwlist"
+ final val CHINALIST = "china-list"
+ final val CUSTOM_RULES = "custom-rules"
+
+ def getPath(id: String): String = app.getApplicationInfo.dataDir + '/' + id + ".acl"
+ def customRules: Acl = {
+ val acl = new Acl()
+ try acl.fromId(CUSTOM_RULES) catch {
+ case _: FileNotFoundException =>
+ }
+ acl.bypass = true
+ acl.bypassHostnames.clear() // everything is bypassed
+ acl
+ }
+ def save(id: String, acl: Acl): Unit = IOUtils.writeString(getPath(id), acl.toString)
+}
diff --git a/src/main/scala/com/github/shadowsocks/job/AclSyncJob.scala b/src/main/scala/com/github/shadowsocks/acl/AclSyncJob.scala
similarity index 91%
rename from src/main/scala/com/github/shadowsocks/job/AclSyncJob.scala
rename to src/main/scala/com/github/shadowsocks/acl/AclSyncJob.scala
index 3af59e8a..9fa08255 100644
--- a/src/main/scala/com/github/shadowsocks/job/AclSyncJob.scala
+++ b/src/main/scala/com/github/shadowsocks/acl/AclSyncJob.scala
@@ -18,18 +18,18 @@
/* */
/*******************************************************************************/
-package com.github.shadowsocks.job
+package com.github.shadowsocks.acl
import java.io.IOException
-import java.net.URL
import java.util.concurrent.TimeUnit
import com.evernote.android.job.Job.{Params, Result}
import com.evernote.android.job.{Job, JobRequest}
import com.github.shadowsocks.ShadowsocksApplication.app
-import com.github.shadowsocks.utils.CloseUtils._
import com.github.shadowsocks.utils.IOUtils
+import scala.io.Source
+
/**
* @author Mygod
*/
@@ -50,9 +50,8 @@ class AclSyncJob(route: String) extends Job {
val filename = route + ".acl"
try {
//noinspection JavaAccessorMethodCalledAsEmptyParen
- IOUtils.writeString(app.getApplicationInfo.dataDir + '/' + filename, autoClose(
- new URL("https://shadowsocks.org/acl/android/v1/" +
- filename).openConnection().getInputStream())(IOUtils.readString))
+ IOUtils.writeString(app.getApplicationInfo.dataDir + '/' + filename,
+ Source.fromURL("https://shadowsocks.org/acl/android/v1/" + filename).mkString)
Result.SUCCESS
} catch {
case e: IOException =>
diff --git a/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala b/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala
new file mode 100644
index 00000000..01cf8588
--- /dev/null
+++ b/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala
@@ -0,0 +1,268 @@
+package com.github.shadowsocks.acl
+
+import java.net.IDN
+import java.util.Locale
+
+import android.content.{ClipData, ClipboardManager, Context, DialogInterface}
+import android.os.{Bundle, Handler}
+import android.support.v7.app.AlertDialog
+import android.support.v7.widget.RecyclerView.ViewHolder
+import android.support.v7.widget.helper.ItemTouchHelper
+import android.support.v7.widget.helper.ItemTouchHelper.SimpleCallback
+import android.support.v7.widget.{DefaultItemAnimator, LinearLayoutManager, RecyclerView, Toolbar}
+import android.view._
+import android.widget.{EditText, Spinner, TextView, Toast}
+import com.github.shadowsocks.ShadowsocksApplication.app
+import com.github.shadowsocks.utils.State
+import com.github.shadowsocks.widget.UndoSnackbarManager
+import com.github.shadowsocks.{MainActivity, R, ToolbarFragment}
+
+import scala.collection.mutable
+
+/**
+ * @author Mygod
+ */
+object CustomRulesFragment {
+ private final val GENERIC = 0
+ private final val DOMAIN = 1
+ private val PATTERN_DOMAIN = """(?<=^\(\^\|\\\.\)).*(?=\$$)""".r
+ private final val TEMPLATE_DOMAIN = "(^|\\.)%s$"
+
+ private final val SELECTED_SUBNETS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_SUBNETS"
+ private final val SELECTED_HOSTNAMES = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_HOSTNAMES"
+}
+
+class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
+ import CustomRulesFragment._
+
+ private def isEnabled = getActivity.asInstanceOf[MainActivity].state match {
+ case State.CONNECTED => app.currentProfile.get.route != Acl.CUSTOM_RULES
+ case State.STOPPED => true
+ case _ => false
+ }
+
+ private def createAclRuleDialog(text: CharSequence = "") = {
+ val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
+ val templateSelector = view.findViewById(R.id.template_selector).asInstanceOf[Spinner]
+ val editText = view.findViewById(R.id.content).asInstanceOf[EditText]
+ PATTERN_DOMAIN.findFirstMatchIn(text) match {
+ case Some(m) =>
+ templateSelector.setSelection(DOMAIN)
+ editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."),
+ IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES))
+ case None =>
+ templateSelector.setSelection(GENERIC)
+ editText.setText(text)
+ }
+ (templateSelector, editText, new AlertDialog.Builder(getActivity)
+ .setTitle(R.string.edit_rule)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setView(view))
+ }
+
+ private val handler = new Handler()
+ private val selectedItems = new mutable.HashSet[AnyRef]
+ private def onSelectedItemsUpdated(): Unit = selectionItem.setVisible(selectedItems.nonEmpty)
+
+ private final class AclRuleViewHolder(view: View) extends RecyclerView.ViewHolder(view)
+ with View.OnClickListener with View.OnLongClickListener {
+ var item: AnyRef = _
+ private val text = itemView.findViewById(android.R.id.text1).asInstanceOf[TextView]
+ itemView.setOnClickListener(this)
+ itemView.setOnLongClickListener(this)
+ itemView.setBackgroundResource(R.drawable.background_selectable)
+
+ def bind(hostname: String) {
+ item = hostname
+ text.setText(hostname)
+ itemView.setSelected(selectedItems.contains(hostname))
+ }
+ def bind(subnet: Subnet) {
+ item = subnet
+ text.setText(subnet.toString)
+ itemView.setSelected(selectedItems.contains(subnet))
+ }
+
+ def onClick(v: View): Unit = if (selectedItems.nonEmpty) onLongClick(v) else {
+ val (templateSelector, editText, dialog) = createAclRuleDialog(item.toString)
+ dialog
+ .setNeutralButton(R.string.delete, ((_, _) => {
+ adapter.remove(item)
+ undoManager.remove((-1, item))
+ }): DialogInterface.OnClickListener)
+ .setPositiveButton(android.R.string.ok, ((_, _) =>
+ if (adapter.addFromTemplate(templateSelector.getSelectedItemPosition, editText.getText)) adapter.remove(item))
+ : DialogInterface.OnClickListener)
+ .create().show()
+ }
+ def onLongClick(v: View): Boolean = {
+ if (!selectedItems.add(item)) selectedItems.remove(item) // toggle
+ onSelectedItemsUpdated()
+ itemView.setSelected(!itemView.isSelected)
+ true
+ }
+ }
+
+ private final class AclRulesAdapter extends RecyclerView.Adapter[AclRuleViewHolder] {
+ private val acl = Acl.customRules
+ private var savePending: Boolean = _
+ private def apply() = if (!savePending) {
+ savePending = true
+ handler.post(() => {
+ Acl.save(Acl.CUSTOM_RULES, acl)
+ savePending = false
+ })
+ }
+
+ def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size
+ def onBindViewHolder(vh: AclRuleViewHolder, i: Int): Unit = {
+ val j = i - acl.subnets.size
+ if (j < 0) vh.bind(acl.subnets(i)) else vh.bind(acl.proxyHostnames(j))
+ }
+ def onCreateViewHolder(vg: ViewGroup, i: Int) = new AclRuleViewHolder(LayoutInflater.from(vg.getContext)
+ .inflate(android.R.layout.simple_list_item_1, vg, false))
+
+ def addSubnet(subnet: Subnet): Boolean = if (acl.subnets.add(subnet)) {
+ notifyItemInserted(acl.subnets.indexOf(subnet))
+ apply()
+ true
+ } else false
+ def addHostname(hostname: String): Boolean = if (acl.proxyHostnames.add(hostname)) {
+ notifyItemInserted(acl.proxyHostnames.indexOf(hostname) + acl.subnets.size)
+ apply()
+ true
+ } else false
+ def addToProxy(input: String): Boolean = try addSubnet(Subnet.fromString(input)) catch {
+ case _: IllegalArgumentException => addHostname(input)
+ }
+ def addFromTemplate(template: Int, text: CharSequence): Boolean = template match {
+ case GENERIC => addToProxy(text.toString)
+ case DOMAIN => try addHostname(TEMPLATE_DOMAIN.formatLocal(Locale.ENGLISH,
+ IDN.toASCII(text.toString, IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES).replaceAll("\\.", "\\\\."))) catch {
+ case exc: IllegalArgumentException =>
+ Toast.makeText(getActivity, exc.getMessage, Toast.LENGTH_SHORT).show()
+ false
+ }
+ case _ => false
+ }
+
+ def remove(i: Int) {
+ val j = i - acl.subnets.size
+ if (j < 0) acl.subnets.remove(i) else acl.proxyHostnames.remove(j)
+ notifyItemRemoved(i)
+ apply()
+ }
+ def remove(item: AnyRef): Unit = item match {
+ case subnet: Subnet =>
+ notifyItemRemoved(acl.subnets.indexOf(subnet))
+ acl.subnets.remove(subnet)
+ apply()
+ case hostname: String =>
+ notifyItemRemoved(acl.proxyHostnames.indexOf(hostname))
+ acl.proxyHostnames.remove(hostname)
+ apply()
+ }
+ def removeSelected() {
+ undoManager.remove(selectedItems.map((-1, _)).toSeq: _*)
+ selectedItems.foreach(remove)
+ selectedItems.clear()
+ onSelectedItemsUpdated()
+ }
+ def undo(actions: Iterator[(Int, AnyRef)]): Unit = for ((_, item) <- actions) item match {
+ case hostname: String => if (acl.proxyHostnames.insert(hostname)) {
+ notifyItemInserted(acl.proxyHostnames.indexOf(hostname) + acl.subnets.size)
+ apply()
+ }
+ case subnet: Subnet => if (acl.subnets.insert(subnet)) {
+ notifyItemInserted(acl.subnets.indexOf(subnet))
+ apply()
+ }
+ }
+
+ def selectAll() {
+ selectedItems.clear()
+ selectedItems ++= acl.subnets
+ selectedItems ++= acl.proxyHostnames
+ onSelectedItemsUpdated()
+ notifyDataSetChanged()
+ }
+ }
+
+ private lazy val adapter = new AclRulesAdapter()
+ private var list: RecyclerView = _
+ private var selectionItem: MenuItem = _
+ private var undoManager: UndoSnackbarManager[AnyRef] = _
+ private lazy val clipboard = getActivity.getSystemService(Context.CLIPBOARD_SERVICE).asInstanceOf[ClipboardManager]
+
+ override def onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle): View =
+ inflater.inflate(R.layout.layout_list, container, false)
+ override def onViewCreated(view: View, savedInstanceState: Bundle) {
+ super.onViewCreated(view, savedInstanceState)
+ if (savedInstanceState != null) {
+ savedInstanceState.getStringArray(SELECTED_SUBNETS) match {
+ case null =>
+ case arr => selectedItems ++= arr.map(Subnet.fromString)
+ }
+ savedInstanceState.getStringArray(SELECTED_HOSTNAMES) match {
+ case null =>
+ case arr => selectedItems ++= arr
+ }
+ onSelectedItemsUpdated()
+ }
+ toolbar.setTitle(R.string.custom_rules)
+ toolbar.inflateMenu(R.menu.custom_rules_menu)
+ toolbar.setOnMenuItemClickListener(this)
+ selectionItem = toolbar.getMenu.findItem(R.id.selection)
+ app.updateAssets()
+ list = view.findViewById(R.id.list).asInstanceOf[RecyclerView]
+ list.setLayoutManager(new LinearLayoutManager(getActivity, LinearLayoutManager.VERTICAL, false))
+ list.setItemAnimator(new DefaultItemAnimator)
+ list.setAdapter(adapter)
+ undoManager = new UndoSnackbarManager[AnyRef](getActivity.findViewById(R.id.snackbar), adapter.undo)
+ new ItemTouchHelper(new SimpleCallback(0, ItemTouchHelper.START | ItemTouchHelper.END) {
+ override def getSwipeDirs(recyclerView: RecyclerView, viewHolder: ViewHolder): Int =
+ if (isEnabled) super.getSwipeDirs(recyclerView, viewHolder) else 0
+ def onSwiped(viewHolder: ViewHolder, direction: Int): Unit = adapter.remove(viewHolder.getAdapterPosition)
+ def onMove(recyclerView: RecyclerView, viewHolder: ViewHolder, target: ViewHolder): Boolean = false
+ }).attachToRecyclerView(list)
+ }
+
+ override def onSaveInstanceState(outState: Bundle): Unit = {
+ super.onSaveInstanceState(outState)
+ outState.putStringArray(SELECTED_SUBNETS, selectedItems.filter(_.isInstanceOf[Subnet]).map(_.toString).toArray)
+ outState.putStringArray(SELECTED_HOSTNAMES, selectedItems.filter(_.isInstanceOf[String]).map(_.toString).toArray)
+ }
+
+ override def onMenuItemClick(menuItem: MenuItem): Boolean = menuItem.getItemId match {
+ case R.id.action_select_all =>
+ adapter.selectAll()
+ true
+ case R.id.action_cut =>
+ clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
+ adapter.removeSelected()
+ true
+ case R.id.action_copy =>
+ clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
+ true
+ case R.id.action_delete =>
+ adapter.removeSelected()
+ true
+
+ case R.id.action_manual_settings =>
+ val (templateSelector, editText, dialog) = createAclRuleDialog()
+ dialog
+ .setPositiveButton(android.R.string.ok, ((_, _) => adapter.addFromTemplate(
+ templateSelector.getSelectedItemPosition, editText.getText)): DialogInterface.OnClickListener)
+ .create().show()
+ true
+ case R.id.action_import =>
+ clipboard.getPrimaryClip.getItemAt(0).getText.toString.split("\n").foreach(adapter.addToProxy)
+ true
+ case R.id.action_import_gfwlist =>
+ val acl = new Acl().fromId(Acl.GFWLIST)
+ acl.subnets.foreach(adapter.addSubnet)
+ acl.proxyHostnames.foreach(adapter.addHostname)
+ true
+ case _ => false
+ }
+}
diff --git a/src/main/scala/com/github/shadowsocks/job/DonaldTrump.scala b/src/main/scala/com/github/shadowsocks/acl/DonaldTrump.scala
similarity index 98%
rename from src/main/scala/com/github/shadowsocks/job/DonaldTrump.scala
rename to src/main/scala/com/github/shadowsocks/acl/DonaldTrump.scala
index c710ad3c..30730a8e 100644
--- a/src/main/scala/com/github/shadowsocks/job/DonaldTrump.scala
+++ b/src/main/scala/com/github/shadowsocks/acl/DonaldTrump.scala
@@ -18,7 +18,7 @@
/* */
/*******************************************************************************/
-package com.github.shadowsocks.job
+package com.github.shadowsocks.acl
import android.util.Log
import com.evernote.android.job.JobCreator
diff --git a/src/main/scala/com/github/shadowsocks/acl/Subnet.scala b/src/main/scala/com/github/shadowsocks/acl/Subnet.scala
new file mode 100644
index 00000000..41e7aed3
--- /dev/null
+++ b/src/main/scala/com/github/shadowsocks/acl/Subnet.scala
@@ -0,0 +1,51 @@
+package com.github.shadowsocks.acl
+
+import java.net.{Inet4Address, Inet6Address, InetAddress}
+
+import com.github.shadowsocks.utils.Utils
+
+/**
+ * @author Mygod
+ */
+@throws[IllegalArgumentException]
+class Subnet(val address: InetAddress, val prefixSize: Int) extends Comparable[Subnet] {
+ if (prefixSize < 0) throw new IllegalArgumentException
+ address match {
+ case _: Inet4Address => if (prefixSize > 32) throw new IllegalArgumentException
+ case _: Inet6Address => if (prefixSize > 128) throw new IllegalArgumentException
+ }
+
+ override def toString: String = if (address match {
+ case _: Inet4Address => prefixSize == 32
+ case _: Inet6Address => prefixSize == 128
+ }) address.toString else address.toString + '/' + prefixSize
+
+ override def compareTo(that: Subnet): Int = {
+ val addrThis = address.getAddress
+ val addrThat = that.address.getAddress
+ var result = addrThis lengthCompare addrThat.length // IPv4 address goes first
+ if (result != 0) return result
+ for ((x, y) <- addrThis zip addrThat) {
+ result = x compare y
+ if (result != 0) return result
+ }
+ prefixSize compare that.prefixSize
+ }
+}
+
+object Subnet {
+ @throws[IllegalArgumentException]
+ def fromString(value: String): Subnet = {
+ val parts = value.split("/")
+ if (!Utils.isNumeric(parts(0))) throw new IllegalArgumentException()
+ val addr = InetAddress.getByName(parts(0))
+ parts.length match {
+ case 1 => new Subnet(addr, addr match {
+ case _: Inet4Address => 32
+ case _: Inet6Address => 128
+ })
+ case 2 => new Subnet(addr, parts(1).toInt)
+ case _ => throw new IllegalArgumentException()
+ }
+ }
+}
diff --git a/src/main/scala/com/github/shadowsocks/utils/Constants.scala b/src/main/scala/com/github/shadowsocks/utils/Constants.scala
index 703d2629..6058fd0f 100644
--- a/src/main/scala/com/github/shadowsocks/utils/Constants.scala
+++ b/src/main/scala/com/github/shadowsocks/utils/Constants.scala
@@ -179,12 +179,3 @@ object Action {
final val EXTRA_PROFILE_ID = "com.github.shadowsocks.EXTRA_PROFILE_ID"
}
-
-object Route {
- val ALL = "all"
- val BYPASS_LAN = "bypass-lan"
- val BYPASS_CHN = "bypass-china"
- val BYPASS_LAN_CHN = "bypass-lan-china"
- val GFWLIST = "gfwlist"
- val CHINALIST = "china-list"
-}
diff --git a/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala b/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala
index 9fc0f8fd..fc97c355 100644
--- a/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala
+++ b/src/main/scala/com/github/shadowsocks/utils/IOUtils.scala
@@ -37,16 +37,6 @@ object IOUtils {
}
}
- def readString(in: InputStream): String = {
- val builder = new StringBuilder()
- val buffer = new Array[Byte](BUFFER_SIZE)
- while (true) {
- val count = in.read(buffer)
- if (count >= 0) builder.append(new String(buffer, 0, count)) else return builder.toString()
- }
- null
- }
-
def writeString(file: String, content: String): Unit =
autoClose(new FileWriter(file))(writer => writer.write(content))
}
diff --git a/src/main/scala/com/github/shadowsocks/widget/UndoSnackbarManager.scala b/src/main/scala/com/github/shadowsocks/widget/UndoSnackbarManager.scala
index 64ce7fc4..8c748a81 100644
--- a/src/main/scala/com/github/shadowsocks/widget/UndoSnackbarManager.scala
+++ b/src/main/scala/com/github/shadowsocks/widget/UndoSnackbarManager.scala
@@ -51,8 +51,8 @@ class UndoSnackbarManager[T](view: View, undo: Iterator[(Int, T)] => Unit,
}
private var last: Snackbar = _
- def remove(index: Int, item: T) {
- recycleBin.append((index, item))
+ def remove(items: (Int, T)*) {
+ recycleBin.appendAll(items)
val count = recycleBin.length
last = Snackbar
.make(view, view.getResources.getQuantityString(R.plurals.removed, count, count: Integer), Snackbar.LENGTH_LONG)
diff --git a/src/main/scala/scala/collection/mutable/SortedList.scala b/src/main/scala/scala/collection/mutable/SortedList.scala
new file mode 100644
index 00000000..a55eefe3
--- /dev/null
+++ b/src/main/scala/scala/collection/mutable/SortedList.scala
@@ -0,0 +1,152 @@
+package scala.collection.mutable
+
+import scala.collection.SortedSetLike
+import scala.collection.generic.MutableSortedSetFactory
+import scala.collection.immutable.RedBlackTree.Tree
+import scala.collection.immutable.{RedBlackTree => RB}
+import scala.runtime.ObjectRef
+
+/**
+ * @define Coll `mutable.SortedList`
+ * @define coll mutable sorted list
+ * @factoryInfo
+ * Companion object of SortedList providing factory related utilities.
+ * @author Mygod
+ */
+//noinspection ReferenceMustBePrefixed,ScalaDocUnknownTag
+object SortedList extends MutableSortedSetFactory[SortedList] {
+ /**
+ * The empty list of this type
+ */
+ def empty[A](implicit ordering: Ordering[A]) = new SortedList[A]()
+}
+
+/**
+ * A mutable SortedList heavily based on SortedSet which "is not designed to enable meaningful subclassing".
+ *
+ * Based on: https://github.com/scala/scala/blob/3cc99d7/src/library/scala/collection/mutable/TreeSet.scala
+ *
+ * @author Mygod
+ */
+//noinspection ReferenceMustBePrefixed
+class SortedList[A] private(treeRef: ObjectRef[RB.Tree[A, Null]], from: Option[A], until: Option[A])
+ (implicit val ordering: Ordering[A])
+ extends SortedSet[A] with SetLike[A, SortedList[A]] with SortedSetLike[A, SortedList[A]] with Set[A] with Serializable {
+ def insert(elem: A): Boolean = {
+ val old = size
+ this += elem
+ old < size
+ }
+
+ // AbstractSeq
+ def apply(i: Int): A = RB.nth(treeRef.elem, i).key // Should i out of range result in NullReferenceException?
+ def length: Int = size
+
+ // Buffer
+ def +=:(elem: A): SortedList.this.type = +=(elem) // prepending is the same as the list is sorted
+ def remove(n: Int): A = {
+ val result = apply(n)
+ remove(result)
+ result
+ }
+ def update(idx: Int, elem: A) {
+ val x = apply(idx)
+ if (!ordering.equiv(x, elem)) {
+ remove(x)
+ this.add(elem)
+ }
+ }
+
+ // Optimized methods
+ override def clear(): Unit = treeRef.elem = null
+ def indexOf[B >: A](elem: B, from: Int = 0): Int = lookupIndex(treeRef.elem, elem.asInstanceOf[A]) match {
+ case result if result < from => -1
+ case result => result
+ }
+ override def last: A = RB.greatest(treeRef.elem).key
+
+ // Tree helper methods, TODO: expand to non-recursive form?
+ private def lookupIndex(tree: Tree[A, Null], x: A): Int =
+ if (tree eq null) -1 else ordering.compare(x, tree.key) match {
+ case 0 => RB.count(tree.left)
+ case cmp if cmp < 0 => lookupIndex(tree.left, x)
+ case _ => lookupIndex(tree.right, x) match {
+ case i if i < 0 => i
+ case i => RB.count(tree.left) + 1 + i
+ }
+ }
+
+ // The following is copied from TreeSet
+
+ if (ordering eq null)
+ throw new NullPointerException("ordering must not be null")
+
+ def this()(implicit ordering: Ordering[A]) = this(new ObjectRef(null), None, None)
+
+ override def size: Int = RB.countInRange(treeRef.elem, from, until)
+
+ override def stringPrefix = "SortedList"
+
+ override def empty: SortedList[A] = SortedList.empty
+
+ private def pickBound(comparison: (A, A) => A, oldBound: Option[A], newBound: Option[A]) = (newBound, oldBound) match {
+ case (Some(newB), Some(oldB)) => Some(comparison(newB, oldB))
+ case (None, _) => oldBound
+ case _ => newBound
+ }
+
+ override def rangeImpl(fromArg: Option[A], untilArg: Option[A]): SortedList[A] = {
+ val newFrom = pickBound(ordering.max, fromArg, from)
+ val newUntil = pickBound(ordering.min, untilArg, until)
+
+ new SortedList(treeRef, newFrom, newUntil)
+ }
+
+ override def -=(elem: A): this.type = {
+ treeRef.elem = RB.delete(treeRef.elem, elem)
+ this
+ }
+
+ override def +=(elem: A): this.type = {
+ treeRef.elem = RB.update(treeRef.elem, elem, null, overwrite = false)
+ this
+ }
+
+ /**
+ * Thanks to the immutable nature of the
+ * underlying Tree, we can share it with
+ * the clone. So clone complexity in time is O(1).
+ *
+ */
+ override def clone(): SortedList[A] =
+ new SortedList[A](new ObjectRef(treeRef.elem), from, until)
+
+ private val notProjection = !(from.isDefined || until.isDefined)
+
+ override def contains(elem: A): Boolean = {
+ def leftAcceptable: Boolean = from match {
+ case Some(lb) => ordering.gteq(elem, lb)
+ case _ => true
+ }
+
+ def rightAcceptable: Boolean = until match {
+ case Some(ub) => ordering.lt(elem, ub)
+ case _ => true
+ }
+
+ (notProjection || (leftAcceptable && rightAcceptable)) &&
+ RB.contains(treeRef.elem, elem)
+ }
+
+ override def iterator: Iterator[A] = iteratorFrom(None)
+
+ override def keysIteratorFrom(start: A): Iterator[A] = iteratorFrom(Some(start))
+
+ private def iteratorFrom(start: Option[A]) = {
+ val it = RB.keysIterator(treeRef.elem, pickBound(ordering.max, from, start))
+ until match {
+ case None => it
+ case Some(ub) => it takeWhile (k => ordering.lt(k, ub))
+ }
+ }
+}