diff --git a/mobile/build.sbt b/mobile/build.sbt
index e1bc0a0f96..906f159fd9 100644
--- a/mobile/build.sbt
+++ b/mobile/build.sbt
@@ -35,6 +35,7 @@ libraryDependencies ++=
"com.twofortyfouram" % "android-plugin-api-for-locale" % "1.0.2" ::
"dnsjava" % "dnsjava" % "2.1.8" ::
"eu.chainfire" % "libsuperuser" % "1.0.0.201704021214" ::
+ "me.dm7.barcodescanner" % "zxing" % "1.9.3" ::
"net.glxn.qrgen" % "android" % "2.0" ::
Nil
diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml
index 59bfed1534..06dad74311 100644
--- a/mobile/src/main/AndroidManifest.xml
+++ b/mobile/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
+
@@ -11,6 +12,8 @@
android:required="false"/>
+
+
+
diff --git a/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java b/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java
new file mode 100644
index 0000000000..d203f978c2
--- /dev/null
+++ b/mobile/src/main/java/com/github/shadowsocks/utils/Dns.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2010 Brave New Software Project, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.github.shadowsocks.utils;
+
+import android.annotation.SuppressLint;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.annotation.TargetApi;
+import android.util.Log;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.os.Build;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.net.InetAddress;
+
+public class Dns {
+
+ public static String getDnsResolver(Context context) throws Exception {
+ Collection dnsResolvers = getDnsResolvers(context);
+ if (dnsResolvers.isEmpty()) {
+ throw new Exception("Couldn't find an active DNS resolver");
+ }
+ String dnsResolver = dnsResolvers.iterator().next().toString();
+ if (dnsResolver.startsWith("/")) {
+ dnsResolver = dnsResolver.substring(1);
+ }
+ return dnsResolver;
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ private static Collection getDnsResolvers(Context context) throws Exception {
+ ArrayList addresses = new ArrayList();
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ Class> LinkPropertiesClass = Class.forName("android.net.LinkProperties");
+ Method getActiveLinkPropertiesMethod = ConnectivityManager.class.getMethod("getActiveLinkProperties", new Class []{});
+ Object linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager);
+ if (linkProperties != null) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ Method getDnsesMethod = LinkPropertiesClass.getMethod("getDnses", new Class []{});
+ Collection> dnses = (Collection>)getDnsesMethod.invoke(linkProperties);
+ for (Object dns : dnses) {
+ addresses.add((InetAddress)dns);
+ }
+ } else {
+ for (InetAddress dns : ((LinkProperties)linkProperties).getDnsServers()) {
+ addresses.add(dns);
+ }
+ }
+ }
+
+ return addresses;
+ }
+}
diff --git a/mobile/src/main/res/layout/layout_scanner.xml b/mobile/src/main/res/layout/layout_scanner.xml
new file mode 100644
index 0000000000..ae575a3919
--- /dev/null
+++ b/mobile/src/main/res/layout/layout_scanner.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/mobile/src/main/res/values-ja/strings.xml b/mobile/src/main/res/values-ja/strings.xml
index 46db6437a7..fa52639a76 100644
--- a/mobile/src/main/res/values-ja/strings.xml
+++ b/mobile/src/main/res/values-ja/strings.xml
@@ -91,6 +91,7 @@
"QR コードを読み取る"
"手動設定"
"Zxingに準拠したQRコードスキャンアプリをインストールして下さい"
+QRコードを読み取るにはカメラの使用権限が必要です。
- "削除済み"
@@ -122,7 +123,7 @@
"LAN 及び中国本土のアドレスを迂回する"
"中国本土のアドレス以外を迂回する"
"選択したアプリにプロキシを設定する"
-"サブネット/ホスト名 PCRE パターン"
+"URL/サブネット/ホスト名 PCRE パターン"
"ドメイン及び全てのサブドメイン"
@@ -132,4 +133,4 @@
"不明なプラグイン"
"警告:このプラグインは信頼されていないソースからの可能性があります"
"プラグイン"
-
\ No newline at end of file
+
diff --git a/mobile/src/main/res/values-ko/strings.xml b/mobile/src/main/res/values-ko/strings.xml
index c7eab89763..013fb557c8 100644
--- a/mobile/src/main/res/values-ko/strings.xml
+++ b/mobile/src/main/res/values-ko/strings.xml
@@ -45,4 +45,4 @@
"지원하지 않는 버전의 커널입니다: %s < 3.7.1"
"DNS 포워딩"
"모든 DNS 요청을 외부로 포워딩 합니다"
-
\ No newline at end of file
+
diff --git a/mobile/src/main/res/values-ru/strings.xml b/mobile/src/main/res/values-ru/strings.xml
index 30d47daf63..37db95cf95 100644
--- a/mobile/src/main/res/values-ru/strings.xml
+++ b/mobile/src/main/res/values-ru/strings.xml
@@ -96,6 +96,7 @@
"Сканировать QR-код"
"Ручные настройки"
"Пожалуйста, установите любое ZXing-совместимое приложение для сканирования QR-кодов."
+Разрешение камеры требуется для сканирования QR код.
- "Удалено"
- "Удалено %d элемента"
@@ -130,7 +131,7 @@
"Все, кроме LAN и Китая"
"Список Китай"
"Установить прокси для выбранных приложений"
-"Подсеть/Регулярное выражение (PCRE) имени хоста"
+"URL/Подсеть/Регулярное выражение (PCRE) имени хоста"
"Доменное имя и все его поддомены"
@@ -140,4 +141,4 @@
"Неизвестный плагин %s"
"Предупреждение: этот плагин получен из недоверенного источника."
"Плагин: %s"
-
\ No newline at end of file
+
diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml
index 14dcab6c7d..390e2e3eb0 100644
--- a/mobile/src/main/res/values-zh-rCN/strings.xml
+++ b/mobile/src/main/res/values-zh-rCN/strings.xml
@@ -92,6 +92,7 @@
"扫描二维码"
"手动设置"
"请安装任意兼容 ZXing 的二维码扫描应用。"
+扫描二维码需要相机权限。
- "已删除 %d 项"
@@ -123,7 +124,7 @@
"绕过局域网及中国大陆地址"
"仅代理中国大陆地址"
"为应用程序分别设置代理"
-"子网/域名 PCRE 正则表达式"
+"URL/子网/域名 PCRE 正则表达式"
"域名及其子域名"
@@ -133,4 +134,4 @@
"未知插件 %s"
"警告:该插件似乎并非来自已知的可信源。"
"插件:%s"
-
\ No newline at end of file
+
diff --git a/mobile/src/main/res/values-zh-rTW/strings.xml b/mobile/src/main/res/values-zh-rTW/strings.xml
index 9e01852e3d..3b05c65d61 100644
--- a/mobile/src/main/res/values-zh-rTW/strings.xml
+++ b/mobile/src/main/res/values-zh-rTW/strings.xml
@@ -90,6 +90,7 @@
"掃描 QR 碼"
"手動設定"
"請安裝任何相容 ZXing 的 QR 碼掃描應用程式。"
+掃描 QR 碼需要相機權限。
- "已移除 %d 項"
@@ -121,7 +122,7 @@
"略過區域網路及中國大陸"
"China List"
"為已選擇的應用程式設定 Proxy"
-"子網路/主機名稱 PCRE 模式"
+"URL/子網路/主機名稱 PCRE 模式"
"網域及其所有子網域"
diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml
index ca21e907b4..d97b17f9a5 100644
--- a/mobile/src/main/res/values/strings.xml
+++ b/mobile/src/main/res/values/strings.xml
@@ -104,6 +104,7 @@
Scan QR code
Manual Settings
Please install any ZXing-compliant QR code scanning app.
+ Camera permission is required for scanning QR code.
- Removed
- %d items removed
@@ -129,7 +130,7 @@
Custom rules
Selection…
Add rule(s)…
- Subnet/Hostname PCRE pattern
+ URL, 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 b9a8ac0227..efa325eb42 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/BaseService.scala
@@ -154,7 +154,7 @@ trait BaseService extends Service {
}
if (profile.route == Acl.CUSTOM_RULES) // rationalize custom rules
- Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES))
+ Acl.save(Acl.CUSTOM_RULES, new Acl().fromId(Acl.CUSTOM_RULES), true)
plugin = new PluginConfiguration(profile.plugin).selectedOptions
pluginPath = PluginManager.init(plugin)
@@ -341,12 +341,21 @@ trait BaseService extends Service {
val remoteDns = new JSONArray(profile.remoteDns.split(",").zipWithIndex.map {
case (dns, i) => makeDns("UserDef-" + i, dns.trim)
})
+ val localDns = new JSONArray(Array(
+ makeDns("Primary-1", "119.29.29.29", edns = false),
+ makeDns("Primary-2", "114.114.114.114", edns = false)
+ ))
+
+ try {
+ val localLinkDns = com.github.shadowsocks.utils.Dns.getDnsResolver(this)
+ localDns.put(makeDns("Primary-3", localLinkDns, edns = false))
+ } catch {
+ case _: Exception => // Ignore
+ }
+
profile.route match {
case Acl.BYPASS_CHN | Acl.BYPASS_LAN_CHN | Acl.GFWLIST | Acl.CUSTOM_RULES => config
- .put("PrimaryDNS", new JSONArray(Array(
- makeDns("Primary-1", "119.29.29.29", edns = false),
- makeDns("Primary-2", "114.114.114.114", edns = false)
- )))
+ .put("PrimaryDNS", localDns)
.put("AlternativeDNS", remoteDns)
.put("IPNetworkFile", "china_ip_list.txt")
.put("DomainFile", "gfwlist.txt")
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala
index 708ddf5c65..fd60465b53 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/ProfilesFragment.scala
@@ -323,21 +323,16 @@ final class ProfilesFragment extends ToolbarFragment with Toolbar.OnMenuItemClic
def onMenuItemClick(item: MenuItem): Boolean = item.getItemId match {
case R.id.action_scan_qr_code =>
- def installScanner() = {
- Toast.makeText(getActivity, R.string.add_profile_scanner_not_installed, Toast.LENGTH_LONG).show()
- try startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=com.google.zxing.client.android"))) catch {
- case exc: ActivityNotFoundException => exc.printStackTrace()
- }
- }
try startActivityForResult(new Intent("com.google.zxing.client.android.SCAN")
.addCategory(Intent.CATEGORY_DEFAULT)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_DOCUMENT),
REQUEST_SCAN_QR_CODE) catch {
- case _: ActivityNotFoundException => installScanner()
+ case _: ActivityNotFoundException =>
+ startActivity(new Intent(getActivity, classOf[ScannerActivity]))
case e: SecurityException =>
e.printStackTrace()
app.track(e)
- installScanner()
+ startActivity(new Intent(getActivity, classOf[ScannerActivity]))
}
true
case R.id.action_import =>
diff --git a/mobile/src/main/scala/com/github/shadowsocks/ScannerActivity.scala b/mobile/src/main/scala/com/github/shadowsocks/ScannerActivity.scala
new file mode 100644
index 0000000000..b1dd4de6c0
--- /dev/null
+++ b/mobile/src/main/scala/com/github/shadowsocks/ScannerActivity.scala
@@ -0,0 +1,105 @@
+/*******************************************************************************/
+/* */
+/* Copyright (C) 2016 by Max Lv */
+/* Copyright (C) 2016 by Mygod Studio */
+/* */
+/* This program is free software: you can redistribute it and/or modify */
+/* it under the terms of the GNU General Public License as published by */
+/* the Free Software Foundation, either version 3 of the License, or */
+/* (at your option) any later version. */
+/* */
+/* This program is distributed in the hope that it will be useful, */
+/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
+/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
+/* GNU General Public License for more details. */
+/* */
+/* You should have received a copy of the GNU General Public License */
+/* along with this program. If not, see . */
+/* */
+/*******************************************************************************/
+
+package com.github.shadowsocks
+
+import android.app.{Activity, TaskStackBuilder}
+import android.content.Intent
+import android.content.pm.{PackageManager, ShortcutManager}
+import android.os.{Build, Bundle}
+import android.support.v4.app.ActivityCompat
+import android.support.v4.content.ContextCompat
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.widget.Toolbar
+import android.text.TextUtils
+import android.widget.Toast
+import com.google.zxing.Result
+import com.github.shadowsocks.ShadowsocksApplication.app
+import com.github.shadowsocks.utils.Parser
+import me.dm7.barcodescanner.zxing.ZXingScannerView
+
+object ScannerActivity {
+ private final val MY_PERMISSIONS_REQUEST_CAMERA = 1
+}
+
+class ScannerActivity extends AppCompatActivity with ZXingScannerView.ResultHandler {
+ import ScannerActivity._
+
+ private var scannerView: ZXingScannerView = _
+
+ override def onRequestPermissionsResult(requestCode: Int, permissions: Array[String],
+ grantResults: Array[Int]) {
+ if (requestCode == MY_PERMISSIONS_REQUEST_CAMERA) {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0
+ && grantResults(0) == PackageManager.PERMISSION_GRANTED) {
+ scannerView.setResultHandler(this)
+ scannerView.startCamera()
+ } else {
+ Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
+ finish()
+ }
+ }
+ }
+
+ def navigateUp() {
+ val intent = getParentActivityIntent
+ if (shouldUpRecreateTask(intent) || isTaskRoot)
+ TaskStackBuilder.create(this).addNextIntentWithParentStack(intent).startActivities()
+ else finish()
+ }
+
+ override def onCreate(state: Bundle) {
+ super.onCreate(state)
+ setContentView(R.layout.layout_scanner)
+ val toolbar = findViewById(R.id.toolbar).asInstanceOf[Toolbar]
+ toolbar.setTitle(getTitle)
+ toolbar.setNavigationIcon(R.drawable.abc_ic_ab_back_material)
+ toolbar.setNavigationOnClickListener(_ => navigateUp())
+ scannerView = findViewById(R.id.scanner).asInstanceOf[ZXingScannerView]
+ if (Build.VERSION.SDK_INT >= 25) getSystemService(classOf[ShortcutManager]).reportShortcutUsed("scan")
+ }
+
+ override def onResume() {
+ super.onResume()
+ val permissionCheck = ContextCompat.checkSelfPermission(this,
+ android.Manifest.permission.CAMERA)
+ if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
+ scannerView.setResultHandler(this) // Register ourselves as a handler for scan results.
+ scannerView.startCamera() // Start camera on resume
+ } else {
+ ActivityCompat.requestPermissions(this,
+ Array(android.Manifest.permission.CAMERA), MY_PERMISSIONS_REQUEST_CAMERA)
+ }
+ }
+
+ override def onPause() {
+ super.onPause()
+ scannerView.stopCamera() // Stop camera on pause
+ }
+
+ override def handleResult(rawResult: Result) = {
+ val uri = rawResult.getText
+ if (!TextUtils.isEmpty(uri))
+ Parser.findAll(uri).foreach(app.profileManager.createProfile)
+ navigateUp()
+ }
+}
+
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 676f09e4ec..dc953f6c91 100644
--- a/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala
+++ b/mobile/src/main/scala/com/github/shadowsocks/acl/Acl.scala
@@ -1,6 +1,6 @@
package com.github.shadowsocks.acl
-import java.io.{File, FileNotFoundException}
+import java.io.{File, FileNotFoundException, IOException}
import com.github.shadowsocks.ShadowsocksApplication.app
import com.github.shadowsocks.utils.IOUtils
@@ -22,6 +22,7 @@ class Acl {
val bypassHostnames = new mutable.SortedList[String]()
val proxyHostnames = new mutable.SortedList[String]()
val subnets = new mutable.SortedList[Subnet]()
+ val urls = new mutable.SortedList[String]()
@DatabaseField
var bypass: Boolean = _
@@ -40,6 +41,10 @@ class Acl {
subnets.clear()
subnets ++= value.split("\n").map(Subnet.fromString)
}
+ def setUrlRules(value: String) {
+ urls.clear()
+ urls ++= value.split("\n")
+ }
def fromAcl(other: Acl): Acl = {
bypassHostnames.clear()
@@ -48,21 +53,35 @@ class Acl {
proxyHostnames ++= other.proxyHostnames
subnets.clear()
subnets ++= other.subnets
+ urls.clear()
+ urls ++= other.urls
bypass = other.bypass
this
}
- def fromSource(value: Source, defaultBypass: Boolean = false): Acl = {
+ def fromSource(value: Source, defaultBypass: Boolean = true): Acl = {
bypassHostnames.clear()
proxyHostnames.clear()
this.subnets.clear()
+ this.urls.clear()
bypass = defaultBypass
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
+ var in_urls = false
for (line <- value.getLines()) (line.indexOf('#') match {
- case -1 => line
- case index => line.substring(0, index) // trim comments
+ case -1 => if (!in_urls) line else ""
+ case index => {
+ line.indexOf("URLS_BEGIN") match {
+ case -1 =>
+ case index => in_urls = true
+ }
+ line.indexOf("URLS_END") match {
+ case -1 =>
+ case index => in_urls = false
+ }
+ "" // ignore any comment lines
+ }
}).trim match {
case "[outbound_block_list]" =>
hostnames = null
@@ -76,7 +95,10 @@ class Acl {
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 _: IllegalArgumentException => if (input.startsWith("http://") || input.startsWith("https://")) {
+ urls += input
+ }
+ hostnames += input
}
case _ =>
}
@@ -85,9 +107,24 @@ class Acl {
}
final def fromId(id: String): Acl = fromSource(Source.fromFile(Acl.getFile(id)))
- override def toString: String = {
+ def getAclString(network: Boolean): String = {
val result = new StringBuilder()
- result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
+ if (urls.nonEmpty) {
+ result.append("#URLS_BEGIN\n")
+ result.append(urls.mkString("\n"))
+ if (network) {
+ try {
+ urls.foreach((url: String) => result.append(Source.fromURL(url).mkString))
+ } catch {
+ case e: IOException => // ignore
+ }
+ }
+ result.append("#URLS_END\n")
+
+ }
+ if (result.isEmpty) {
+ 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)
@@ -104,6 +141,10 @@ class Acl {
result.toString
}
+ override def toString: String = {
+ getAclString(false)
+ }
+
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
// Don't change: dummy fields for OrmLite interaction
@@ -134,9 +175,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): Unit = IOUtils.writeString(getFile(id), acl.toString)
+ def save(id: String, acl: Acl, network: Boolean = false): Unit = {
+ IOUtils.writeString(getFile(id), acl.getAclString(network))
+ }
}