Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb25df8692 | ||
|
|
e8b5953701 | ||
|
|
b8d7ade44d | ||
|
|
d7183fe147 | ||
|
|
8b870877d5 | ||
|
|
024628483e | ||
|
|
73fe14d643 | ||
|
|
512cb06671 | ||
|
|
3d069b10ee | ||
|
|
4490d95c7f | ||
|
|
390cb522d5 | ||
|
|
b14fdcaed9 | ||
|
|
b6adeda924 | ||
|
|
253286c482 | ||
|
|
f25406b86c | ||
|
|
2c8718163a | ||
|
|
450bde5305 | ||
|
|
d51bf46413 | ||
|
|
a69183cc0f | ||
|
|
62a0bb59c7 |
@@ -14,6 +14,11 @@ Crash: [Submit an issue](https://github.com/shadowsocks/shadowsocks-android/issu
|
||||
* Tap the number to enter the port you wish to use; (if the keyboard doesn't pop up automatically for some reason)
|
||||
* Use Tasker integration to create a desktop widget.
|
||||
|
||||
### How to add QR code from local gallery?
|
||||
|
||||
Scan it with a third-party scanner like [QuickMark Barcode Scanner](https://play.google.com/store/apps/details?id=tw.com.quickmark) and click the `ss` url.
|
||||
|
||||
|
||||
### Why is NAT mode deprecated?
|
||||
|
||||
1. Requiring ROOT permission;
|
||||
@@ -47,3 +52,8 @@ So if you notice a significant increase in battery usage after you use Shadowsoc
|
||||
### It works fine under Wi-Fi but can't connect through cellular data?
|
||||
|
||||
Allow this app to consume background data in app settings.
|
||||
|
||||
### Why Camera permission is required on devices below Android 6.0?
|
||||
|
||||
To scan the QR code through the integrated QR scanner.
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ local.sbt
|
||||
#Intellij IDEA
|
||||
.idea/
|
||||
.d
|
||||
gen/
|
||||
|
||||
mobile/src/main/libs
|
||||
mobile/src/main/obj
|
||||
|
||||
@@ -18,7 +18,7 @@ lazy val commonSettings = Seq(
|
||||
shrinkResources := true,
|
||||
typedResources := false,
|
||||
|
||||
resConfigs := Seq("ja", "ko", "ru", "zh-rCN", "zh-rTW"),
|
||||
resConfigs := Seq("fa", "ja", "ko", "ru", "zh-rCN", "zh-rTW"),
|
||||
|
||||
resolvers += "google" at "https://maven.google.com"
|
||||
)
|
||||
|
||||
+8
-8
@@ -4,8 +4,8 @@ enablePlugins(AndroidGms)
|
||||
android.useSupportVectors
|
||||
|
||||
name := "shadowsocks"
|
||||
version := "4.2.1"
|
||||
versionCode := Some(191)
|
||||
version := "4.2.5"
|
||||
versionCode := Some(195)
|
||||
|
||||
proguardOptions ++=
|
||||
"-dontwarn com.google.android.gms.internal.**" ::
|
||||
@@ -23,11 +23,11 @@ proguardOptions ++=
|
||||
"-keep public class com.evernote.android.job.JobRescheduleService" ::
|
||||
Nil
|
||||
|
||||
val playServicesVersion = "11.0.4"
|
||||
val playServicesVersion = "11.2.0"
|
||||
resolvers += Resolver.jcenterRepo
|
||||
libraryDependencies ++=
|
||||
"com.futuremind.recyclerfastscroll" % "fastscroll" % "0.2.5" ::
|
||||
"com.evernote" % "android-job" % "1.2.0-alpha3" ::
|
||||
"com.evernote" % "android-job" % "1.2.0-alpha4" ::
|
||||
"com.github.jorgecastilloprz" % "fabprogresscircle" % "1.01" ::
|
||||
"com.google.android.gms" % "play-services-ads" % playServicesVersion ::
|
||||
"com.google.android.gms" % "play-services-analytics" % playServicesVersion ::
|
||||
@@ -35,15 +35,15 @@ libraryDependencies ++=
|
||||
"com.google.firebase" % "firebase-config" % playServicesVersion ::
|
||||
"com.j256.ormlite" % "ormlite-android" % "5.0" ::
|
||||
"com.mikepenz" % "crossfader" % "1.5.0" ::
|
||||
"com.mikepenz" % "fastadapter" % "2.6.2" ::
|
||||
"com.mikepenz" % "iconics-core" % "2.8.9" ::
|
||||
"com.mikepenz" % "materialdrawer" % "5.9.4" ::
|
||||
"com.mikepenz" % "fastadapter" % "2.6.3" ::
|
||||
"com.mikepenz" % "iconics-core" % "2.9.3" ::
|
||||
"com.mikepenz" % "materialdrawer" % "5.9.5" ::
|
||||
"com.mikepenz" % "materialize" % "1.0.3" ::
|
||||
"com.squareup.okhttp3" % "okhttp" % "3.8.1" ::
|
||||
"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" ::
|
||||
"me.dm7.barcodescanner" % "zxing" % "1.9.8" ::
|
||||
"net.glxn.qrgen" % "android" % "2.0" ::
|
||||
Nil
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">"شدوساکس"</string>
|
||||
|
||||
<!-- misc -->
|
||||
<string name="profile">"پروفایل"</string>
|
||||
<string name="connection_test_testing">"در حال آزمایش..."</string>
|
||||
|
||||
<!-- proxy category -->
|
||||
<string name="profile_name">"نام پروفایل"</string>
|
||||
<string name="proxy">"سرور"</string>
|
||||
<string name="sitekey">"کلمه عبور"</string>
|
||||
<string name="enc_method">"طریقه رمزگذاری"</string>
|
||||
<string name="on">"روشن"</string>
|
||||
<string name="auto_connect">"وصلشدن اتوماتیک"</string>
|
||||
<string name="auto_connect_summary">"فعالشدن شدوساکس لحظه روشنشدن گوشی"</string>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="forward_success">"شدوساکس فعال شد."</string>
|
||||
<string name="invalid_server">"آدرس سرور نامعتبر است"</string>
|
||||
<string name="service_failed">"اتصال به سرور ناموفق بود!"</string>
|
||||
<string name="stop">"غیرفعالکردن"</string>
|
||||
<string name="stopping">"در حال قطعکردن اتصال..."</string>
|
||||
<string name="profile_invalid_input">"هیچ پروفایل معتبری یافت نشد!"</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="close">"خروج"</string>
|
||||
<string name="profile_empty">"لطفا یک پروفایل انتخاب کنید."</string>
|
||||
<string name="proxy_empty">"پروکسی یا کلمه عبور نباید خالی باشند!"</string>
|
||||
<string name="connect">"اتصال"</string>
|
||||
|
||||
<!-- menu category -->
|
||||
<string name="profiles">"پروفایلها"</string>
|
||||
<string name="settings">"تنظیمات"</string>
|
||||
<string name="faq">"سوالات مداول"</string>
|
||||
<string name="recovery">"ریست"</string>
|
||||
<string name="about">"درباره ما"</string>
|
||||
<string name="edit">"ویرایش"</string>
|
||||
<string name="share">"به اشتراک گذاری"</string>
|
||||
<string name="add_profile">"افزودن پروفایل"</string>
|
||||
<string name="delete">"حذف"</string>
|
||||
<string name="delete_confirm_prompt">"آیا مطمئن هستید که میخواهید این پروفایل را حذف کنید؟"</string>
|
||||
<string name="share_qr_nfc">"NFC یا QR Code"</string>
|
||||
<string name="add_profile_methods_scan_qr_code">"اسکنکردن QR Code"</string>
|
||||
<string name="add_profile_methods_manual_settings">"تنظیمات دستی"</string>
|
||||
|
||||
<!-- tasker -->
|
||||
<string name="toggle_service_state">"فعالکردن سرویس"</string>
|
||||
<string name="start_service_default">"وصلشدن به سرور فعلی"</string>
|
||||
<string name="profile_default">"استفاده از پروفایل فعلی"</string>
|
||||
|
||||
<!-- status -->
|
||||
<string name="sent">"فرستادهشده:"</string>
|
||||
<string name="received">"دریافتشده:"</string>
|
||||
<string name="connecting">"در حال اتصال..."</string>
|
||||
<string name="vpn_connected">"وصل شد. برای بررسی اتصال ضربه (Tap) بزنید."</string>
|
||||
<string name="nat_connected">"اتصال برقرار است."</string>
|
||||
<string name="not_connected">"اتصال برقرار نیست!"</string>
|
||||
<string name="route_entry_all">"همه"</string>
|
||||
</resources>
|
||||
@@ -2,8 +2,7 @@
|
||||
<resources>
|
||||
<string name="quick_toggle">"ON/OFF"</string>
|
||||
|
||||
<!-- global/misc category -->
|
||||
<string name="misc_cat">"その他"</string>
|
||||
<!-- misc -->
|
||||
<string name="profile">"プロファイル"</string>
|
||||
<string name="profile_summary">"新規作成または既存ファイルを開く"</string>
|
||||
<string name="nat">"NAT モード (デバックのみ)"</string>
|
||||
@@ -90,8 +89,6 @@
|
||||
<string name="add_profile_dialog">"Shadowsocks用プロファイルを追加しますか"</string>
|
||||
<string name="add_profile_methods_scan_qr_code">"QR コードを読み取る"</string>
|
||||
<string name="add_profile_methods_manual_settings">"手動設定"</string>
|
||||
<string name="add_profile_scanner_not_installed">"Zxingに準拠したQRコードスキャンアプリをインストールして下さい"</string>
|
||||
<string name="add_profile_scanner_permission_required">QRコードを読み取るにはカメラの使用権限が必要です。</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="other">"削除済み"</item>
|
||||
</plurals>
|
||||
@@ -133,4 +130,9 @@
|
||||
<string name="plugin_unknown">"不明なプラグイン"</string>
|
||||
<string name="plugin_untrusted">"警告:このプラグインは信頼されていないソースからの可能性があります"</string>
|
||||
<string name="profile_plugin">"プラグイン"</string>
|
||||
</resources>
|
||||
<string name="add_profile_scanner_permission_required">"QRコードをスキャンするにはカメラ権限が必要です。"</string>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="service_vpn">"VPNサービス"</string>
|
||||
<string name="service_nat">"NATサービス"</string>
|
||||
</resources>
|
||||
@@ -8,16 +8,11 @@
|
||||
<string name="nat">"NAT 모드 (지원 중단)"</string>
|
||||
<string name="nat_summary">"VPN 모드 대신 NAT 모드를 사용합니다. 루트 권한이 필요합니다."</string>
|
||||
<string name="remote_dns">"원격 DNS"</string>
|
||||
<string name="stat_summary">"송신: \t\t\t\t\t%3$s\t↑\t%1$s/s
|
||||
수신: \t%4$s\t↓\t%2$s/s "</string>
|
||||
<string name="stat_profiles">"%1$s↑\t%2$s↓"</string>
|
||||
<string name="traffic_summary">"%1$s/s↑\t%2$s/s↓"</string>
|
||||
<plurals name="bytes">
|
||||
<item quantity="other">"바이트"</item>
|
||||
</plurals>
|
||||
<string name="stat_summary">"송신: \t%3$s\t↑\t%1$s/s
|
||||
수신: \t%4$s\t↓\t%2$s/s"</string>
|
||||
<string name="connection_test_pending">"인터넷 연결 상태 검사하기"</string>
|
||||
<string name="connection_test_testing">"검사 중…"</string>
|
||||
<string name="connection_test_available">"성공: %dms"</string>
|
||||
<string name="connection_test_available">"성공: 지연시간 %dms"</string>
|
||||
<string name="connection_test_error">"인터넷 연결을 찾을 수 없습니다: %s"</string>
|
||||
<string name="connection_test_fail">"인터넷에 연결할 수 없습니다"</string>
|
||||
<string name="connection_test_error_status_code">"오류 코드: #%d"</string>
|
||||
@@ -45,4 +40,73 @@
|
||||
<string name="tcp_fastopen_summary_unsupported">"지원하지 않는 버전의 커널입니다: %s < 3.7.1"</string>
|
||||
<string name="udp_dns">"DNS 포워딩"</string>
|
||||
<string name="udp_dns_summary">"모든 DNS 요청을 외부로 포워딩 합니다"</string>
|
||||
</resources>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="forward_success">"Shadowsocks가 시작되었습니다"</string>
|
||||
<string name="invalid_server">"잘못된 서버 이름입니다"</string>
|
||||
<string name="service_failed">"원격 서버에 접속하는 데 실패했습니다"</string>
|
||||
<string name="nat_deprecated">"경고: NAT 모드는 Android 5.0부터 지원 중단되었습니다"</string>
|
||||
<string name="nat_no_root">"NAT 모드는 루트 권한이 필요합니다"</string>
|
||||
<string name="switch_to_vpn">"VPN 모드로 전환"</string>
|
||||
<string name="stop">"중지"</string>
|
||||
<string name="stopping">"종료 중…"</string>
|
||||
<string name="reboot_required">"VPN 서비스를 시작하는 데 실패했습니다. 장치를 재시작해 보세요."</string>
|
||||
<string name="profile_invalid_input">"올바른 프로필 데이터를 찾을 수 없습니다"</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="close">"닫기"</string>
|
||||
<string name="profile_empty">"프로필을 선택해 주세요"</string>
|
||||
<string name="proxy_empty">"서버 주소와 비밀번호는 반드시 입력해야 합니다"</string>
|
||||
<string name="connect">"접속"</string>
|
||||
<string name="recovering">"재설정 중…"</string>
|
||||
<string name="remove_profile">"'%s' 프로필을 삭제 하시겠습니까?"</string>
|
||||
|
||||
<!-- menu category -->
|
||||
<string name="profiles">"프로필"</string>
|
||||
<string name="settings">"설정"</string>
|
||||
<string name="faq">"자주 묻는 질문"</string>
|
||||
<string name="recovery">"재설정"</string>
|
||||
<string name="about">"이 앱에 대하여"</string>
|
||||
<string name="edit">"수정"</string>
|
||||
<string name="share">"공유"</string>
|
||||
<string name="add_profile">"프로필 추가"</string>
|
||||
<string name="action_export">"클립보드로 내보내기"</string>
|
||||
<string name="action_import">"클립보드에서 불러오기"</string>
|
||||
<string name="action_export_msg">"성공적으로 내보냈습니다"</string>
|
||||
<string name="action_export_err">"내보내기에 실패했습니다"</string>
|
||||
<string name="action_import_msg">"성공적으로 불러왔습니다"</string>
|
||||
<string name="action_import_err">"불러오는 데 실패했습니다"</string>
|
||||
|
||||
<!-- profile -->
|
||||
<string name="profile_config">"프로필 설정"</string>
|
||||
<string name="delete">"삭제"</string>
|
||||
<string name="delete_confirm_prompt">"정말 이 프로필을 삭제하시겠습니까?"</string>
|
||||
<string name="share_qr_nfc">"QR 코드/NFC"</string>
|
||||
<string name="add_profile_dialog">"이 Shadowsocks 프로필을 추가하시겠습니까?"</string>
|
||||
<string name="add_profile_methods_scan_qr_code">"QR 코드 읽기"</string>
|
||||
<string name="add_profile_methods_manual_settings">"손수 써넣기"</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="other">"삭제했습니다"</item>
|
||||
</plurals>
|
||||
<string name="undo">"실행 취소"</string>
|
||||
|
||||
<!-- status -->
|
||||
<string name="sent">"송신:"</string>
|
||||
<string name="received">"수신:"</string>
|
||||
<string name="connecting">"연결 중…"</string>
|
||||
<string name="vpn_connected">"연결 완료. 탭 하면 연결 상태를 검사합니다."</string>
|
||||
<string name="nat_connected">"연결 완료"</string>
|
||||
|
||||
<!-- acl -->
|
||||
<string name="custom_rules">"사용자 정의 규칙"</string>
|
||||
<string name="acl_rule_templates_generic">"URL, 서브넷 혹은 호스트 이름 PCRE 패턴"</string>
|
||||
|
||||
<!-- plugin -->
|
||||
<string name="plugin">"플러그인"</string>
|
||||
<string name="plugin_configure">"플러그인 설정…"</string>
|
||||
<string name="plugin_disabled">"비활성화 됨"</string>
|
||||
<string name="plugin_unknown">"알 수 없는 플러그인: %s"</string>
|
||||
<string name="plugin_untrusted">"경고: 이 플러그인은 신뢰할 수 있는 출처에서 온 것이 아닌 것 같습니다"</string>
|
||||
<string name="profile_plugin">"플러그인: %s"</string>
|
||||
<string name="add_profile_scanner_permission_required">"QR 코드를 읽어 들이려면 카메라 권한이 필요합니다"</string>
|
||||
</resources>
|
||||
@@ -95,8 +95,6 @@
|
||||
<string name="add_profile_dialog">"Добавить этот Профиль Shadowsocks?"</string>
|
||||
<string name="add_profile_methods_scan_qr_code">"Сканировать QR-код"</string>
|
||||
<string name="add_profile_methods_manual_settings">"Ручные настройки"</string>
|
||||
<string name="add_profile_scanner_not_installed">"Пожалуйста, установите любое ZXing-совместимое приложение для сканирования QR-кодов."</string>
|
||||
<string name="add_profile_scanner_permission_required">Разрешение камеры требуется для сканирования QR код.</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="one">"Удалено"</item>
|
||||
<item quantity="few">"Удалено %d элемента"</item>
|
||||
@@ -141,4 +139,5 @@
|
||||
<string name="plugin_unknown">"Неизвестный плагин %s"</string>
|
||||
<string name="plugin_untrusted">"Предупреждение: этот плагин получен из недоверенного источника."</string>
|
||||
<string name="profile_plugin">"Плагин: %s"</string>
|
||||
</resources>
|
||||
<string name="add_profile_scanner_permission_required">"Разрешение камеры требуется для сканирования QR код."</string>
|
||||
</resources>
|
||||
@@ -3,8 +3,7 @@
|
||||
<string name="app_name">"影梭"</string>
|
||||
<string name="quick_toggle">"开关"</string>
|
||||
|
||||
<!-- global/misc category -->
|
||||
<string name="misc_cat">"其他"</string>
|
||||
<!-- misc -->
|
||||
<string name="profile">"配置文件"</string>
|
||||
<string name="profile_summary">"新建或切换到其他配置文件"</string>
|
||||
<string name="nat">"NAT 模式(仅限调试)"</string>
|
||||
@@ -12,6 +11,9 @@
|
||||
<string name="remote_dns">"远程 DNS"</string>
|
||||
<string name="stat_summary">"发送: \t%3$s\t↑\t%1$s/s
|
||||
接收: \t%4$s\t↓\t%2$s/s"</string>
|
||||
<plurals name="bytes">
|
||||
<item quantity="other">"字节"</item>
|
||||
</plurals>
|
||||
<string name="connection_test_pending">"检查网络连接"</string>
|
||||
<string name="connection_test_testing">"测试中…"</string>
|
||||
<string name="connection_test_available">"连接有效:延时 %d 毫秒"</string>
|
||||
@@ -63,7 +65,7 @@
|
||||
<string name="proxy_empty">"代理服务器地址及密码不能为空"</string>
|
||||
<string name="connect">"连接"</string>
|
||||
<string name="recovering">"重置中…"</string>
|
||||
<string name="remove_profile">"删除此配置文件“%s”?"</string>
|
||||
<string name="remove_profile">"删除此配置文件“%s”?"</string>
|
||||
|
||||
<!-- menu category -->
|
||||
<string name="profiles">"配置文件"</string>
|
||||
@@ -71,7 +73,7 @@
|
||||
<string name="faq">"常见问题"</string>
|
||||
<string name="recovery">"重置"</string>
|
||||
<string name="about">"关于"</string>
|
||||
<string name="about_title">"影梭 (shadowsocks) %s"</string>
|
||||
<string name="about_title">"影梭 (Shadowsocks) %s"</string>
|
||||
<string name="edit">"编辑"</string>
|
||||
<string name="share">"分享"</string>
|
||||
<string name="add_profile">"添加配置文件"</string>
|
||||
@@ -91,8 +93,6 @@
|
||||
<string name="add_profile_dialog">"为影梭添加此配置文件?"</string>
|
||||
<string name="add_profile_methods_scan_qr_code">"扫描二维码"</string>
|
||||
<string name="add_profile_methods_manual_settings">"手动设置"</string>
|
||||
<string name="add_profile_scanner_not_installed">"请安装任意兼容 ZXing 的二维码扫描应用。"</string>
|
||||
<string name="add_profile_scanner_permission_required">扫描二维码需要相机权限。</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="other">"已删除 %d 项"</item>
|
||||
</plurals>
|
||||
@@ -134,4 +134,9 @@
|
||||
<string name="plugin_unknown">"未知插件 %s"</string>
|
||||
<string name="plugin_untrusted">"警告:该插件似乎并非来自已知的可信源。"</string>
|
||||
<string name="profile_plugin">"插件:%s"</string>
|
||||
</resources>
|
||||
<string name="add_profile_scanner_permission_required">"扫描二维码需要获得使用相机的权限。"</string>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="service_vpn">"VPN 服务"</string>
|
||||
<string name="service_nat">"NAT 服务"</string>
|
||||
</resources>
|
||||
@@ -89,8 +89,6 @@
|
||||
<string name="add_profile_dialog">"為 Shadowsocks 新增此設定檔?"</string>
|
||||
<string name="add_profile_methods_scan_qr_code">"掃描 QR 碼"</string>
|
||||
<string name="add_profile_methods_manual_settings">"手動設定"</string>
|
||||
<string name="add_profile_scanner_not_installed">"請安裝任何相容 ZXing 的 QR 碼掃描應用程式。"</string>
|
||||
<string name="add_profile_scanner_permission_required">掃描 QR 碼需要相機權限。</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="other">"已移除 %d 項"</item>
|
||||
</plurals>
|
||||
@@ -126,10 +124,15 @@
|
||||
<string name="acl_rule_templates_domain">"網域及其所有子網域"</string>
|
||||
|
||||
<!-- plugin -->
|
||||
<string name="plugin">"插件"</string>
|
||||
<string name="plugin_configure">"配置..."</string>
|
||||
<string name="plugin_disabled">"未啟用"</string>
|
||||
<string name="plugin">"外掛程式"</string>
|
||||
<string name="plugin_configure">"設定..."</string>
|
||||
<string name="plugin_disabled">"停用"</string>
|
||||
<string name="plugin_unknown">"未知插件 %s"</string>
|
||||
<string name="plugin_untrusted">"警告:該插件似乎不是來自一個已知的可信任源。"</string>
|
||||
<string name="profile_plugin">"插件: %s"</string>
|
||||
</resources>
|
||||
<string name="plugin_untrusted">"警告:此外掛程式似乎不是來自一個已知的受信任來源。"</string>
|
||||
<string name="profile_plugin">"外掛程式: %s"</string>
|
||||
<string name="add_profile_scanner_permission_required">"掃描 QR 碼需要相機權限。"</string>
|
||||
|
||||
<!-- notification category -->
|
||||
<string name="service_vpn">"VPN 服務"</string>
|
||||
<string name="service_nat">"NAT 服務"</string>
|
||||
</resources>
|
||||
@@ -224,5 +224,6 @@
|
||||
<string-array name="acl_rule_templates">
|
||||
<item>@string/acl_rule_templates_generic</item>
|
||||
<item>@string/acl_rule_templates_domain</item>
|
||||
<item>URL</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
<string name="custom_rules">Custom rules</string>
|
||||
<string name="action_selection">Selection…</string>
|
||||
<string name="action_add_rule">Add rule(s)…</string>
|
||||
<string name="acl_rule_templates_generic">URL, Subnet or Hostname PCRE pattern</string>
|
||||
<string name="acl_rule_templates_generic">Subnet or Hostname PCRE pattern</string>
|
||||
<string name="acl_rule_templates_domain">Domain name and all its subdomain names</string>
|
||||
<string name="edit_rule">Edit rule</string>
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -374,7 +373,7 @@ trait BaseService extends Service {
|
||||
.put("IPNetworkFile", "china_ip_list.txt")
|
||||
.put("DomainFile", "gfwlist.txt")
|
||||
case Acl.CHINALIST => config
|
||||
.put("PrimaryDNS", new JSONArray().put(makeDns("Primary", "119.29.29.29")))
|
||||
.put("PrimaryDNS", localDns)
|
||||
.put("AlternativeDNS", remoteDns)
|
||||
case _ => config
|
||||
.put("PrimaryDNS", remoteDns)
|
||||
|
||||
@@ -385,6 +385,7 @@ class MainActivity extends Activity with ServiceBoundContext with Drawer.OnDrawe
|
||||
displayFragment(new AboutFragment)
|
||||
case DRAWER_FAQ => launchUrl(getString(R.string.faq_url))
|
||||
case DRAWER_CUSTOM_RULES => displayFragment(new CustomRulesFragment)
|
||||
case _ => // Ignore
|
||||
}
|
||||
true // unexpected cases will throw exception
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,15 +19,39 @@ import scala.io.Source
|
||||
* @author Mygod
|
||||
*/
|
||||
class Acl {
|
||||
import 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]()
|
||||
val urls = new mutable.SortedList[String]()
|
||||
val urls = new mutable.SortedList[URL]()(urlOrdering)
|
||||
|
||||
@DatabaseField
|
||||
var bypass: Boolean = _
|
||||
|
||||
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 = {
|
||||
bypassHostnames.clear()
|
||||
bypassHostnames ++= other.bypassHostnames
|
||||
@@ -38,23 +64,24 @@ class Acl {
|
||||
bypass = other.bypass
|
||||
this
|
||||
}
|
||||
def fromSource(value: Source, defaultBypass: Boolean = true): Acl = {
|
||||
def fromSource(value: Source, defaultBypass: Boolean = false): Acl = {
|
||||
bypassHostnames.clear()
|
||||
proxyHostnames.clear()
|
||||
this.subnets.clear()
|
||||
this.urls.clear()
|
||||
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 => if (!in_urls) line else ""
|
||||
case -1 => line
|
||||
case index =>
|
||||
if (line.contains("URLS_BEGIN")) in_urls = true
|
||||
if (line.contains("URLS_END")) in_urls = false
|
||||
line.substring(0, index) // trim comments
|
||||
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
|
||||
@@ -68,8 +95,7 @@ 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 => if (input.startsWith("http://") || input.startsWith("https://"))
|
||||
urls += input else hostnames += input
|
||||
case _: IllegalArgumentException => hostnames += input
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
@@ -78,24 +104,26 @@ class Acl {
|
||||
}
|
||||
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_BEGIN\n")
|
||||
result.append(urls.mkString("\n"))
|
||||
if (network) {
|
||||
try {
|
||||
urls.foreach((url: String) => result.append(Source.fromURL(url).mkString))
|
||||
} catch {
|
||||
case _: IOException => // ignore
|
||||
}
|
||||
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("#URLS_END\n")
|
||||
bypassHostnames ++= child.bypassHostnames
|
||||
proxyHostnames ++= child.proxyHostnames
|
||||
subnets ++= child.subnets
|
||||
}
|
||||
urls.clear()
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
if (result.isEmpty) {
|
||||
result.append(if (bypass) "[bypass_all]\n" else "[proxy_all]\n")
|
||||
}
|
||||
override def toString: String = {
|
||||
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)
|
||||
@@ -109,13 +137,10 @@ class Acl {
|
||||
result.append(proxyList.mkString("\n"))
|
||||
result.append('\n')
|
||||
}
|
||||
result.append(urls.map("#IMPORT_URL <%s>\n".format(_)).mkString)
|
||||
result.toString
|
||||
}
|
||||
|
||||
override def toString: String = getAclString(false)
|
||||
|
||||
def isValidCustomRules: Boolean = bypass && bypassHostnames.isEmpty
|
||||
|
||||
// Don't change: dummy fields for OrmLite interaction
|
||||
|
||||
// noinspection ScalaUnusedSymbol
|
||||
@@ -130,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"
|
||||
@@ -137,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 = {
|
||||
@@ -144,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)
|
||||
}
|
||||
|
||||
@@ -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,10 +140,13 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
})
|
||||
}
|
||||
|
||||
def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size
|
||||
def getItemCount: Int = acl.subnets.size + acl.proxyHostnames.size + acl.urls.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))
|
||||
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))
|
||||
@@ -145,21 +163,29 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
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), defaultBypass = true)
|
||||
var result = -1
|
||||
for (hostname <- acl.proxyHostnames) result = addHostname(hostname)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -169,8 +195,14 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
undoManager.remove((i, acl.subnets(i)))
|
||||
acl.subnets.remove(i)
|
||||
} else {
|
||||
undoManager.remove((j, acl.proxyHostnames(j)))
|
||||
acl.proxyHostnames.remove(j)
|
||||
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()
|
||||
@@ -181,9 +213,13 @@ class CustomRulesFragment extends ToolbarFragment with Toolbar.OnMenuItemClickLi
|
||||
acl.subnets.remove(subnet)
|
||||
apply()
|
||||
case hostname: String =>
|
||||
notifyItemRemoved(acl.proxyHostnames.indexOf(hostname))
|
||||
notifyItemRemoved(acl.subnets.size + acl.proxyHostnames.indexOf(hostname))
|
||||
acl.proxyHostnames.remove(hostname)
|
||||
apply()
|
||||
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: _*)
|
||||
@@ -192,20 +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.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()
|
||||
}
|
||||
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.subnets
|
||||
selectedItems ++= acl.proxyHostnames
|
||||
selectedItems ++= acl.urls
|
||||
onSelectedItemsUpdated()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
@@ -214,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.proxyHostnames(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 => " "
|
||||
@@ -245,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)
|
||||
@@ -277,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()
|
||||
@@ -302,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.proxyHostnames.foreach(adapter.addHostname)
|
||||
acl.urls.foreach(adapter.addURL)
|
||||
true
|
||||
case _ => false
|
||||
}
|
||||
|
||||
@@ -141,7 +141,9 @@ class DBHelper(val context: Context)
|
||||
}
|
||||
|
||||
if (oldVersion < 23) {
|
||||
profileDao.executeRawNoArgs("BEGIN TRANSACTION;")
|
||||
TableUtils.createTable(connectionSource, classOf[KeyValuePair])
|
||||
profileDao.executeRawNoArgs("COMMIT;")
|
||||
import KeyValuePair._
|
||||
val old = PreferenceManager.getDefaultSharedPreferences(app)
|
||||
kvPairDao.createOrUpdate(new KeyValuePair(Key.id, TYPE_INT,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- proxy category -->
|
||||
<string name="proxy_cat">"تنظیمات سرور"</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="yes">"بله"</string>
|
||||
<string name="no">"خیر"</string>
|
||||
<string name="apply">"تایید"</string>
|
||||
</resources>
|
||||
@@ -6,4 +6,10 @@
|
||||
|
||||
<!-- feature category -->
|
||||
<string name="feature_cat">"기능 설정"</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="yes">"예"</string>
|
||||
<string name="no">"아니오"</string>
|
||||
<string name="unsaved_changes_prompt">"변경 사항이 저장되지 않았습니다. 저장하시겠습니까?"</string>
|
||||
<string name="apply">"적용"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1 @@
|
||||
sbt.version=0.13.16
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.8")
|
||||
addSbtPlugin("org.scala-android" % "sbt-android" % "1.7.9")
|
||||
|
||||
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.10")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user