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. ...@@ -48,7 +48,7 @@ Put an `x` inside the [ ] that applies.
* [ ] Apps VPN mode * [ ] Apps VPN mode
* [ ] Bypass mode * [ ] Bypass mode
* Remote DNS: 8.8.8.8 * Remote DNS: 8.8.8.8
* [ ] DNS Forwarding * [ ] DNS over UDP
* Plugin configuration (if applicable): * Plugin configuration (if applicable):
* [ ] Auto Connect * [ ] Auto Connect
* [ ] TCP Fast Open * [ ] TCP Fast Open
......
...@@ -12,8 +12,9 @@ buildscript { ...@@ -12,8 +12,9 @@ buildscript {
junitVersion = '4.12' junitVersion = '4.12'
androidTestVersion = '1.1.1' androidTestVersion = '1.1.1'
androidEspressoVersion = '3.1.1' androidEspressoVersion = '3.1.1'
versionCode = 4070150 versionCode = 4070350
versionName = '4.7.1-nightly' versionName = '4.7.3-nightly'
resConfigs = ['es', 'fa', 'fr', 'ja', 'ko', 'ru', 'tr', 'zh-rCN', 'zh-rTW']
} }
repositories { repositories {
...@@ -25,10 +26,10 @@ buildscript { ...@@ -25,10 +26,10 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.8.0' classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.android.tools.build:gradle:3.3.1' classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.20.0'
classpath 'com.google.gms:google-services:4.2.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 'io.fabric.tools:gradle:1.27.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
} }
......
...@@ -45,7 +45,7 @@ androidExtensions { ...@@ -45,7 +45,7 @@ androidExtensions {
def lifecycleVersion = '2.0.0' def lifecycleVersion = '2.0.0'
def roomVersion = '2.0.0' def roomVersion = '2.0.0'
def workVersion = '1.0.0-rc02' def workVersion = '1.0.0'
dependencies { dependencies {
api project(':plugin') api project(':plugin')
api "android.arch.work:work-runtime-ktx:$workVersion" api "android.arch.work:work-runtime-ktx:$workVersion"
......
...@@ -106,8 +106,8 @@ object BaseService { ...@@ -106,8 +106,8 @@ object BaseService {
private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier private val bandwidthListeners = mutableMapOf<IBinder, Long>() // the binder is the real identifier
private val handler = Handler() private val handler = Handler()
override fun getState(): Int = data!!.state.ordinal override fun getState(): Int = (data?.state ?: State.Idle).ordinal
override fun getProfileName(): String = data!!.proxy?.profile?.name ?: "Idle" override fun getProfileName(): String = data?.proxy?.profile?.name ?: "Idle"
override fun registerCallback(cb: IShadowsocksServiceCallback) { override fun registerCallback(cb: IShadowsocksServiceCallback) {
callbacks.register(cb) callbacks.register(cb)
...@@ -117,6 +117,7 @@ object BaseService { ...@@ -117,6 +117,7 @@ object BaseService {
repeat(callbacks.beginBroadcast()) { repeat(callbacks.beginBroadcast()) {
try { try {
work(callbacks.getBroadcastItem(it)) work(callbacks.getBroadcastItem(it))
} catch (_: DeadObjectException) {
} catch (e: Exception) { } catch (e: Exception) {
printLog(e) printLog(e)
} }
...@@ -133,7 +134,7 @@ object BaseService { ...@@ -133,7 +134,7 @@ object BaseService {
.map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) } .map { Pair(it.profile.id, it.trafficMonitor?.requestUpdate()) }
.filter { it.second != null } .filter { it.second != null }
.map { Triple(it.first, it.second!!.first, it.second!!.second) } .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 } val sum = stats.fold(TrafficStats()) { a, b -> a + b.second }
broadcast { item -> broadcast { item ->
if (bandwidthListeners.contains(item.asBinder())) { if (bandwidthListeners.contains(item.asBinder())) {
...@@ -149,16 +150,17 @@ object BaseService { ...@@ -149,16 +150,17 @@ object BaseService {
val wasEmpty = bandwidthListeners.isEmpty() val wasEmpty = bandwidthListeners.isEmpty()
if (bandwidthListeners.put(cb.asBinder(), timeout) == null) { if (bandwidthListeners.put(cb.asBinder(), timeout) == null) {
if (wasEmpty) registerTimeout() if (wasEmpty) registerTimeout()
if (data!!.state != State.Connected) return if (data?.state != State.Connected) return
var sum = TrafficStats() var sum = TrafficStats()
val proxy = data!!.proxy ?: return val data = data
val proxy = data?.proxy ?: return
proxy.trafficMonitor?.out.also { stats -> proxy.trafficMonitor?.out.also { stats ->
cb.trafficUpdated(proxy.profile.id, if (stats == null) sum else { cb.trafficUpdated(proxy.profile.id, if (stats == null) sum else {
sum += stats sum += stats
stats stats
}) })
} }
data!!.udpFallback?.also { udpFallback -> data.udpFallback?.also { udpFallback ->
udpFallback.trafficMonitor?.out.also { stats -> udpFallback.trafficMonitor?.out.also { stats ->
cb.trafficUpdated(udpFallback.profile.id, if (stats == null) TrafficStats() else { cb.trafficUpdated(udpFallback.profile.id, if (stats == null) TrafficStats() else {
sum += stats sum += stats
...@@ -232,7 +234,7 @@ object BaseService { ...@@ -232,7 +234,7 @@ object BaseService {
File(Core.deviceStorage.noBackupFilesDir, "stat_main"), File(Core.deviceStorage.noBackupFilesDir, "stat_main"),
File(configRoot, CONFIG_FILE), File(configRoot, CONFIG_FILE),
if (udpFallback == null) "-u" else null) if (udpFallback == null) "-u" else null)
check(udpFallback?.pluginPath == null) check(udpFallback?.pluginPath == null) { "UDP fallback cannot have plugins" }
udpFallback?.start(this, udpFallback?.start(this,
File(Core.deviceStorage.noBackupFilesDir, "stat_udp"), File(Core.deviceStorage.noBackupFilesDir, "stat_udp"),
File(configRoot, CONFIG_FILE_UDP), File(configRoot, CONFIG_FILE_UDP),
......
...@@ -27,7 +27,7 @@ import android.text.TextUtils ...@@ -27,7 +27,7 @@ import android.text.TextUtils
import android.util.Log import android.util.Log
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.IOException
object Executable { object Executable {
const val REDSOCKS = "libredsocks.so" const val REDSOCKS = "libredsocks.so"
...@@ -40,7 +40,7 @@ object Executable { ...@@ -40,7 +40,7 @@ object Executable {
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) { for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
val exe = File(try { val exe = File(try {
File(process, "cmdline").inputStream().bufferedReader().readText() File(process, "cmdline").inputStream().bufferedReader().readText()
} catch (_: FileNotFoundException) { } catch (_: IOException) {
continue continue
}.split(Character.MIN_VALUE, limit = 2).first()) }.split(Character.MIN_VALUE, limit = 2).first())
if (EXECUTABLES.contains(exe.name)) try { if (EXECUTABLES.contains(exe.name)) try {
......
...@@ -61,14 +61,12 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -61,14 +61,12 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro
conn.doOutput = true conn.doOutput = true
val proxies = try { val proxies = try {
withTimeoutOrNull(10_000) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
conn.outputStream.bufferedWriter().use { conn.outputStream.bufferedWriter().use {
it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT)) it.write("sig=" + Base64.encodeToString(mdg.digest(), Base64.DEFAULT))
} }
conn.inputStream.bufferedReader().readText() conn.inputStream.bufferedReader().readText()
} }
} ?: throw UnknownHostException()
} finally { } finally {
conn.disconnectFromMain() conn.disconnectFromMain()
}.split('|').toMutableList() }.split('|').toMutableList()
...@@ -85,19 +83,19 @@ class ProxyInstance(val profile: Profile, private val route: String = profile.ro ...@@ -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 // 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 var retries = 0
while (isActive) try { while (true) try {
val io = GlobalScope.async(Dispatchers.IO) { service.resolver(profile.host) } 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) { } catch (e: UnknownHostException) {
// retries are only needed on Chrome OS where arc0 is brought up/down during VPN changes // retries are only needed on Chrome OS where arc0 is brought up/down during VPN changes
if (!DataStore.hasArc0) throw e if (!DataStore.hasArc0) throw e
Thread.yield() Thread.yield()
Crashlytics.log(Log.WARN, "ProxyInstance-resolver", "Retry resolving attempt #${++retries}") 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") { ...@@ -55,7 +55,7 @@ class ChannelMonitor : Thread("ChannelMonitor") {
pendingRegistrations.poll()!!.apply { pendingRegistrations.poll()!!.apply {
try { try {
result.complete(registerInternal(channel, ops, listener)) result.complete(registerInternal(channel, ops, listener))
} catch (e: ClosedChannelException) { } catch (e: Exception) {
result.completeExceptionally(e) result.completeExceptionally(e)
} }
} }
...@@ -100,7 +100,7 @@ class ChannelMonitor : Thread("ChannelMonitor") { ...@@ -100,7 +100,7 @@ class ChannelMonitor : Thread("ChannelMonitor") {
while (running) { while (running) {
val num = try { val num = try {
selector.select() selector.select()
} catch (e: IOException) { } catch (e: Exception) {
printLog(e) printLog(e)
continue continue
} }
......
...@@ -81,7 +81,7 @@ class HttpsTest : ViewModel() { ...@@ -81,7 +81,7 @@ class HttpsTest : ViewModel() {
fun testConnection() { fun testConnection() {
cancelTest() cancelTest()
status.value = Status.Testing 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" Acl.CHINALIST -> "www.qualcomm.cn"
else -> "www.google.com" else -> "www.google.com"
}, "/generate_204") }, "/generate_204")
......
...@@ -157,9 +157,12 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd ...@@ -157,9 +157,12 @@ class LocalDnsServer(private val localResolver: suspend (String) -> Array<InetAd
channel.configureBlocking(false) channel.configureBlocking(false)
monitor.wait(channel, SelectionKey.OP_WRITE) monitor.wait(channel, SelectionKey.OP_WRITE)
check(channel.send(remoteDns.udpWrap(packet), proxy) > 0) check(channel.send(remoteDns.udpWrap(packet), proxy) > 0)
monitor.wait(channel, SelectionKey.OP_READ)
val result = remoteDns.udpReceiveBuffer(UDP_PACKET_SIZE) 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() result.flip()
remoteDns.udpUnwrap(result) remoteDns.udpUnwrap(result)
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 @@ ...@@ -20,11 +20,17 @@
<string name="proxied_apps">"حالت "</string> <string name="proxied_apps">"حالت "</string>
<string name="proxied_apps_summary">"تنظیم حالت وی‌پی‌ان برای اپلیکیشن‌های انتخاب شده"</string> <string name="proxied_apps_summary">"تنظیم حالت وی‌پی‌ان برای اپلیکیشن‌های انتخاب شده"</string>
<string name="on">"روشن"</string> <string name="on">"روشن"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"حالت صرف‌نظر"</string> <string name="bypass_apps">"حالت صرف‌نظر"</string>
<string name="bypass_apps_summary">"این اپلیکیشن‌ها از وی‌پی‌ان استفاده نکنند"</string> <string name="bypass_apps_summary">"این اپلیکیشن‌ها از وی‌پی‌ان استفاده نکنند"</string>
<string name="auto_connect">"وصل‌شدن اتوماتیک"</string> <string name="auto_connect">"وصل‌شدن اتوماتیک"</string>
<string name="auto_connect_summary">"فعال‌شدن شدوساکس لحظه روشن‌شدن گوشی"</string> <string name="auto_connect_summary">"فعال‌شدن شدوساکس لحظه روشن‌شدن گوشی"</string>
<!-- Fuzzy -->
<string name="udp_dns">"ارسال دی‌ان‌اس"</string> <string name="udp_dns">"ارسال دی‌ان‌اس"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"ارسال کلیه درخواست‌های دی‌ان‌اس به سرور "</string> <string name="udp_dns_summary">"ارسال کلیه درخواست‌های دی‌ان‌اس به سرور "</string>
<!-- notification category --> <!-- notification category -->
...@@ -100,6 +106,8 @@ ...@@ -100,6 +106,8 @@
<!-- misc --> <!-- misc -->
<string name="advanced">"پیشرفته"</string> <string name="advanced">"پیشرفته"</string>
<!-- misc -->
<string name="service_mode">"وضعیت سرویس"</string> <string name="service_mode">"وضعیت سرویس"</string>
<string name="service_mode_proxy">"فقط پروکسی"</string> <string name="service_mode_proxy">"فقط پروکسی"</string>
<string name="service_mode_vpn">"وی‌پی‌ان"</string> <string name="service_mode_vpn">"وی‌پی‌ان"</string>
......
...@@ -25,6 +25,8 @@ Reçu : \t\t\t%4$s\t↓\t%2$s"</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">"Mode VPN Apps"</string>
<string name="proxied_apps_summary">"Configurer le mode VPN pour les applications sélectionnées"</string> <string name="proxied_apps_summary">"Configurer le mode VPN pour les applications sélectionnées"</string>
<string name="on">"Activé"</string> <string name="on">"Activé"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"Mode exclusion"</string> <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="bypass_apps_summary">"Activer cette option pour exclure les applications sélectionnées"</string>
<string name="auto_connect">"Connection auto"</string> <string name="auto_connect">"Connection auto"</string>
...@@ -33,6 +35,8 @@ Reçu : \t\t\t%4$s\t↓\t%2$s"</string> ...@@ -33,6 +35,8 @@ Reçu : \t\t\t%4$s\t↓\t%2$s"</string>
<!-- Fuzzy --> <!-- Fuzzy -->
<string name="tcp_fastopen_summary">"Activer nécessite la permission ROOT"</string> <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> <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> <string name="udp_dns_summary">"Faire suivre toutes les requêtes DNS au serveur distant"</string>
<!-- notification category --> <!-- notification category -->
...@@ -116,6 +120,8 @@ Reçu : \t\t\t%4$s\t↓\t%2$s"</string> ...@@ -116,6 +120,8 @@ Reçu : \t\t\t%4$s\t↓\t%2$s"</string>
<!-- misc --> <!-- misc -->
<string name="advanced">"Avancé"</string> <string name="advanced">"Avancé"</string>
<!-- misc -->
<string name="service_mode">"Mode Service"</string> <string name="service_mode">"Mode Service"</string>
<string name="service_mode_proxy">"Proxy seul"</string> <string name="service_mode_proxy">"Proxy seul"</string>
<string name="port_proxy">"Port proxy SOCKS5"</string> <string name="port_proxy">"Port proxy SOCKS5"</string>
......
...@@ -24,6 +24,8 @@ ...@@ -24,6 +24,8 @@
<string name="proxied_apps">"アプリ別のVPNモードを使用"</string> <string name="proxied_apps">"アプリ別のVPNモードを使用"</string>
<string name="proxied_apps_summary">"アプリ別のVPNモードを設定"</string> <string name="proxied_apps_summary">"アプリ別のVPNモードを設定"</string>
<string name="on">"ON"</string> <string name="on">"ON"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"バイパスモード(迂回モード)"</string> <string name="bypass_apps">"バイパスモード(迂回モード)"</string>
<string name="bypass_apps_summary">"このモードを選択した場合、指定したアプリのパケットはプロキシを経由しない"</string> <string name="bypass_apps_summary">"このモードを選択した場合、指定したアプリのパケットはプロキシを経由しない"</string>
<string name="auto_connect">"自動接続"</string> <string name="auto_connect">"自動接続"</string>
...@@ -32,7 +34,11 @@ ...@@ -32,7 +34,11 @@
<!-- Fuzzy --> <!-- Fuzzy -->
<string name="tcp_fastopen_summary">"使用するには ROOT 権限が必要"</string> <string name="tcp_fastopen_summary">"使用するには ROOT 権限が必要"</string>
<string name="tcp_fastopen_summary_unsupported">"ご利用のカーネルバージョンはサポートしておりません:%s &lt; 3.7.1"</string> <string name="tcp_fastopen_summary_unsupported">"ご利用のカーネルバージョンはサポートしておりません:%s &lt; 3.7.1"</string>
<!-- Fuzzy -->
<string name="udp_dns">"DNS 転送"</string> <string name="udp_dns">"DNS 転送"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"DNS プロトコルでリモードサーバーにパケットを転送"</string> <string name="udp_dns_summary">"DNS プロトコルでリモードサーバーにパケットを転送"</string>
<!-- notification category --> <!-- notification category -->
...@@ -122,6 +128,8 @@ ...@@ -122,6 +128,8 @@
<!-- misc --> <!-- misc -->
<string name="advanced">"アドバンス"</string> <string name="advanced">"アドバンス"</string>
<!-- misc -->
<string name="service_mode">"サービスモード"</string> <string name="service_mode">"サービスモード"</string>
<string name="service_mode_proxy">"プロキシのみ"</string> <string name="service_mode_proxy">"プロキシのみ"</string>
<string name="service_mode_transproxy">"トランスプロキシ"</string> <string name="service_mode_transproxy">"トランスプロキシ"</string>
......
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
<string name="proxied_apps">"원하는 앱만 프락시 적용하기"</string> <string name="proxied_apps">"원하는 앱만 프락시 적용하기"</string>
<string name="proxied_apps_summary">"선택한 앱에만 프락시를 적용합니다"</string> <string name="proxied_apps_summary">"선택한 앱에만 프락시를 적용합니다"</string>
<string name="on">"활성화"</string> <string name="on">"활성화"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"선택된 앱들만 프록시 적용 제외하기"</string> <string name="bypass_apps">"선택된 앱들만 프록시 적용 제외하기"</string>
<string name="bypass_apps_summary">"이 옵션을 활성화하면 선택된 앱들은 프락시를 사용하지 않습니다"</string> <string name="bypass_apps_summary">"이 옵션을 활성화하면 선택된 앱들은 프락시를 사용하지 않습니다"</string>
<string name="auto_connect">"자동 접속"</string> <string name="auto_connect">"자동 접속"</string>
...@@ -34,7 +36,11 @@ ...@@ -34,7 +36,11 @@
<!-- Fuzzy --> <!-- Fuzzy -->
<string name="tcp_fastopen_summary">"루트 권한이 필요합니다"</string> <string name="tcp_fastopen_summary">"루트 권한이 필요합니다"</string>
<string name="tcp_fastopen_summary_unsupported">"지원하지 않는 버전의 커널입니다: %s &lt; 3.7.1"</string> <string name="tcp_fastopen_summary_unsupported">"지원하지 않는 버전의 커널입니다: %s &lt; 3.7.1"</string>
<!-- Fuzzy -->
<string name="udp_dns">"DNS 포워딩"</string> <string name="udp_dns">"DNS 포워딩"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"모든 DNS 요청을 외부로 포워딩 합니다"</string> <string name="udp_dns_summary">"모든 DNS 요청을 외부로 포워딩 합니다"</string>
<!-- notification category --> <!-- notification category -->
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
<string name="stat_summary">"Отправлено: \t%3$s\t↑\t%1$s <string name="stat_summary">"Отправлено: \t%3$s\t↑\t%1$s
Получено: \t\t\t%4$s\t↓\t%2$s"</string> Получено: \t\t\t%4$s\t↓\t%2$s"</string>
<string name="connection_test_testing">"Проверка…"</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_error">"Сбой проверки интернет-соединения: %s"</string>
<string name="connection_test_fail">"Интернет недоступен"</string> <string name="connection_test_fail">"Интернет недоступен"</string>
<string name="connection_test_error_status_code">"Код ошибки: #%d"</string> <string name="connection_test_error_status_code">"Код ошибки: #%d"</string>
...@@ -24,15 +25,19 @@ ...@@ -24,15 +25,19 @@
<string name="proxied_apps">"Режим VPN для выбранных приложений"</string> <string name="proxied_apps">"Режим VPN для выбранных приложений"</string>
<string name="proxied_apps_summary">"Выбрать приложения, для которых нужно использовать режим VPN"</string> <string name="proxied_apps_summary">"Выбрать приложения, для которых нужно использовать режим VPN"</string>
<string name="on">"Вкл"</string> <string name="on">"Вкл"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"В обход прокси"</string> <string name="bypass_apps">"В обход прокси"</string>
<string name="bypass_apps_summary">"Включите эту опцию для работы выбранных приложений в обход прокси"</string> <string name="bypass_apps_summary">"Включите эту опцию для работы выбранных приложений в обход прокси"</string>
<string name="auto_connect">"Авто-подключение"</string> <string name="auto_connect">"Авто-подключение"</string>
<string name="auto_connect_summary">"Запускать Shadowsocks при включении"</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 --> <!-- 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> <string name="udp_dns">"Перенаправление DNS"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"Перенаправлять все DNS-запросы на удалённый сервер"</string> <string name="udp_dns_summary">"Перенаправлять все DNS-запросы на удалённый сервер"</string>
<!-- notification category --> <!-- notification category -->
...@@ -125,6 +130,8 @@ ...@@ -125,6 +130,8 @@
<!-- misc --> <!-- misc -->
<string name="advanced">"Дополнительно"</string> <string name="advanced">"Дополнительно"</string>
<!-- misc -->
<string name="service_mode">"Режим"</string> <string name="service_mode">"Режим"</string>
<string name="service_mode_proxy">"Только прокси"</string> <string name="service_mode_proxy">"Только прокси"</string>
<string name="service_mode_transproxy">"Прозрачный прокси"</string> <string name="service_mode_transproxy">"Прозрачный прокси"</string>
......
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
<string name="proxied_apps">"Uygulamalar VPN modu"</string> <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="proxied_apps_summary">"Seçili uygulamalar için VPN'i yapılandır"</string>
<string name="on">"Açık"</string> <string name="on">"Açık"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"Bypass Modu"</string> <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="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> <string name="auto_connect">"Otomatik Bağlan"</string>
...@@ -29,7 +31,11 @@ ...@@ -29,7 +31,11 @@
<!-- Fuzzy --> <!-- Fuzzy -->
<string name="tcp_fastopen_summary">"Açıp kapamak için ROOT yetkisi gerekiyor"</string> <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> <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> <string name="udp_dns">"DNS yönlendirme"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"Tüm DNS isteklerini remote'a yönlendir"</string> <string name="udp_dns_summary">"Tüm DNS isteklerini remote'a yönlendir"</string>
<!-- notification category --> <!-- notification category -->
...@@ -118,6 +124,8 @@ ...@@ -118,6 +124,8 @@
<!-- misc --> <!-- misc -->
<string name="advanced">"Gelişmiş"</string> <string name="advanced">"Gelişmiş"</string>
<!-- misc -->
<string name="service_mode">"Servis modu"</string> <string name="service_mode">"Servis modu"</string>
<string name="service_mode_proxy">"Sadece proxy"</string> <string name="service_mode_proxy">"Sadece proxy"</string>
<string name="port_proxy">"SOCKS5 proxy portu"</string> <string name="port_proxy">"SOCKS5 proxy portu"</string>
......
...@@ -25,14 +25,14 @@ ...@@ -25,14 +25,14 @@
<string name="proxied_apps">"分应用 VPN"</string> <string name="proxied_apps">"分应用 VPN"</string>
<string name="proxied_apps_summary">"允许部分应用绕过 VPN"</string> <string name="proxied_apps_summary">"允许部分应用绕过 VPN"</string>
<string name="on">"启用"</string> <string name="on">"启用"</string>
<string name="bypass_apps">"绕行模式"</string> <string name="bypass_apps">"绕行"</string>
<string name="bypass_apps_summary">"绕过选择的应用"</string> <string name="bypass_apps_summary">"绕过选择的应用"</string>
<string name="auto_connect">"自动连接"</string> <string name="auto_connect">"自动连接"</string>
<string name="auto_connect_summary">"允许 Shadowsocks 随系统启动"</string> <string name="auto_connect_summary">"允许 Shadowsocks 随系统启动"</string>
<string name="tcp_fastopen_summary">"切换可能需要 ROOT 权限"</string> <string name="tcp_fastopen_summary">"切换可能需要 ROOT 权限"</string>
<string name="tcp_fastopen_summary_unsupported">"不支持的内核版本: %s &lt; 3.7.1"</string> <string name="tcp_fastopen_summary_unsupported">"不支持的内核版本: %s &lt; 3.7.1"</string>
<string name="udp_dns">"DNS 转发"</string> <string name="udp_dns">"使用 UDP DNS"</string>
<string name="udp_dns_summary">"转发所有 DNS 请求到远程服务器"</string> <string name="udp_dns_summary">"需要远程服务器支持 UDP 转发"</string>
<!-- notification category --> <!-- notification category -->
<string name="forward_success">"后台服务已开始运行。"</string> <string name="forward_success">"后台服务已开始运行。"</string>
...@@ -121,6 +121,8 @@ ...@@ -121,6 +121,8 @@
<!-- misc --> <!-- misc -->
<string name="advanced">"高级选项"</string> <string name="advanced">"高级选项"</string>
<!-- misc -->
<string name="service_mode">"服务模式"</string> <string name="service_mode">"服务模式"</string>
<string name="service_mode_proxy">"仅代理"</string> <string name="service_mode_proxy">"仅代理"</string>
<string name="service_mode_transproxy">"透明代理"</string> <string name="service_mode_transproxy">"透明代理"</string>
...@@ -147,4 +149,9 @@ ...@@ -147,4 +149,9 @@
<string name="share_over_lan">"通过局域网分享"</string> <string name="share_over_lan">"通过局域网分享"</string>
<string name="connection_test_pending">"检查连接"</string> <string name="connection_test_pending">"检查连接"</string>
<string name="file_manager_missing">"请安装文件管理器,如 MiXplorer"</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> </resources>
\ No newline at end of file
...@@ -26,6 +26,8 @@ ...@@ -26,6 +26,8 @@
<string name="proxied_apps">"個別應用程式的 VPN"</string> <string name="proxied_apps">"個別應用程式的 VPN"</string>
<string name="proxied_apps_summary">"為已選擇的應用程式設定 VPN"</string> <string name="proxied_apps_summary">"為已選擇的應用程式設定 VPN"</string>
<string name="on">"開"</string> <string name="on">"開"</string>
<!-- Fuzzy -->
<string name="bypass_apps">"略過模式"</string> <string name="bypass_apps">"略過模式"</string>
<string name="bypass_apps_summary">"啟用此選項,會略過已選擇的應用程式"</string> <string name="bypass_apps_summary">"啟用此選項,會略過已選擇的應用程式"</string>
<string name="auto_connect">"自動連線"</string> <string name="auto_connect">"自動連線"</string>
...@@ -34,7 +36,11 @@ ...@@ -34,7 +36,11 @@
<!-- Fuzzy --> <!-- Fuzzy -->
<string name="tcp_fastopen_summary">"切換需要 ROOT 權限"</string> <string name="tcp_fastopen_summary">"切換需要 ROOT 權限"</string>
<string name="tcp_fastopen_summary_unsupported">"不支援的核心版本:%s &lt; 3.7.1"</string> <string name="tcp_fastopen_summary_unsupported">"不支援的核心版本:%s &lt; 3.7.1"</string>
<!-- Fuzzy -->
<string name="udp_dns">"DNS 轉送"</string> <string name="udp_dns">"DNS 轉送"</string>
<!-- Fuzzy -->
<string name="udp_dns_summary">"使用 UDP 向遠端轉送 DNS 封包"</string> <string name="udp_dns_summary">"使用 UDP 向遠端轉送 DNS 封包"</string>
<!-- notification category --> <!-- notification category -->
...@@ -123,6 +129,8 @@ ...@@ -123,6 +129,8 @@
<!-- misc --> <!-- misc -->
<string name="advanced">"高級"</string> <string name="advanced">"高級"</string>
<!-- misc -->
<string name="service_mode">"服務模式"</string> <string name="service_mode">"服務模式"</string>
<string name="service_mode_proxy">"仅代理"</string> <string name="service_mode_proxy">"仅代理"</string>
<string name="service_mode_transproxy">"透明代理"</string> <string name="service_mode_transproxy">"透明代理"</string>
......
...@@ -46,7 +46,9 @@ ...@@ -46,7 +46,9 @@
<string name="proxied_apps">Apps VPN mode</string> <string name="proxied_apps">Apps VPN mode</string>
<string name="proxied_apps_summary">Configure VPN mode for selected apps</string> <string name="proxied_apps_summary">Configure VPN mode for selected apps</string>
<string name="on">On</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="bypass_apps_summary">Enable this option to bypass selected apps</string>
<string name="auto_connect">Auto Connect</string> <string name="auto_connect">Auto Connect</string>
<string name="auto_connect_summary">Enable Shadowsocks on startup</string> <string name="auto_connect_summary">Enable Shadowsocks on startup</string>
......
...@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME ...@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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 { ...@@ -26,7 +26,7 @@ android {
versionCode rootProject.versionCode versionCode rootProject.versionCode
versionName rootProject.versionName versionName rootProject.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "fa", "fr", "ja", "ko", "ru", "tr", "zh-rCN", "zh-rTW" resConfigs rootProject.resConfigs
} }
buildTypes { buildTypes {
debug { debug {
...@@ -57,8 +57,9 @@ androidExtensions { ...@@ -57,8 +57,9 @@ androidExtensions {
dependencies { dependencies {
implementation project(':core') implementation project(':core')
implementation 'androidx.browser:browser:1.0.0' 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.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.takisoft.preferencex:preferencex-simplemenu:1.0.0'
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4' implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.4'
implementation 'net.glxn.qrgen:android:2.0' implementation 'net.glxn.qrgen:android:2.0'
......
...@@ -32,13 +32,15 @@ import android.content.pm.PackageInfo ...@@ -32,13 +32,15 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.util.SparseBooleanArray
import android.view.* import android.view.*
import android.widget.ImageView import android.widget.Filter
import android.widget.Switch import android.widget.Filterable
import android.widget.SearchView
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.util.set
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
...@@ -48,16 +50,19 @@ import com.github.shadowsocks.preference.DataStore ...@@ -48,16 +50,19 @@ import com.github.shadowsocks.preference.DataStore
import com.github.shadowsocks.utils.DirectBoot import com.github.shadowsocks.utils.DirectBoot
import com.github.shadowsocks.utils.Key import com.github.shadowsocks.utils.Key
import com.google.android.material.snackbar.Snackbar 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.* import kotlinx.coroutines.*
class AppManager : AppCompatActivity() { class AppManager : AppCompatActivity() {
companion object { companion object {
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private var instance: AppManager? = null private var instance: AppManager? = null
private const val SWITCH = "switch"
private var receiver: BroadcastReceiver? = null private var receiver: BroadcastReceiver? = null
private var cachedApps: List<PackageInfo>? = null private var cachedApps: Map<String, PackageInfo>? = null
private suspend fun getApps(pm: PackageManager) = synchronized(AppManager) { private fun getCachedApps(pm: PackageManager) = synchronized(AppManager) {
if (receiver == null) receiver = Core.listenForPackageChanges { if (receiver == null) receiver = Core.listenForPackageChanges {
synchronized(AppManager) { synchronized(AppManager) {
receiver = null receiver = null
...@@ -67,13 +72,16 @@ class AppManager : AppCompatActivity() { ...@@ -67,13 +72,16 @@ class AppManager : AppCompatActivity() {
} }
// Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached. // Labels and icons can change on configuration (locale, etc.) changes, therefore they are not cached.
val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS) val cachedApps = cachedApps ?: pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
.filter { it.packageName != app.packageName && .filter {
it.requestedPermissions?.contains(Manifest.permission.INTERNET) ?: false } when (it.packageName) {
app.packageName -> false
"android" -> true
else -> it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
}
}
.associateBy { it.packageName }
this.cachedApps = cachedApps this.cachedApps = cachedApps
cachedApps cachedApps
}.map {
yield()
ProxiedApp(pm, it.applicationInfo, it.packageName)
} }
} }
...@@ -81,59 +89,86 @@ class AppManager : AppCompatActivity() { ...@@ -81,59 +89,86 @@ class AppManager : AppCompatActivity() {
val packageName: String) { val packageName: String) {
val name: CharSequence = appInfo.loadLabel(pm) // cached for sorting val name: CharSequence = appInfo.loadLabel(pm) // cached for sorting
val icon: Drawable get() = appInfo.loadIcon(pm) val icon: Drawable get() = appInfo.loadIcon(pm)
val uid get() = appInfo.uid
} }
private inner class AppViewHolder(view: View) : RecyclerView.ViewHolder(view), View.OnClickListener { 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 lateinit var item: ProxiedApp
private val proxied get() = proxiedApps.contains(item.packageName)
init { init {
view.setOnClickListener(this) view.setOnClickListener(this)
} }
fun bind(app: ProxiedApp) { fun bind(app: ProxiedApp) {
this.item = app item = app
icon.setImageDrawable(app.icon) itemView.itemicon.setImageDrawable(app.icon)
check.text = app.name itemView.title.text = app.name
check.isChecked = proxied 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?) { override fun onClick(v: View?) {
if (proxied) { if (isProxiedApp(item)) proxiedUids.delete(item.uid) else proxiedUids[item.uid] = true
proxiedApps.remove(item.packageName) DataStore.individual = apps.filter { isProxiedApp(it) }.joinToString("\n") { it.packageName }
check.isChecked = false
} else {
proxiedApps.add(item.packageName)
check.isChecked = true
}
DataStore.individual = proxiedApps.joinToString("\n")
DataStore.dirty = true DataStore.dirty = true
appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, AppManager.SWITCH)
} }
} }
private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>() { private inner class AppsAdapter : RecyclerView.Adapter<AppViewHolder>(), Filterable {
private var apps = listOf<ProxiedApp>() private var filteredApps = apps
suspend fun reload() { suspend fun reload() {
apps = getApps(packageManager) apps = getCachedApps(packageManager).map { (packageName, packageInfo) ->
.sortedWith(compareBy({ !proxiedApps.contains(it.packageName) }, { it.name.toString() })) 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 = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder =
AppViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_apps_item, parent, false)) 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> override fun publishResults(constraint: CharSequence, results: FilterResults) {
private lateinit var toolbar: Toolbar @Suppress("UNCHECKED_CAST")
private lateinit var bypassSwitch: Switch filteredApps = results.values as List<ProxiedApp>
private lateinit var appListView: RecyclerView notifyDataSetChanged()
private lateinit var loadingView: View }
}
override fun getFilter(): Filter = filterImpl
}
private val proxiedUids = SparseBooleanArray()
private val clipboard by lazy { getSystemService<ClipboardManager>()!! } private val clipboard by lazy { getSystemService<ClipboardManager>()!! }
private var loader: Job? = null 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 val shortAnimTime by lazy { resources.getInteger(android.R.integer.config_shortAnimTime).toLong() }
private fun View.crossFadeFrom(other: View) { private fun View.crossFadeFrom(other: View) {
...@@ -150,25 +185,29 @@ class AppManager : AppCompatActivity() { ...@@ -150,25 +185,29 @@ class AppManager : AppCompatActivity() {
}).duration = shortAnimTime }).duration = shortAnimTime
} }
private fun initProxiedApps(str: String = DataStore.individual) { private fun initProxiedUids(str: String = DataStore.individual) {
proxiedApps = str.split('\n').toHashSet() 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 @UiThread
private fun loadApps() { private fun loadApps() {
loader?.cancel() loader?.cancel()
loader = GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) { loader = GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) {
loadingView.crossFadeFrom(appListView) loading.crossFadeFrom(list)
val adapter = appListView.adapter as AppsAdapter val adapter = list.adapter as AppsAdapter
withContext(Dispatchers.IO) { adapter.reload() } withContext(Dispatchers.IO) { adapter.reload() }
adapter.notifyDataSetChanged() adapter.filter.filter(search.query)
appListView.crossFadeFrom(loadingView) list.crossFadeFrom(loading)
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.layout_apps) setContentView(R.layout.layout_apps)
toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true) supportActionBar!!.setDisplayHomeAsUpEnabled(true)
...@@ -176,25 +215,29 @@ class AppManager : AppCompatActivity() { ...@@ -176,25 +215,29 @@ class AppManager : AppCompatActivity() {
DataStore.proxyApps = true DataStore.proxyApps = true
DataStore.dirty = 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 DataStore.dirty = true
when (checkedId) {
R.id.btn_off -> {
DataStore.proxyApps = false
finish() finish()
} }
R.id.btn_on -> DataStore.bypass = false
bypassSwitch = findViewById(R.id.bypassSwitch) R.id.btn_bypass -> DataStore.bypass = true
bypassSwitch.isChecked = DataStore.bypass
bypassSwitch.setOnCheckedChangeListener { _, checked ->
DataStore.bypass = checked
DataStore.dirty = true
} }
}
initProxiedUids()
list.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
list.itemAnimator = DefaultItemAnimator()
list.adapter = appsAdapter
initProxiedApps() search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
loadingView = findViewById(R.id.loading) override fun onQueryTextSubmit(query: String?) = false
appListView = findViewById(R.id.list) override fun onQueryTextChange(newText: String?) = true.also { appsAdapter.filter.filter(newText) }
appListView.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false) })
appListView.itemAnimator = DefaultItemAnimator()
appListView.adapter = AppsAdapter()
instance = this instance = this
loadApps() loadApps()
...@@ -215,14 +258,14 @@ class AppManager : AppCompatActivity() { ...@@ -215,14 +258,14 @@ class AppManager : AppCompatActivity() {
ProfileManager.updateProfile(it) ProfileManager.updateProfile(it)
} }
if (DataStore.directBootAware) DirectBoot.update() if (DataStore.directBootAware) DirectBoot.update()
Snackbar.make(appListView, R.string.action_apply_all, Snackbar.LENGTH_LONG).show() Snackbar.make(list, R.string.action_apply_all, Snackbar.LENGTH_LONG).show()
} else Snackbar.make(appListView, R.string.action_export_err, Snackbar.LENGTH_LONG).show() } else Snackbar.make(list, R.string.action_export_err, Snackbar.LENGTH_LONG).show()
return true return true
} }
R.id.action_export_clipboard -> { R.id.action_export_clipboard -> {
clipboard.primaryClip = ClipData.newPlainText(Key.individual, clipboard.primaryClip = ClipData.newPlainText(Key.individual,
"${DataStore.bypass}\n${DataStore.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 return true
} }
R.id.action_import_clipboard -> { R.id.action_import_clipboard -> {
...@@ -232,16 +275,16 @@ class AppManager : AppCompatActivity() { ...@@ -232,16 +275,16 @@ class AppManager : AppCompatActivity() {
try { try {
val (enabled, apps) = if (i < 0) Pair(proxiedAppString, "") else val (enabled, apps) = if (i < 0) Pair(proxiedAppString, "") else
Pair(proxiedAppString.substring(0, i), proxiedAppString.substring(i + 1)) 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.individual = apps
DataStore.dirty = true DataStore.dirty = true
Snackbar.make(appListView, R.string.action_import_msg, Snackbar.LENGTH_LONG).show() Snackbar.make(list, R.string.action_import_msg, Snackbar.LENGTH_LONG).show()
initProxiedApps(apps) initProxiedUids(apps)
loadApps() appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, AppManager.SWITCH)
return true return true
} catch (_: IllegalArgumentException) { } } 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 return false
......
...@@ -108,7 +108,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -108,7 +108,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
// service // service
var state = BaseService.State.Idle 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) { override fun trafficUpdated(profileId: Long, stats: TrafficStats) {
if (profileId == 0L) this@MainActivity.stats.updateTraffic( if (profileId == 0L) this@MainActivity.stats.updateTraffic(
stats.txRate, stats.rxRate, stats.txTotal, stats.rxTotal) stats.txRate, stats.rxRate, stats.txTotal, stats.rxTotal)
...@@ -122,7 +123,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref ...@@ -122,7 +123,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Callback, OnPref
} }
private fun changeState(state: BaseService.State, msg: String? = null, animate: Boolean = false) { 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) stats.changeState(state)
if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show() if (msg != null) snackbar(getString(R.string.vpn_error, msg)).show()
this.state = state this.state = state
......
...@@ -41,7 +41,8 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback { ...@@ -41,7 +41,8 @@ class TileService : BaseTileService(), ShadowsocksConnection.Callback {
private var tapPending = false private var tapPending = false
private val connection = ShadowsocksConnection() 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) { override fun onServiceConnected(service: IShadowsocksService) {
updateTile(BaseService.State.values()[service.state]) { service.profileName } updateTile(BaseService.State.values()[service.state]) { service.profileName }
if (tapPending) { if (tapPending) {
......
...@@ -68,11 +68,13 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute ...@@ -68,11 +68,13 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
return drawableState return drawableState
} }
fun changeState(state: BaseService.State, animate: Boolean) { fun changeState(state: BaseService.State, previousState: BaseService.State, animate: Boolean) {
when (state) { when (state) {
BaseService.State.Connecting -> changeState(iconConnecting, animate) BaseService.State.Connecting -> changeState(iconConnecting, animate)
BaseService.State.Connected -> changeState(iconConnected, 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) else -> changeState(iconStopped, animate)
} }
if (state == BaseService.State.Connected) { if (state == BaseService.State.Connected) {
...@@ -86,13 +88,12 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute ...@@ -86,13 +88,12 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
isEnabled = state.canStop || state == BaseService.State.Stopped 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 == iconStopped && b == iconConnecting ||
a == iconConnecting && b == iconStopped || a == iconConnecting && b == iconStopped ||
a == iconConnected && b == iconStopping || a == iconConnected && b == iconStopping ||
a == iconStopping && b == iconConnected a == iconStopping && b == iconConnected
private fun changeState(icon: AnimatedVectorDrawableCompat, animate: Boolean) {
if (animate) { if (animate) {
if (animationQueue.size < 2 || !counters(animationQueue.last, icon)) { if (animationQueue.size < 2 || !counters(animationQueue.last, icon)) {
animationQueue.add(icon) animationQueue.add(icon)
......
...@@ -7,41 +7,80 @@ ...@@ -7,41 +7,80 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:duplicateParentState="false"> android:duplicateParentState="false">
<include layout="@layout/toolbar_light_dark"/> <androidx.appcompat.widget.Toolbar
<LinearLayout android:layout_width="fill_parent" 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:layout_height="wrap_content"
android:elevation="1dp"
android:background="?android:colorBackground" android:background="?android:colorBackground"
android:orientation="vertical" android:elevation="1dp">
tools:ignore="RtlSymmetry">
<Switch <TextView
android:id="@+id/onSwitch" android:id="@+id/tv_mode"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="true" android:layout_marginStart="16dp"
android:paddingBottom="12dp" android:minWidth="42dp"
android:paddingEnd="16dp" android:text="@string/proxied_apps_mode"
android:paddingStart="72dp" app:layout_constraintBottom_toBottomOf="@+id/bypassGroup"
android:paddingTop="12dp" app:layout_constraintStart_toStartOf="parent"
android:text="@string/on" app:layout_constraintTop_toTopOf="@+id/bypassGroup" />
android:textColor="?android:attr/textColorSecondary"
android:textSize="18sp"/>
<Switch <RadioGroup
android:id="@+id/bypassSwitch" android:id="@+id/bypassGroup"
android:layout_width="match_parent" 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:layout_height="wrap_content"
android:checked="true" android:layout_weight="1"
android:paddingBottom="12dp" android:text="@string/on" />
android:paddingEnd="16dp"
android:paddingStart="72dp" <androidx.appcompat.widget.AppCompatRadioButton
android:paddingTop="12dp" android:id="@+id/btn_bypass"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/bypass_apps" android:text="@string/bypass_apps"
android:textColor="?android:attr/textColorSecondary" tools:checked="true"
android:textSize="18sp" android:tooltipText="@string/bypass_apps_summary" />
android:tooltipText="@string/bypass_apps_summary"/> </RadioGroup>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/snackbar" android:id="@+id/snackbar"
android:layout_width="match_parent" android:layout_width="match_parent"
......
<?xml version="1.0" encoding="utf-8"?> <?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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:focusable="true" android:background="?android:attr/selectableItemBackground"
android:background="?android:attr/selectableItemBackground"> android:focusable="true">
<ImageView <ImageView
android:id="@+id/itemicon" android:id="@+id/itemicon"
android:layout_width="72dp" android:layout_width="40dp"
android:layout_height="56dp" android:layout_height="40dp"
android:paddingBottom="4dp" android:layout_marginStart="16dp"
android:paddingEnd="16dp" android:layout_marginTop="8dp"
android:paddingStart="8dp" android:layout_marginBottom="8dp"
android:paddingTop="4dp" android:scaleType="fitCenter"
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 <Switch
android:id="@+id/itemcheck" android:id="@+id/itemcheck"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="fill_parent" android:layout_height="wrap_content"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_weight="1"
android:clickable="false" android:clickable="false"
android:ellipsize="end"
android:focusable="false" android:focusable="false"
android:focusableInTouchMode="false" android:focusableInTouchMode="false"
android:gravity="center_vertical" app:layout_constraintBottom_toBottomOf="parent"
android:textColor="?android:attr/textColorSecondary" app:layout_constraintEnd_toEndOf="parent"
android:textSize="18sp"/> app:layout_constraintTop_toTopOf="parent" />
</LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>
...@@ -50,7 +50,7 @@ mavenPublish { ...@@ -50,7 +50,7 @@ mavenPublish {
dependencies { dependencies {
api 'androidx.core:core-ktx:1.0.1' 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" api "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test:runner:$androidTestVersion" 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 { ...@@ -25,7 +25,7 @@ android {
targetSdkVersion rootProject.sdkVersion targetSdkVersion rootProject.sdkVersion
versionCode rootProject.versionCode versionCode rootProject.versionCode
versionName rootProject.versionName versionName rootProject.versionName
resConfigs "fa", "fr", "ja", "ko", "ru", "tr", "zh-rCN", "zh-rTW" resConfigs rootProject.resConfigs
} }
buildTypes { buildTypes {
debug { debug {
......
...@@ -17,7 +17,8 @@ ...@@ -17,7 +17,8 @@
tools:replace="theme"> tools:replace="theme">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:label="@string/app_name"> android:label="@string/app_name"
android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN"/> <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