diff --git a/mobile/src/main/res/values/arrays.xml b/mobile/src/main/res/values/arrays.xml
index 518a0c6d..70a4596d 100644
--- a/mobile/src/main/res/values/arrays.xml
+++ b/mobile/src/main/res/values/arrays.xml
@@ -224,5 +224,6 @@
- @string/acl_rule_templates_generic
- @string/acl_rule_templates_domain
+ - URL
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
index 6de18162..c1f558ca 100644
--- a/mobile/src/main/res/values/strings.xml
+++ b/mobile/src/main/res/values/strings.xml
@@ -132,7 +132,7 @@
Custom rules
Selection…
Add rule(s)…
- URL, Subnet or Hostname PCRE pattern
+ Subnet or Hostname PCRE pattern
Domain name and all its subdomain names
Edit rule
diff --git a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala
index 7552ccb5..19a2e8aa 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala
@@ -150,8 +150,7 @@ trait BaseService extends Service {
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), true)
+ if (profile.route == Acl.CUSTOM_RULES) Acl.save(Acl.CUSTOM_RULES_FLATTENED, Acl.customRules.flatten(10))
plugin = new PluginConfiguration(profile.plugin).selectedOptions
pluginPath = PluginManager.init(plugin)
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
index a1db6d74..dd915eb3 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksNatService.scala
@@ -63,7 +63,10 @@ class ShadowsocksNatService extends BaseService {
if (profile.route != Acl.ALL) {
cmd += "--acl"
- cmd += Acl.getFile(profile.route).getAbsolutePath
+ cmd += Acl.getFile(profile.route match {
+ case Acl.CUSTOM_RULES => Acl.CUSTOM_RULES_FLATTENED
+ case route => route
+ }).getAbsolutePath
}
sslocalProcess = new GuardedProcess(cmd: _*).start()
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
index 1f124776..242f4460 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ShadowsocksVpnService.scala
@@ -161,7 +161,10 @@ class ShadowsocksVpnService extends VpnService with BaseService {
if (profile.route != Acl.ALL) {
cmd += "--acl"
- cmd += Acl.getFile(profile.route).getAbsolutePath
+ cmd += Acl.getFile(profile.route match {
+ case Acl.CUSTOM_RULES => Acl.CUSTOM_RULES_FLATTENED
+ case route => route
+ }).getAbsolutePath
}
if (TcpFastOpen.sendEnabled) cmd += "--fast-open"
diff --git a/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala
index 4974024e..293737a7 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala
@@ -1,7 +1,9 @@
package com.github.shadowsocks.acl
-import java.io.{File, FileNotFoundException, IOException}
+import java.io.{File, FileNotFoundException}
+import java.net.URL
+import android.util.Log
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.IOUtils
import com.j256.ormlite.field.DatabaseField
@@ -17,21 +19,44 @@ import scala.io.Source
* @author Mygod
*/
class Acl {
+ import Acl._
+
@DatabaseField(generatedId = true)
var id: Int = _
-
- val hostnames = new mutable.SortedList[String]()
+ val bypassHostnames = new mutable.SortedList[String]()
+ val proxyHostnames = new mutable.SortedList[String]()
val subnets = new mutable.SortedList[Subnet]()
- val urls = new mutable.SortedList[String]()
+ val urls = new mutable.SortedList[URL]()(urlOrdering)
@DatabaseField
var bypass: Boolean = _
- def isUrl(url: String): Boolean = url.startsWith("http://") || url.startsWith("https://")
+ def getBypassHostnamesString: String = bypassHostnames.mkString("\n")
+ def getProxyHostnamesString: String = proxyHostnames.mkString("\n")
+ def getSubnetsString: String = subnets.mkString("\n")
+ def getUrlsString: String = urls.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 setUrlsString(value: String) {
+ urls.clear()
+ urls ++= value.split("\n").map(new URL(_))
+ }
def fromAcl(other: Acl): Acl = {
- hostnames.clear()
- hostnames ++= other.hostnames
+ bypassHostnames.clear()
+ bypassHostnames ++= other.bypassHostnames
+ proxyHostnames.clear()
+ proxyHostnames ++= other.proxyHostnames
subnets.clear()
subnets ++= other.subnets
urls.clear()
@@ -39,78 +64,83 @@ class Acl {
bypass = other.bypass
this
}
-
- def fromSource(value: Source, defaultBypass: Boolean = true): Acl = {
- subnets.clear()
+ def fromSource(value: Source, defaultBypass: Boolean = false): Acl = {
+ bypassHostnames.clear()
+ proxyHostnames.clear()
+ this.subnets.clear()
urls.clear()
bypass = defaultBypass
- var in_urls = false
- for (line <- value.getLines()) (line.trim.indexOf('#') match {
- case 0 => {
- line.indexOf("NETWORK_ACL_BEGIN") match {
- case -1 =>
- case index => in_urls = true
- }
- line.indexOf("NETWORK_ACL_END") match {
- case -1 =>
- case index => in_urls = false
- }
- "" // ignore any comment lines
- }
- case index => if (!in_urls) line else ""
- }).trim match {
- // Ignore all section controls
- case "[outbound_block_list]" =>
- case "[black_list]" | "[bypass_list]" =>
- case "[white_list]" | "[proxy_list]" =>
- case "[reject_all]" | "[bypass_all]" =>
- case "[accept_all]" | "[proxy_all]" =>
- case input if subnets != null && input.nonEmpty => try subnets += Subnet.fromString(input) catch {
- case _: IllegalArgumentException => if (isUrl(input)) {
- urls += input
- } else {
- hostnames += input
+ lazy val bypassSubnets = new mutable.SortedList[Subnet]()
+ lazy val proxySubnets = new mutable.SortedList[Subnet]()
+ var hostnames: mutable.SortedList[String] = if (defaultBypass) proxyHostnames else bypassHostnames
+ var subnets: mutable.SortedList[Subnet] = if (defaultBypass) proxySubnets else bypassSubnets
+ for (line <- value.getLines()) (line.indexOf('#') match {
+ case -1 => line
+ case index =>
+ line.substring(index + 1) match {
+ case networkAclParser(url) => urls.add(new URL(url))
+ case _ => // ignore
}
+ 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) proxySubnets else bypassSubnets)
this
}
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id)))
- def getAclString(network: Boolean): String = {
- val result = new StringBuilder()
- if (urls.nonEmpty) {
- result.append(urls.mkString("\n"))
- if (network) {
- result.append("\n#NETWORK_ACL_BEGIN\n")
- try {
- urls.foreach((url: String) => result.append(Source.fromURL(url).mkString))
- } catch {
- case e: IOException => // ignore
- }
- result.append("\n#NETWORK_ACL_END\n")
+ def flatten(depth: Int): Acl = {
+ if (depth > 0) for (url <- urls) {
+ val child = new Acl().fromSource(Source.fromURL(url), bypass).flatten(depth - 1)
+ if (bypass != child.bypass) {
+ Log.w(TAG, "Imported network ACL has a conflicting mode set. This will probably not work as intended. URL: %s"
+ .format(url))
+ child.subnets.clear() // subnets for the different mode are discarded
+ child.bypass = bypass
}
- result.append("\n")
+ bypassHostnames ++= child.bypassHostnames
+ proxyHostnames ++= child.proxyHostnames
+ subnets ++= child.subnets
}
- if (result.isEmpty) {
- result.append("[bypass_all]\n")
- }
- val list = subnets.toStream.map(_.toString) #::: hostnames.toStream
- if (list.nonEmpty) {
- result.append("[proxy_list]\n")
- result.append(list.mkString("\n"))
- result.append('\n')
- }
- result.toString
+ urls.clear()
+ this
}
override def toString: String = {
- getAclString(false)
+ val result = new StringBuilder()
+ result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
+ val (bypassList, proxyList) =
+ if (bypass) (bypassHostnames.toStream, subnets.toStream.map(_.toString) #::: proxyHostnames.toStream)
+ else (subnets.toStream.map(_.toString) #::: bypassHostnames.toStream, proxyHostnames.toStream)
+ 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.append(urls.map("#IMPORT_URL <%s>\n".format(_)).mkString)
+ result.toString
}
- def isValidCustomRules: Boolean = !hostnames.isEmpty
-
// Don't change: dummy fields for OrmLite interaction
// noinspection ScalaUnusedSymbol
@@ -125,6 +155,7 @@ class Acl {
}
object Acl {
+ final val TAG = "Acl"
final val ALL = "all"
final val BYPASS_LAN = "bypass-lan"
final val BYPASS_CHN = "bypass-china"
@@ -132,6 +163,10 @@ object Acl {
final val GFWLIST = "gfwlist"
final val CHINALIST = "china-list"
final val CUSTOM_RULES = "custom-rules"
+ final val CUSTOM_RULES_FLATTENED = "custom-rules-flattened"
+ private val networkAclParser = "^IMPORT_URL\\s*<(.+)>\\s*$".r
+
+ private val urlOrdering: Ordering[URL] = Ordering.by(url => (url.getHost, url.getPort, url.getFile, url.getProtocol))
def getFile(id: String) = new File(app.getFilesDir, id + ".acl")
def customRules: Acl = {
@@ -139,9 +174,9 @@ object 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, network: Boolean = false): Unit = {
- IOUtils.writeString(getFile(id), acl.getAclString(network))
- }
+ def save(id: String, acl: Acl): Unit = IOUtils.writeString(getFile(id), acl.toString)
}
diff --git a/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala
index 6df3a281..6e4a2c79 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/acl/CustomRulesFragment.scala
@@ -1,6 +1,6 @@
package com.github.shadowsocks.acl
-import java.net.IDN
+import java.net.{IDN, URL}
import java.util.Locale
import android.content.{ClipData, ClipboardManager, Context, DialogInterface}
@@ -26,13 +26,15 @@ import scala.io.Source
* @author Mygod
*/
object CustomRulesFragment {
- private final val GENERIC = 0
- private final val DOMAIN = 1
+ private final val TEMPLATE_GENERIC = 0
+ private final val TEMPLATE_DOMAIN = 1
+ private final val TEMPLATE_URL = 2
private val PATTERN_DOMAIN = """(?<=^\(\^\|\\\.\)).*(?=\$$)""".r
- private final val TEMPLATE_DOMAIN = "(^|\\.)%s$"
+ private final val TEMPLATE_REGEX_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"
+ private final val SELECTED_URLS = "com.github.shadowsocks.acl.CustomRulesFragment.SELECTED_URLS"
}
class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickListener {
@@ -44,18 +46,26 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
case _ => false
}
- private def createAclRuleDialog(text: CharSequence = "") = {
+ private def createAclRuleDialog(item: AnyRef = "") = {
val view = getActivity.getLayoutInflater.inflate(R.layout.dialog_acl_rule, null)
val templateSelector = view.findViewById[Spinner](R.id.template_selector)
val editText = view.findViewById[EditText](R.id.content)
- 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)
+ item match {
+ case hostname: String => PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
+ case Some(m) =>
+ templateSelector.setSelection(TEMPLATE_DOMAIN)
+ editText.setText(IDN.toUnicode(m.matched.replaceAll("\\\\.", "."),
+ IDN.ALLOW_UNASSIGNED | IDN.USE_STD3_ASCII_RULES))
+ case None =>
+ templateSelector.setSelection(TEMPLATE_GENERIC)
+ editText.setText(hostname)
+ }
+ case url: URL =>
+ templateSelector.setSelection(TEMPLATE_URL)
+ editText.setText(url.toString)
+ case i =>
+ templateSelector.setSelection(TEMPLATE_GENERIC)
+ editText.setText(i.toString)
}
(templateSelector, editText, new AlertDialog.Builder(getActivity)
.setTitle(R.string.edit_rule)
@@ -85,9 +95,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
text.setText(subnet.toString)
itemView.setSelected(selectedItems.contains(subnet))
}
+ def bind(url: URL) {
+ item = url
+ text.setText(url.toString)
+ itemView.setSelected(selectedItems.contains(url))
+ }
def onClick(v: View): Unit = if (selectedItems.nonEmpty) onLongClick(v) else {
- val (templateSelector, editText, dialog) = createAclRuleDialog(item.toString)
+ val (templateSelector, editText, dialog) = createAclRuleDialog(item)
dialog
.setNeutralButton(R.string.delete, ((_, _) => {
adapter.remove(item)
@@ -125,88 +140,86 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
})
}
- def getItemCount: Int = acl.subnets.size + acl.hostnames.size + acl.urls.size
+ def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size + acl.urls.size
def onBindViewHolder(vh: AclRuleViewHolder, i: Int): Unit = {
- val j = i - acl.urls.size
- val k = i - acl.subnets.size - acl.urls.size
- if (j < 0)
- vh.bind(acl.urls(i))
- else if (k < 0)
- vh.bind(acl.subnets(j))
- else
- vh.bind(acl.hostnames(k))
+ val j = i - acl.subnets.size
+ if (j < 0) vh.bind(acl.subnets(i)) else {
+ val k = j - acl.proxyHostnames.size
+ if (k < 0) vh.bind(acl.proxyHostnames(j)) else vh.bind(acl.urls(k))
+ }
}
def onCreateViewHolder(vg: ViewGroup, i: Int) = new AclRuleViewHolder(LayoutInflater.from(vg.getContext)
.inflate(android.R.layout.simple_list_item_1, vg, false))
- def addUrl(url: String): Int = if (acl.urls.add(url)) {
- val index = acl.urls.indexOf(url)
- notifyItemInserted(index)
- apply()
- index
- } else -1
def addSubnet(subnet: Subnet): Int = if (acl.subnets.add(subnet)) {
- val index = acl.subnets.indexOf(subnet) + acl.urls.size
+ val index = acl.subnets.indexOf(subnet)
notifyItemInserted(index)
apply()
index
} else -1
- def addHostname(hostname: String): Int = if (acl.hostnames.add(hostname)) {
- val index = acl.hostnames.indexOf(hostname) + acl.urls.size + acl.subnets.size
+ def addHostname(hostname: String): Int = if (acl.proxyHostnames.add(hostname)) {
+ val index = acl.proxyHostnames.indexOf(hostname) + acl.subnets.size
+ notifyItemInserted(index)
+ apply()
+ index
+ } else -1
+ def addURL(url: URL): Int = if (acl.urls.add(url)) {
+ val index = acl.urls.indexOf(url) + acl.subnets.size + acl.proxyHostnames.size
notifyItemInserted(index)
apply()
index
} else -1
def addToProxy(input: String): Int = {
- val acl = new Acl().fromSource(Source.fromString(input))
+ val acl = new Acl().fromSource(Source.fromString(input), defaultBypass = true)
var result = -1
- for (url <- acl.urls) result = addUrl(url)
- for (hostname <- acl.hostnames) result = addHostname(hostname)
- for (subnet <- acl.subnets) result = addSubnet(subnet)
+ if (acl.bypass) for (subnet <- acl.subnets) result = addSubnet(subnet)
+ for (hostname <- acl.proxyHostnames) result = addHostname(hostname)
+ for (url <- acl.urls) result = addURL(url)
result
}
def addFromTemplate(template: Int, text: CharSequence): Int = template match {
- case GENERIC => addToProxy(text.toString)
- case DOMAIN => try addHostname(TEMPLATE_DOMAIN.formatLocal(Locale.ENGLISH,
+ case TEMPLATE_GENERIC => addToProxy(text.toString)
+ case TEMPLATE_DOMAIN => try addHostname(TEMPLATE_REGEX_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()
-1
}
+ case TEMPLATE_URL => addURL(new URL(text.toString))
case _ => -1
}
def remove(i: Int) {
- val j = i - acl.urls.size
- val k = i - acl.urls.size - acl.subnets.size
+ val j = i - acl.subnets.size
if (j < 0) {
- undoManager.remove((i, acl.urls(i)))
- acl.urls.remove(i)
- } else if (k < 0) {
- undoManager.remove((i, acl.subnets(j)))
- acl.subnets.remove(j)
+ undoManager.remove((i, acl.subnets(i)))
+ acl.subnets.remove(i)
} else {
- undoManager.remove((i, acl.hostnames(k)))
- acl.hostnames.remove(k)
+ val k = j - acl.proxyHostnames.size
+ if (k < 0) {
+ undoManager.remove((j, acl.proxyHostnames(j)))
+ acl.proxyHostnames.remove(j)
+ } else {
+ undoManager.remove((k, acl.urls(k)))
+ acl.urls.remove(k)
+ }
}
notifyItemRemoved(i)
apply()
}
def remove(item: AnyRef): Unit = item match {
case subnet: Subnet =>
- notifyItemRemoved(acl.subnets.indexOf(subnet) + acl.urls.size)
+ notifyItemRemoved(acl.subnets.indexOf(subnet))
acl.subnets.remove(subnet)
apply()
- case hostname: String => if (acl.isUrl(hostname)) {
- notifyItemRemoved(acl.urls.indexOf(hostname))
- acl.urls.remove(hostname)
+ case hostname: String =>
+ notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.indexOf(hostname))
+ acl.proxyHostnames.remove(hostname)
apply()
- } else {
- notifyItemRemoved(acl.hostnames.indexOf(hostname)
- + acl.urls.size + acl.subnets.size)
- acl.hostnames.remove(hostname)
+ case url: URL =>
+ notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.size + acl.urls.indexOf(url))
+ acl.urls.remove(url)
apply()
- }
}
def removeSelected() {
undoManager.remove(selectedItems.map((-1, _)).toSeq: _*)
@@ -215,28 +228,25 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
onSelectedItemsUpdated()
}
def undo(actions: Iterator[(Int, AnyRef)]): Unit = for ((_, item) <- actions) item match {
- case hostname: String => if (acl.isUrl(hostname)) {
- if (acl.urls.insert(hostname)) {
- notifyItemInserted(acl.urls.indexOf(hostname))
- apply()
- }
- } else {
- if (acl.hostnames.insert(hostname)) {
- notifyItemInserted(acl.hostnames.indexOf(hostname) + acl.urls.size + acl.subnets.size)
- apply()
- }
- }
case subnet: Subnet => if (acl.subnets.insert(subnet)) {
- notifyItemInserted(acl.subnets.indexOf(subnet) + acl.urls.size)
+ notifyItemInserted(acl.subnets.indexOf(subnet))
+ apply()
+ }
+ case hostname: String => if (acl.proxyHostnames.insert(hostname)) {
+ notifyItemInserted(acl.subnets.size + acl.proxyHostnames.indexOf(hostname))
+ apply()
+ }
+ case url: URL => if (acl.urls.insert(url)) {
+ notifyItemInserted(acl.subnets.size + acl.proxyHostnames.size + acl.urls.indexOf(url))
apply()
}
}
def selectAll() {
selectedItems.clear()
- selectedItems ++= acl.urls
selectedItems ++= acl.subnets
- selectedItems ++= acl.hostnames
+ selectedItems ++= acl.proxyHostnames
+ selectedItems ++= acl.urls
onSelectedItemsUpdated()
notifyDataSetChanged()
}
@@ -245,11 +255,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
val j = i - acl.subnets.size
try {
(if (j < 0) acl.subnets(i).address.getHostAddress.substring(0, 1) else {
- val hostname = acl.hostnames(i)
- PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
- case Some(m) => m.matched.replaceAll("\\\\.", ".") // don't convert IDN yet
- case None => hostname
- }
+ val k = j - acl.proxyHostnames.size
+ if (k < 0) {
+ val hostname = acl.proxyHostnames(j)
+ PATTERN_DOMAIN.findFirstMatchIn(hostname) match {
+ case Some(m) => m.matched.replaceAll("\\\\.", ".") // don't convert IDN yet
+ case None => hostname
+ }
+ } else acl.urls(k).getHost
}).substring(0, 1)
} catch {
case _: IndexOutOfBoundsException => " "
@@ -276,6 +289,10 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
case null =>
case arr => selectedItems ++= arr
}
+ savedInstanceState.getStringArray(SELECTED_URLS) match {
+ case null =>
+ case arr => selectedItems ++= arr.map(new URL(_))
+ }
onSelectedItemsUpdated()
}
toolbar.setTitle(R.string.custom_rules)
@@ -308,18 +325,29 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
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)
+ outState.putStringArray(SELECTED_URLS, selectedItems.filter(_.isInstanceOf[URL]).map(_.toString).toArray)
}
+ def copySelected() {
+ val acl = new Acl()
+ acl.bypass = true
+ selectedItems.foreach {
+ case subnet: Subnet => acl.subnets.add(subnet)
+ case hostname: String => acl.proxyHostnames.add(hostname)
+ case url: URL => acl.urls.add(url)
+ }
+ clipboard.setPrimaryClip(ClipData.newPlainText(null, acl.toString))
+ }
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")))
+ copySelected()
adapter.removeSelected()
true
case R.id.action_copy =>
- clipboard.setPrimaryClip(ClipData.newPlainText(null, selectedItems.mkString("\n")))
+ copySelected()
true
case R.id.action_delete =>
adapter.removeSelected()
@@ -333,15 +361,18 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
.create().show()
true
case R.id.action_import =>
- try if (adapter.addToProxy(clipboard.getPrimaryClip.getItemAt(0).getText.toString) != -1) return true catch {
- case _: Exception =>
+ try adapter.addToProxy(clipboard.getPrimaryClip.getItemAt(0).getText.toString) != -1 catch {
+ case exc: Exception =>
+ Snackbar.make(getActivity.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG)
+ .show()
+ exc.printStackTrace()
}
- Snackbar.make(getActivity.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG).show()
true
case R.id.action_import_gfwlist =>
val acl = new Acl().fromId(Acl.GFWLIST)
acl.subnets.foreach(adapter.addSubnet)
- acl.hostnames.foreach(adapter.addHostname)
+ acl.proxyHostnames.foreach(adapter.addHostname)
+ acl.urls.foreach(adapter.addURL)
true
case _ => false
}