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 }