Commit 952ec45c authored by Mygod's avatar Mygod

Merge branch 'master' into preference-1.1

parents ef1ee897 9a08abc0
......@@ -48,7 +48,7 @@ Put an `x` inside the [ ] that applies.
* [ ] Apps VPN mode
* [ ] Bypass mode
* Remote DNS: 8.8.8.8
* [ ] DNS Forwarding
* [ ] DNS over UDP
* Plugin configuration (if applicable):
* [ ] Auto Connect
* [ ] TCP Fast Open
......
......@@ -12,8 +12,9 @@ buildscript {
junitVersion = '4.12'
androidTestVersion = '1.1.1'
androidEspressoVersion = '3.1.1'
versionCode = 4070150
versionName = '4.7.1-nightly'
versionCode = 4070350
versionName = '4.7.3-nightly'
resConfigs = ['es', 'fa', 'fr', 'ja', 'ko', 'ru', 'tr', 'zh-rCN', 'zh-rTW']
}
repositories {
......@@ -25,10 +26,10 @@ buildscript {
}
dependencies {
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
classpath 'com.android.tools.build:gradle:3.3.1'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0'
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
classpath 'com.google.gms:google-services:4.2.0'
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0'
classpath 'io.fabric.tools:gradle:1.27.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
......
......@@ -45,7 +45,7 @@ androidExtensions {
def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0'
def workVersion = '1.0.0-rc02'
def workVersion = '1.0.0'
dependencies {
api project(':plugin')
api "android.arch.work:work-runtime-ktx:$workVersion"
......
......@@ -106,8 +106,8 @@ object BaseService {
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
private val handler = Handler()
override fun getState(): Int = data!!.state.ordinal
override fun getProfileName(): String = data!!.proxy?.profile?.name ?: "Idle"
override fun getState(): Int = (data?.state ?: State.Idle).ordinal
override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle"
override fun registerCallback(cb: IShadowsocksServiceCallback) {
callbacks.register(cb)
......@@ -117,6 +117,7 @@ object BaseService {
repeat(callbacks.beginBroadcast()) {
try {
work(callbacks.getBroadcastItem(it))
} catch (_: DeadObjectException) {
} catch (e: Exception) {
printLog(e)
}
......@@ -133,7 +134,7 @@ object BaseService {
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
.filter { it.second != null }
.map { Triple(it.first, it.second!!.first, it.second!!.second) }
if (stats.any { it.third } && data!!.state == State.Connected && bandwidthListeners.isNotEmpty()) {
if (stats.any { it.third } && data?.state == State.Connected && bandwidthListeners.isNotEmpty()) {
val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
broadcast { item ->
if (bandwidthListeners.contains(item.asBinder())) {
......@@ -149,16 +150,17 @@ object BaseService {
val wasEmpty = bandwidthListeners.isEmpty()
if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
if (wasEmpty) registerTimeout()
if (data!!.state != State.Connected) return
if (data?.state != State.Connected) return
var sum = TrafficStats()
val proxy = data!!.proxy ?: return
val data = data
val proxy = data?.proxy ?: return
proxy.trafficMonitor?.out.also { stats ->
cb.trafficUpdated(proxy.profile.id, if (stats == null) sum else {
sum += stats
stats
})
}
data!!.udpFallback?.also { udpFallback ->
data.udpFallback?.also { udpFallback ->
udpFallback.trafficMonitor?.out.also { stats ->
cb.trafficUpdated(udpFallback.profile.id, if (stats == null) TrafficStats() else {
sum += stats
......@@ -232,7 +234,7 @@ object BaseService {
File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
File(configRoot, CONFIG_FILE),
if (udpFallback == null) "-u" else null)
check(udpFallback?.pluginPath == null)
check(udpFallback?.pluginPath == null) { "UDP fallback cannot have plugins" }
udpFallback?.start(this,
File(Core.deviceStorage.noBackupFilesDir, "stat_udp"),
File(configRoot, CONFIG_FILE_UDP),
......
......@@ -27,7 +27,7 @@ import android.text.TextUtils
import android.util.Log
import com.crashlytics.android.Crashlytics
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
object Executable {
const val REDSOCKS = "libredsocks.so"
......@@ -40,7 +40,7 @@ object Executable {
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
val exe = File(try {
File(process, "cmdline").inputStream().bufferedReader().readText()
} catch (_: FileNotFoundException) {
} catch (_: IOException) {
continue
}.split(Character.MIN_VALUE, limit = 2).first())
if (EXECUTABLES.contains(exe.name)) try {
......
......@@ -61,14 +61,12 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
conn.doOutput = true
val proxies = try {
withTimeoutOrNull(10_000) {
withContext(Dispatchers.IO) {
conn.outputStream.bufferedWriter().use {
it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
}
conn.inputStream.bufferedReader().readText()
}
} ?: throw UnknownHostException()
} finally {
conn.disconnectFromMain()
}.split('|').toMutableList()
......@@ -85,19 +83,19 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
}
// it's hard to resolve DNS on a specific interface so we'll do it here
if (profile.host.parseNumericAddress() == null) profile.host = withTimeoutOrNull(10_000) {
if (profile.host.parseNumericAddress() == null) {
var retries = 0
while (isActive) try {
while (true) try {
val io = GlobalScope.async(Dispatchers.IO) { service.resolver(profile.host) }
return@withTimeoutOrNull io.await().firstOrNull()
profile.host = io.await().firstOrNull()?.hostAddress ?: throw UnknownHostException()
return
} catch (e: UnknownHostException) {
// retries are only needed on Chrome OS where arc0 is brought up/down during VPN changes
if (!DataStore.hasArc0) throw e
Thread.yield()
Crashlytics.log(Log.WARN, "ProxyInstance-resolver", "Retry resolving attempt #${++retries}")
}
null // only here for type resolving
}?.hostAddress ?: throw UnknownHostException()
}
}
/**
......
......@@ -55,7 +55,7 @@ class ChannelMonitor : Thread("ChannelMonitor") {
pendingRegistrations.poll()!!.apply {
try {
result.complete(registerInternal(channel, ops, listener))
} catch (e: ClosedChannelException) {
} catch (e: Exception) {
result.completeExceptionally(e)
}
}
......@@ -100,7 +100,7 @@ class ChannelMonitor : Thread("ChannelMonitor") {
while (running) {
val num = try {
selector.select()
} catch (e: IOException) {
} catch (e: Exception) {
printLog(e)
continue
}
......
......@@ -81,7 +81,7 @@ class HttpsTest : ViewModel() {
fun testConnection() {
cancelTest()
status.value = Status.Testing
val url = URL("https", when (Core.currentProfile!!.first.route) {
val url = URL("https", when ((Core.currentProfile ?: return).first.route) {
Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com"
}, "/generate_204")
......
......@@ -157,9 +157,12 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
channel.configureBlocking(false)
monitor.wait(channel, SelectionKey.OP_WRITE)
check(channel.send(remoteDns.udpWrap(packet), proxy) > 0)
monitor.wait(channel, SelectionKey.OP_READ)
val result = remoteDns.udpReceiveBuffer(UDP_PACKET_SIZE)
while (channel.receive(result) != proxy) result.clear()
while (isActive) {
monitor.wait(channel, SelectionKey.OP_READ)
if (channel.receive(result) == proxy) break
result.clear()
}
result.flip()
remoteDns.udpUnwrap(result)
result
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="quick_toggle">"Alternar"</string>
<string name="remote_dns">"DNS Remoto"</string>
<string name="stat_summary">"Enviado: \t\t\t\t\t%3$s\t↑\t%1$s
Recibido: \t%4$s\t↓\t%2$s"</string>
<string name="connection_test_testing">"Probando..."</string>
<string name="connection_test_available">"Éxito: El establecimiento de la comunicación HTTPS tomó %dms"</string>
<string name="connection_test_error">"Fallo al detectar conexión a Internet: %s"</string>
<string name="connection_test_fail">"Internet no disponible"</string>
<string name="connection_test_error_status_code">"Código de error: #%d"</string>
<!-- proxy category -->
<string name="profile_name">"Nombre de Perfil"</string>
<string name="proxy">"Servidor"</string>
<string name="remote_port">"Puerto Remoto"</string>
<string name="sitekey">"Contraseña"</string>
<string name="enc_method">"Método de Cifrado"</string>
<!-- feature category -->
<string name="ipv6">"Ruta IPv6"</string>
<string name="ipv6_summary">"Redireccionar tráfico IPv6 a ruta"</string>
<string name="route_list">"Ruta"</string>
<string name="route_entry_gfwlist">"Lista GFW"</string>
<string name="proxied_apps">"Apps en modo VPN"</string>
<string name="proxied_apps_summary">"Configurar modo VPN para las apps seleccionadas"</string>
<string name="on">"Encendido"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"Modo Bypass"</string>
<string name="auto_connect">"Conexión Automática"</string>
<string name="auto_connect_summary">"Activar Shadowsocks al iniciar"</string>
<string name="tcp_fastopen_summary_unsupported">"Versión del Núcleo sin soporte: %s &lt; 3.7.1"</string>
<string name="udp_dns">"Enviar DNS sobre UDP"</string>
<!-- notification category -->
<string name="forward_success">"Shadowsocks inició."</string>
<string name="invalid_server">"Nombre de servidor inválido"</string>
<string name="service_failed">"Fallo al conectar al servidor remoto"</string>
<string name="stop">"Detener"</string>
<string name="stopping">"Apagando..."</string>
<string name="reboot_required">"Fallo al iniciar servicio VPN. Puede que tengas que reiniciar tu dispositivo."</string>
<string name="profile_invalid_input">"No se ha encontrado datos de perfil válidos."</string>
<!-- alert category -->
<string name="profile_empty">"Por favor selecciona un perfil"</string>
<string name="proxy_empty">"Proxy/Contraseña no deberían estar vacíos"</string>
<string name="connect">"Conectar"</string>
<!-- menu category -->
<string name="profiles">"Perfiles"</string>
<string name="settings">"Preferencias"</string>
<string name="faq">"FAQ"</string>
<string name="about">"Acerca de"</string>
<string name="edit">"Editar"</string>
<string name="share">"Compartir"</string>
<string name="add_profile">"Agregar Perfil"</string>
<string name="action_apply_all">"Aplicar preferencias a todos los Perfiles"</string>
<string name="action_export">"Exportar a portapapeles"</string>
<string name="action_import">"Importar desde portapapeles"</string>
<string name="action_export_msg">"¡Exportado satisfactoriamente!"</string>
<string name="action_export_err">"Fallo al exportar."</string>
<string name="action_import_msg">"¡Importado satisfactoriamente!"</string>
<string name="action_import_err">"Fallo al importar."</string>
<!-- profile -->
<string name="profile_config">"Configuración de Perfil"</string>
<string name="delete">"Remover"</string>
<string name="delete_confirm_prompt">"¿Estás seguro de querer remover este perfil?"</string>
<string name="share_qr_nfc">"Código QR/NFC"</string>
<string name="add_profile_dialog">"Agregar este Perfil Shadowsocks?"</string>
<string name="add_profile_methods_scan_qr_code">"Escanear código QR"</string>
<plurals name="removed">
<item quantity="one">"Removido"</item>
<item quantity="other">"%d elementos removidos"</item>
</plurals>
<string name="undo">"Deshacer"</string>
<!-- tasker -->
<string name="toggle_service_state">"Iniciar el servicio"</string>
<string name="start_service_default">"Conectar al servidor actual"</string>
<string name="start_service">"Conectar a %s"</string>
<string name="stop_service">"Cambiar a %s"</string>
<string name="profile_default">"Usar el perfil actual"</string>
<!-- status -->
<string name="sent">"Enviado:"</string>
<string name="received">"Recibido:"</string>
<!-- status -->
<string name="connecting">"Conectando..."</string>
<string name="vpn_connected">"Conectado, presiona para verificar la conexión"</string>
<string name="not_connected">"No conectado"</string>
<!-- acl -->
<string name="custom_rules">"Reglas personalizadas"</string>
<string name="action_add_rule">"Agregar regla(s)..."</string>
<string name="edit_rule">"Editar regla"</string>
<string name="route_entry_all">"Todo"</string>
<!-- plugin -->
<string name="plugin">"Complemento"</string>
<string name="plugin_configure">"Configurar..."</string>
<string name="plugin_disabled">"Deshabilitado"</string>
<string name="plugin_unknown">"Complemento desconocido %s"</string>
<string name="plugin_untrusted">"Precaución: Este complemento no parece venir de una fuente confiable."</string>
<string name="profile_plugin">"Complemento: %s"</string>
<string name="add_profile_scanner_permission_required">"Se requiere permiso de cámara para escanear códigos QR."</string>
<!-- notification category -->
<string name="service_vpn">"Servicio VPN"</string>
<string name="add_profile_methods_manual_settings">"Configuración manual"</string>
<!-- misc -->
<string name="advanced">"Avanzado"</string>
<!-- misc -->
<string name="service_mode">"Modo de servicio"</string>
<string name="service_mode_proxy">"Sólo Proxy"</string>
<string name="service_mode_vpn">"VPN"</string>
<string name="port_proxy">"Puerto proxy SOCKS5"</string>
<string name="port_local_dns">"Puerto DNS local"</string>
<string name="service_proxy">"Servicio Proxy"</string>
<string name="service_transproxy">"Servicio Transproxy"</string>
<string name="vpn_permission_denied">"Permiso denegado al crear servicio VPN"</string>
<string name="direct_boot_aware_summary">"La información del perfil seleccionado será menos protegida"</string>
<string name="action_import_file">"Importar desde archivo..."</string>
<string name="night_mode">"Modo nocturno"</string>
<string name="night_mode_system">"Seguir sistema"</string>
<string name="night_mode_auto">"Auto"</string>
<string name="night_mode_on">"Encendido"</string>
<string name="night_mode_off">"Apagado"</string>
<string name="send_email">"Enviar email"</string>
<string name="action_export_more">"Exportar..."</string>
<string name="action_export_file">"Exportar a archivo..."</string>
<string name="share_over_lan">"Compartir por LAN"</string>
<string name="connection_test_pending">"Verificar conectividad"</string>
<string name="file_manager_missing">"Por favor, instala un explorador de archivos como MiXplorer"</string>
<string name="action_replace_file">"Reemplazar desde archivo..."</string>
</resources>
\ No newline at end of file
......@@ -20,11 +20,17 @@
<string name="proxied_apps">"حالت "</string>
<string name="proxied_apps_summary">"تنظیم حالت وی‌پی‌ان برای اپلیکیشن‌های انتخاب شده"</string>
<string name="on">"روشن"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"حالت صرف‌نظر"</string>
<string name="bypass_apps_summary">"این اپلیکیشن‌ها از وی‌پی‌ان استفاده نکنند"</string>
<string name="auto_connect">"وصل‌شدن اتوماتیک"</string>
<string name="auto_connect_summary">"فعال‌شدن شدوساکس لحظه روشن‌شدن گوشی"</string>
<!-- Fuzzy -->
<string name="udp_dns">"ارسال دی‌ان‌اس"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"ارسال کلیه درخواست‌های دی‌ان‌اس به سرور "</string>
<!-- notification category -->
......@@ -100,6 +106,8 @@
<!-- misc -->
<string name="advanced">"پیشرفته"</string>
<!-- misc -->
<string name="service_mode">"وضعیت سرویس"</string>
<string name="service_mode_proxy">"فقط پروکسی"</string>
<string name="service_mode_vpn">"وی‌پی‌ان"</string>
......
......@@ -25,6 +25,8 @@ Reçu : \t\t\t%4$s\t↓\t%2$s"</string>
<string name="proxied_apps">"Mode VPN Apps"</string>
<string name="proxied_apps_summary">"Configurer le mode VPN pour les applications sélectionnées"</string>
<string name="on">"Activé"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"Mode exclusion"</string>
<string name="bypass_apps_summary">"Activer cette option pour exclure les applications sélectionnées"</string>
<string name="auto_connect">"Connection auto"</string>
......@@ -33,6 +35,8 @@ Reçu : \t\t\t%4$s\t↓\t%2$s"</string>
<!-- Fuzzy -->
<string name="tcp_fastopen_summary">"Activer nécessite la permission ROOT"</string>
<string name="tcp_fastopen_summary_unsupported">"Version du noyau non supportée : %s &lt;3.7.1"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"Faire suivre toutes les requêtes DNS au serveur distant"</string>
<!-- notification category -->
......@@ -116,6 +120,8 @@ Reçu : \t\t\t%4$s\t↓\t%2$s"</string>
<!-- misc -->
<string name="advanced">"Avancé"</string>
<!-- misc -->
<string name="service_mode">"Mode Service"</string>
<string name="service_mode_proxy">"Proxy seul"</string>
<string name="port_proxy">"Port proxy SOCKS5"</string>
......
......@@ -24,6 +24,8 @@
<string name="proxied_apps">"アプリ別のVPNモードを使用"</string>
<string name="proxied_apps_summary">"アプリ別のVPNモードを設定"</string>
<string name="on">"ON"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"バイパスモード(迂回モード)"</string>
<string name="bypass_apps_summary">"このモードを選択した場合、指定したアプリのパケットはプロキシを経由しない"</string>
<string name="auto_connect">"自動接続"</string>
......@@ -32,7 +34,11 @@
<!-- Fuzzy -->
<string name="tcp_fastopen_summary">"使用するには ROOT 権限が必要"</string>
<string name="tcp_fastopen_summary_unsupported">"ご利用のカーネルバージョンはサポートしておりません:%s &lt; 3.7.1"</string>
<!-- Fuzzy -->
<string name="udp_dns">"DNS 転送"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"DNS プロトコルでリモードサーバーにパケットを転送"</string>
<!-- notification category -->
......@@ -122,6 +128,8 @@
<!-- misc -->
<string name="advanced">"アドバンス"</string>
<!-- misc -->
<string name="service_mode">"サービスモード"</string>
<string name="service_mode_proxy">"プロキシのみ"</string>
<string name="service_mode_transproxy">"トランスプロキシ"</string>
......
......@@ -26,6 +26,8 @@
<string name="proxied_apps">"원하는 앱만 프락시 적용하기"</string>
<string name="proxied_apps_summary">"선택한 앱에만 프락시를 적용합니다"</string>
<string name="on">"활성화"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"선택된 앱들만 프록시 적용 제외하기"</string>
<string name="bypass_apps_summary">"이 옵션을 활성화하면 선택된 앱들은 프락시를 사용하지 않습니다"</string>
<string name="auto_connect">"자동 접속"</string>
......@@ -34,7 +36,11 @@
<!-- Fuzzy -->
<string name="tcp_fastopen_summary">"루트 권한이 필요합니다"</string>
<string name="tcp_fastopen_summary_unsupported">"지원하지 않는 버전의 커널입니다: %s &lt; 3.7.1"</string>
<!-- Fuzzy -->
<string name="udp_dns">"DNS 포워딩"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"모든 DNS 요청을 외부로 포워딩 합니다"</string>
<!-- notification category -->
......
......@@ -5,6 +5,7 @@
<string name="stat_summary">"Отправлено: \t%3$s\t↑\t%1$s
Получено: \t\t\t%4$s\t↓\t%2$s"</string>
<string name="connection_test_testing">"Проверка…"</string>
<string name="connection_test_available">"Успешно: рукопожатие HTTPS заняло %dмс"</string>
<string name="connection_test_error">"Сбой проверки интернет-соединения: %s"</string>
<string name="connection_test_fail">"Интернет недоступен"</string>
<string name="connection_test_error_status_code">"Код ошибки: #%d"</string>
......@@ -24,15 +25,19 @@
<string name="proxied_apps">"Режим VPN для выбранных приложений"</string>
<string name="proxied_apps_summary">"Выбрать приложения, для которых нужно использовать режим VPN"</string>
<string name="on">"Вкл"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"В обход прокси"</string>
<string name="bypass_apps_summary">"Включите эту опцию для работы выбранных приложений в обход прокси"</string>
<string name="auto_connect">"Авто-подключение"</string>
<string name="auto_connect_summary">"Запускать Shadowsocks при включении"</string>
<string name="tcp_fastopen_summary">"Переключение может запрашивать ROOT права"</string>
<string name="tcp_fastopen_summary_unsupported">"Неподдерживаемая версия ядра: %s &lt; 3.7.1"</string>
<!-- Fuzzy -->
<string name="tcp_fastopen_summary">"Переключение требует наличия ROOT прав"</string>
<string name="tcp_fastopen_summary_unsupported">"Неподдерживаемая версия ядра: %s &lt; 3.7.1"</string>
<string name="udp_dns">"Перенаправление DNS"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"Перенаправлять все DNS-запросы на удалённый сервер"</string>
<!-- notification category -->
......@@ -125,6 +130,8 @@
<!-- misc -->
<string name="advanced">"Дополнительно"</string>
<!-- misc -->
<string name="service_mode">"Режим"</string>
<string name="service_mode_proxy">"Только прокси"</string>
<string name="service_mode_transproxy">"Прозрачный прокси"</string>
......
......@@ -21,6 +21,8 @@
<string name="proxied_apps">"Uygulamalar VPN modu"</string>
<string name="proxied_apps_summary">"Seçili uygulamalar için VPN'i yapılandır"</string>
<string name="on">"Açık"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"Bypass Modu"</string>
<string name="bypass_apps_summary">"Seçili uygulamaları bypass etmek için bu seçeneği açın"</string>
<string name="auto_connect">"Otomatik Bağlan"</string>
......@@ -29,7 +31,11 @@
<!-- Fuzzy -->
<string name="tcp_fastopen_summary">"Açıp kapamak için ROOT yetkisi gerekiyor"</string>
<string name="tcp_fastopen_summary_unsupported">"Desteklenmeyen kernel sürümü: %s &lt; 3.7.1"</string>
<!-- Fuzzy -->
<string name="udp_dns">"DNS yönlendirme"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"Tüm DNS isteklerini remote'a yönlendir"</string>
<!-- notification category -->
......@@ -118,6 +124,8 @@
<!-- misc -->
<string name="advanced">"Gelişmiş"</string>
<!-- misc -->
<string name="service_mode">"Servis modu"</string>
<string name="service_mode_proxy">"Sadece proxy"</string>
<string name="port_proxy">"SOCKS5 proxy portu"</string>
......
......@@ -25,14 +25,14 @@
<string name="proxied_apps">"分应用 VPN"</string>
<string name="proxied_apps_summary">"允许部分应用绕过 VPN"</string>
<string name="on">"启用"</string>
<string name="bypass_apps">"绕行模式"</string>
<string name="bypass_apps">"绕行"</string>
<string name="bypass_apps_summary">"绕过选择的应用"</string>
<string name="auto_connect">"自动连接"</string>
<string name="auto_connect_summary">"允许 Shadowsocks 随系统启动"</string>
<string name="tcp_fastopen_summary">"切换可能需要 ROOT 权限"</string>
<string name="tcp_fastopen_summary_unsupported">"不支持的内核版本: %s &lt; 3.7.1"</string>
<string name="udp_dns">"DNS 转发"</string>
<string name="udp_dns_summary">"转发所有 DNS 请求到远程服务器"</string>
<string name="udp_dns">"使用 UDP DNS"</string>
<string name="udp_dns_summary">"需要远程服务器支持 UDP 转发"</string>
<!-- notification category -->
<string name="forward_success">"后台服务已开始运行。"</string>
......@@ -121,6 +121,8 @@
<!-- misc -->
<string name="advanced">"高级选项"</string>
<!-- misc -->
<string name="service_mode">"服务模式"</string>
<string name="service_mode_proxy">"仅代理"</string>
<string name="service_mode_transproxy">"透明代理"</string>
......@@ -147,4 +149,9 @@
<string name="share_over_lan">"通过局域网分享"</string>
<string name="connection_test_pending">"检查连接"</string>
<string name="file_manager_missing">"请安装文件管理器,如 MiXplorer"</string>
<string name="tcp_fastopen_failure">"切换失败"</string>
<string name="udp_fallback">"UDP 配置"</string>
<string name="action_replace_file">"从文件替换…"</string>
<string name="off">"关"</string>
<string name="proxied_apps_mode">"模式"</string>
</resources>
\ No newline at end of file
......@@ -26,6 +26,8 @@
<string name="proxied_apps">"個別應用程式的 VPN"</string>
<string name="proxied_apps_summary">"為已選擇的應用程式設定 VPN"</string>
<string name="on">"開"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"略過模式"</string>
<string name="bypass_apps_summary">"啟用此選項,會略過已選擇的應用程式"</string>
<string name="auto_connect">"自動連線"</string>
......@@ -34,7 +36,11 @@
<!-- Fuzzy -->
<string name="tcp_fastopen_summary">"切換需要 ROOT 權限"</string>
<string name="tcp_fastopen_summary_unsupported">"不支援的核心版本:%s &lt; 3.7.1"</string>
<!-- Fuzzy -->
<string name="udp_dns">"DNS 轉送"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"使用 UDP 向遠端轉送 DNS 封包"</string>
<!-- notification category -->
......@@ -123,6 +129,8 @@
<!-- misc -->
<string name="advanced">"高級"</string>
<!-- misc -->
<string name="service_mode">"服務模式"</string>
<string name="service_mode_proxy">"仅代理"</string>
<string name="service_mode_transproxy">"透明代理"</string>
......
......@@ -46,7 +46,9 @@
<string name="proxied_apps">Apps VPN mode</string>
<string name="proxied_apps_summary">Configure VPN mode for selected apps</string>
<string name="on">On</string>
<string name="bypass_apps">Bypass Mode</string>
<string name="off">Off</string>
<string name="proxied_apps_mode">Mode</string>
<string name="bypass_apps">Bypass</string>
<string name="bypass_apps_summary">Enable this option to bypass selected apps</string>
<string name="auto_connect">Auto Connect</string>
<string name="auto_connect_summary">Enable Shadowsocks on startup</string>
......
......@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
......@@ -26,7 +26,7 @@ android {
versionCode rootProject.versionCode
versionName rootProject.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "fa", "fr", "ja", "ko", "ru", "tr", "zh-rCN", "zh-rTW"
resConfigs rootProject.resConfigs
}
buildTypes {
debug {
......@@ -57,8 +57,9 @@ androidExtensions {
dependencies {
implementation project(':core')
implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.gms:play-services-vision:17.0.2'
implementation 'com.google.firebase:firebase-ads:17.1.3'
implementation 'com.google.firebase:firebase-ads:17.2.0'
implementation 'com.takisoft.preferencex:preferencex-simplemenu:1.0.0'
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'net.glxn.qrgen:android:2.0'
......
......@@ -32,13 +32,15 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.SparseBooleanArray
import android.view.*
import android.widget.ImageView
import android.widget.Switch
import android.widget.Filter
import android.widget.Filterable
import android.widget.SearchView
import androidx.annotation.UiThread
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.getSystemService
import androidx.core.util.set
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
......@@ -48,16 +50,19 @@ import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.layout_apps.*
import kotlinx.android.synthetic.main.layout_apps_item.view.*
import kotlinx.coroutines.*
class AppManager : AppCompatActivity() {
companion object {
@SuppressLint("StaticFieldLeak")
private var instance: AppManager? = null
private const val SWITCH = "switch"
private var receiver: BroadcastReceiver? = null
private var cachedApps: List<PackageInfo>? = null
private suspend fun getApps(pm: PackageManager) = synchronized(AppManager) {
private var cachedApps: Map<String, PackageInfo>? = null
private fun getCachedApps(pm: PackageManager) = synchronized(AppManager) {
if (receiver == null) receiver = Core.listenForPackageChanges {
synchronized(AppManager) {
receiver = null
......@@ -67,13 +72,16 @@ class AppManager : AppCompatActivity() {
}
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
.filter { it.packageName != app.packageName &&
it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false }
.filter {
when (it.packageName) {
app.packageName -> false
"android" -> true
else -> it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
}
}
.associateBy { it.packageName }
this.cachedApps = cachedApps
cachedApps
}.map {
yield()
ProxiedApp(pm, it.applicationInfo, it.packageName)
}
}
......@@ -81,59 +89,86 @@ class AppManager : AppCompatActivity() {
val packageName: String) {
val name: CharSequence = appInfo.loadLabel(pm) // cached for sorting
val icon: Drawable get() = appInfo.loadIcon(pm)
val uid get() = appInfo.uid
}
private inner class AppViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener {
private val icon = view.findViewById<ImageView>(R.id.itemicon)
private val check = view.findViewById<Switch>(R.id.itemcheck)
private lateinit var item: ProxiedApp
private val proxied get() = proxiedApps.contains(item.packageName)
init {
view.setOnClickListener(this)
}
fun bind(app: ProxiedApp) {
this.item = app
icon.setImageDrawable(app.icon)
check.text = app.name
check.isChecked = proxied
item = app
itemView.itemicon.setImageDrawable(app.icon)
itemView.title.text = app.name
itemView.desc.text = "${app.packageName} (${app.uid})"
itemView.itemcheck.isChecked = isProxiedApp(app)
}
fun handlePayload(payloads: List<String>) {
if (payloads.contains(AppManager.SWITCH)) itemView.itemcheck.isChecked = isProxiedApp(item)
}
override fun onClick(v: View?) {
if (proxied) {
proxiedApps.remove(item.packageName)
check.isChecked = false
} else {
proxiedApps.add(item.packageName)
check.isChecked = true
}
DataStore.individual = proxiedApps.joinToString("\n")
if (isProxiedApp(item)) proxiedUids.delete(item.uid) else proxiedUids[item.uid] = true
DataStore.individual = apps.filter { isProxiedApp(it) }.joinToString("\n") { it.packageName }
DataStore.dirty = true
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, AppManager.SWITCH)
}
}
private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>() {
private var apps = listOf<ProxiedApp>()
private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>(), Filterable {
private var filteredApps = apps
suspend fun reload() {
apps = getApps(packageManager)
.sortedWith(compareBy({ !proxiedApps.contains(it.packageName) }, { it.name.toString() }))
apps = getCachedApps(packageManager).map { (packageName, packageInfo) ->
yield()
ProxiedApp(packageManager, packageInfo.applicationInfo, packageName)
}.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))
}
override fun onBindViewHolder(holder: AppViewHolder, position: Int) = holder.bind(filteredApps[position])
override fun onBindViewHolder(holder: AppViewHolder, position: Int, payloads: List<Any>) {
if (payloads.isNotEmpty()) {
@Suppress("UNCHECKED_CAST")
holder.handlePayload(payloads as List<String>)
return
}
override fun onBindViewHolder(holder: AppViewHolder, position: Int) = holder.bind(apps[position])
onBindViewHolder(holder, position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder =
AppViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_apps_item, parent, false))
override fun getItemCount(): Int = apps.size
override fun getItemCount(): Int = filteredApps.size
private val filterImpl = object : Filter() {
override fun performFiltering(constraint: CharSequence) = FilterResults().apply {
val filteredApps = if (constraint.isEmpty()) apps else apps.filter {
it.name.contains(constraint, true) ||
it.packageName.contains(constraint, true) ||
it.uid.toString().contains(constraint)
}
count = filteredApps.size
values = filteredApps
}
private lateinit var proxiedApps: HashSet<String>
private lateinit var toolbar: Toolbar
private lateinit var bypassSwitch: Switch
private lateinit var appListView: RecyclerView
private lateinit var loadingView: View
override fun publishResults(constraint: CharSequence, results: FilterResults) {
@Suppress("UNCHECKED_CAST")
filteredApps = results.values as List<ProxiedApp>
notifyDataSetChanged()
}
}
override fun getFilter(): Filter = filterImpl
}
private val proxiedUids = SparseBooleanArray()
private val clipboard by lazy { getSystemService<ClipboardManager>()!! }
private var loader: Job? = null
private var apps = emptyList<ProxiedApp>()
private val appsAdapter = AppsAdapter()
private val shortAnimTime by lazy { resources.getInteger(android.R.integer.config_shortAnimTime).toLong() }
private fun View.crossFadeFrom(other: View) {
......@@ -150,25 +185,29 @@ class AppManager : AppCompatActivity() {
}).duration = shortAnimTime
}
private fun initProxiedApps(str: String = DataStore.individual) {
proxiedApps = str.split('\n').toHashSet()
private fun initProxiedUids(str: String = DataStore.individual) {
proxiedUids.clear()
val apps = getCachedApps(packageManager)
for (line in str.lineSequence()) proxiedUids[(apps[line] ?: continue).applicationInfo.uid] = true
}
private fun isProxiedApp(app: ProxiedApp) = proxiedUids[app.uid]
@UiThread
private fun loadApps() {
loader?.cancel()
loader = GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
loadingView.crossFadeFrom(appListView)
val adapter = appListView.adapter as AppsAdapter
loading.crossFadeFrom(list)
val adapter = list.adapter as AppsAdapter
withContext(Dispatchers.IO) { adapter.reload() }
adapter.notifyDataSetChanged()
appListView.crossFadeFrom(loadingView)
adapter.filter.filter(search.query)
list.crossFadeFrom(loading)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.layout_apps)
toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
......@@ -176,25 +215,29 @@ class AppManager : AppCompatActivity() {
DataStore.proxyApps = true
DataStore.dirty = true
}
findViewById<Switch>(R.id.onSwitch).setOnCheckedChangeListener { _, checked ->
DataStore.proxyApps = checked
bypassGroup.check(if (DataStore.bypass) R.id.btn_bypass else R.id.btn_on)
bypassGroup.setOnCheckedChangeListener { _, checkedId ->
DataStore.dirty = true
when (checkedId) {
R.id.btn_off -> {
DataStore.proxyApps = false
finish()
}
bypassSwitch = findViewById(R.id.bypassSwitch)
bypassSwitch.isChecked = DataStore.bypass
bypassSwitch.setOnCheckedChangeListener { _, checked ->
DataStore.bypass = checked
DataStore.dirty = true
R.id.btn_on -> DataStore.bypass = false
R.id.btn_bypass -> DataStore.bypass = true
}
}
initProxiedUids()
list.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
list.itemAnimator = DefaultItemAnimator()
list.adapter = appsAdapter
initProxiedApps()
loadingView = findViewById(R.id.loading)
appListView = findViewById(R.id.list)
appListView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
appListView.itemAnimator = DefaultItemAnimator()
appListView.adapter = AppsAdapter()
search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = false
override fun onQueryTextChange(newText: String?) = true.also { appsAdapter.filter.filter(newText) }
})
instance = this
loadApps()
......@@ -215,14 +258,14 @@ class AppManager : AppCompatActivity() {
ProfileManager.updateProfile(it)
}
if (DataStore.directBootAware) DirectBoot.update()
Snackbar.make(appListView, R.string.action_apply_all, Snackbar.LENGTH_LONG).show()
} else Snackbar.make(appListView, R.string.action_export_err, Snackbar.LENGTH_LONG).show()
Snackbar.make(list, R.string.action_apply_all, Snackbar.LENGTH_LONG).show()
} else Snackbar.make(list, R.string.action_export_err, Snackbar.LENGTH_LONG).show()
return true
}
R.id.action_export_clipboard -> {
clipboard.primaryClip = ClipData.newPlainText(Key.individual,
"${DataStore.bypass}\n${DataStore.individual}")
Snackbar.make(appListView, R.string.action_export_msg, Snackbar.LENGTH_LONG).show()
Snackbar.make(list, R.string.action_export_msg, Snackbar.LENGTH_LONG).show()
return true
}
R.id.action_import_clipboard -> {
......@@ -232,16 +275,16 @@ class AppManager : AppCompatActivity() {
try {
val (enabled, apps) = if (i < 0) Pair(proxiedAppString, "") else
Pair(proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1))
bypassSwitch.isChecked = enabled.toBoolean()
bypassGroup.check(if (enabled.toBoolean()) R.id.btn_bypass else R.id.btn_on)
DataStore.individual = apps
DataStore.dirty = true
Snackbar.make(appListView, R.string.action_import_msg, Snackbar.LENGTH_LONG).show()
initProxiedApps(apps)
loadApps()
Snackbar.make(list, R.string.action_import_msg, Snackbar.LENGTH_LONG).show()
initProxiedUids(apps)
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, AppManager.SWITCH)
return true
} catch (_: IllegalArgumentException) { }
}
Snackbar.make(appListView, R.string.action_import_err, Snackbar.LENGTH_LONG).show()
Snackbar.make(list, R.string.action_import_err, Snackbar.LENGTH_LONG).show()
}
}
return false
......
......@@ -108,7 +108,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
// service
var state = BaseService.State.Idle
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = changeState(state, msg, true)
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) =
changeState(state, msg, true)
override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
if (profileId == 0L) this@MainActivity.stats.updateTraffic(
stats.txRate, stats.rxRate, stats.txTotal, stats.rxTotal)
......@@ -122,7 +123,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
}
private fun changeState(state: BaseService.State, msg: String? = null, animate: Boolean = false) {
fab.changeState(state, animate)
fab.changeState(state, this.state, animate)
stats.changeState(state)
if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show()
this.state = state
......
......@@ -41,7 +41,8 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
private var tapPending = false
private val connection = ShadowsocksConnection()
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) = updateTile(state) { profileName }
override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) =
updateTile(state) { profileName }
override fun onServiceConnected(service: IShadowsocksService) {
updateTile(BaseService.State.values()[service.state]) { service.profileName }
if (tapPending) {
......
......@@ -68,11 +68,13 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
return drawableState
}
fun changeState(state: BaseService.State, animate: Boolean) {
fun changeState(state: BaseService.State, previousState: BaseService.State, animate: Boolean) {
when (state) {
BaseService.State.Connecting -> changeState(iconConnecting, animate)
BaseService.State.Connected -> changeState(iconConnected, animate)
BaseService.State.Stopping -> changeState(iconStopping, animate)
BaseService.State.Stopping -> {
changeState(iconStopping, animate && previousState == BaseService.State.Connected)
}
else -> changeState(iconStopped, animate)
}
if (state == BaseService.State.Connected) {
......@@ -86,13 +88,12 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
isEnabled = state.canStop || state == BaseService.State.Stopped
}
private fun counters(a: AnimatedVectorDrawableCompat, b: AnimatedVectorDrawableCompat): Boolean =
private fun changeState(icon: AnimatedVectorDrawableCompat, animate: Boolean) {
fun counters(a: AnimatedVectorDrawableCompat, b: AnimatedVectorDrawableCompat): Boolean =
a == iconStopped && b == iconConnecting ||
a == iconConnecting && b == iconStopped ||
a == iconConnected && b == iconStopping ||
a == iconStopping && b == iconConnected
private fun changeState(icon: AnimatedVectorDrawableCompat, animate: Boolean) {
if (animate) {
if (animationQueue.size < 2 || !counters(animationQueue.last, icon)) {
animationQueue.add(icon)
......
......@@ -7,41 +7,80 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:duplicateParentState="false">
<include layout="@layout/toolbar_light_dark"/>
<LinearLayout android:layout_width="fill_parent"
<androidx.appcompat.widget.Toolbar
android:layout_height="?attr/actionBarSize"
android:layout_width="match_parent"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:touchscreenBlocksFocus="false"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.DayNight"
android:id="@+id/toolbar">
<SearchView
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:iconifiedByDefault="false"
android:queryHint="@android:string/search_go"/>
</androidx.appcompat.widget.Toolbar>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:elevation="1dp"
android:background="?android:colorBackground"
android:orientation="vertical"
tools:ignore="RtlSymmetry">
android:elevation="1dp">
<Switch
android:id="@+id/onSwitch"
android:layout_width="match_parent"
<TextView
android:id="@+id/tv_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:paddingBottom="12dp"
android:paddingEnd="16dp"
android:paddingStart="72dp"
android:paddingTop="12dp"
android:text="@string/on"
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp"/>
android:layout_marginStart="16dp"
android:minWidth="42dp"
android:text="@string/proxied_apps_mode"
app:layout_constraintBottom_toBottomOf="@+id/bypassGroup"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/bypassGroup" />
<Switch
android:id="@+id/bypassSwitch"
android:layout_width="match_parent"
<RadioGroup
android:id="@+id/bypassGroup"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/tv_mode"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/btn_off"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/off" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/btn_on"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:checked="true"
android:paddingBottom="12dp"
android:paddingEnd="16dp"
android:paddingStart="72dp"
android:paddingTop="12dp"
android:layout_weight="1"
android:text="@string/on" />
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/btn_bypass"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/bypass_apps"
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp"
android:tooltipText="@string/bypass_apps_summary"/>
</LinearLayout>
tools:checked="true"
android:tooltipText="@string/bypass_apps_summary" />
</RadioGroup>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/snackbar"
android:layout_width="match_parent"
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:background="?android:attr/selectableItemBackground">
android:background="?android:attr/selectableItemBackground"
android:focusable="true">
<ImageView
android:id="@+id/itemicon"
android:layout_width="72dp"
android:layout_height="56dp"
android:paddingBottom="4dp"
android:paddingEnd="16dp"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:scaleType="fitCenter"/>
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:scaleType="fitCenter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@android:drawable/sym_def_app_icon" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@+id/itemcheck"
app:layout_constraintStart_toEndOf="@+id/itemicon"
app:layout_constraintTop_toTopOf="parent"
tools:text="Package label" />
<TextView
android:id="@+id/desc"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:ellipsize="end"
android:textColor="?android:attr/textColorTertiary"
android:textSize="12sp"
android:breakStrategy="high_quality"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/title"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="com.package.name (10000)" />
<Switch
android:id="@+id/itemcheck"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:clickable="false"
android:ellipsize="end"
android:focusable="false"
android:focusableInTouchMode="false"
android:gravity="center_vertical"
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp"/>
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
......@@ -50,7 +50,7 @@ mavenPublish {
dependencies {
api 'androidx.core:core-ktx:1.0.1'
api 'com.google.android.material:material:1.1.0-alpha03'
api 'com.google.android.material:material:1.1.0-alpha04'
api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion"
......
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="proxy_cat">"Propiedades del Servidor"</string>
<string name="yes">"Sí"</string>
<string name="no">"No"</string>
<string name="apply">"Aplicar"</string>
</resources>
\ No newline at end of file
#!/usr/bin/env python3
"""
Copyright (C) 2019 by Max Lv <max.c.lv@gmail.com>
Copyright (C) 2019 by Mygod Studio <contact-shadowsocks-android@mygod.be>
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 <http://www.gnu.org/licenses/>.
Usage: ./translate.py <API token>
Prerequisite: pip install poeditor
See also: https://poeditor.com/docs/languages
"""
import itertools
import multiprocessing
import os
import sys
import threading
from multiprocessing.pool import ThreadPool
from poeditor import POEditorAPI
def main(api_token, project_id, languages, tags, file_path):
client = POEditorAPI(api_token)
# See also: https://github.com/sporteasy/python-poeditor/pull/15
client.FILTER_BY += 'proofread'
done = 0
lock = threading.Lock()
def export_worker(params):
((language_code, language_id), (tag, module)) = params
output = file_path.format(language_id, module)
try:
os.makedirs(os.path.dirname(output))
except FileExistsError:
pass
client.export(project_id, language_code, 'android_strings', 'proofread', tag, output)
with lock:
nonlocal done
done += 1
print("{}/{}: {}".format(done, len(languages) * len(tags), output))
pool = ThreadPool(max(64, multiprocessing.cpu_count()))
pool.map(export_worker, itertools.product(languages.items(), tags.items()))
if __name__ == "__main__":
sys.exit(main(
api_token=sys.argv[1],
project_id='89595',
languages={
# Chinese (simplified)
'zh-CN': 'zh-rCN',
# Chinese (traditional)
'zh-TW': 'zh-rTW',
# French
'fr': 'fr',
# Japanese
'ja': 'ja',
# Korean
'ko': 'ko',
# Persian
'fa': 'fa',
# Russian
'ru': 'ru',
# Spanish
'es': 'es',
# Turkish
'tr': 'tr',
},
tags={
'mobile': 'core',
'plugin': 'plugin',
},
file_path='{1}/src/main/res/values-{0}/strings.xml',
))
......@@ -25,7 +25,7 @@ android {
targetSdkVersion rootProject.sdkVersion
versionCode rootProject.versionCode
versionName rootProject.versionName
resConfigs "fa", "fr", "ja", "ko", "ru", "tr", "zh-rCN", "zh-rTW"
resConfigs rootProject.resConfigs
}
buildTypes {
debug {
......
......@@ -17,7 +17,8 @@
tools:replace="theme">
<activity
android:name=".MainActivity"
android:label="@string/app_name">
android:label="@string/app_name"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment