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)) + } }