Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0e5681052 | ||
|
|
d10a406584 | ||
|
|
a99a07f586 | ||
|
|
39a0a61da2 | ||
|
|
cf118a1bbf | ||
|
|
510098911e | ||
|
|
201bb4fe58 | ||
|
|
026d1ecb2b | ||
|
|
7461d3bf30 | ||
|
|
4483765b2d | ||
|
|
62375a576c | ||
|
|
4904e9c06c | ||
|
|
f75b54699a | ||
|
|
22ffcc53bb | ||
|
|
b69c20d086 | ||
|
|
c71c6ffe02 | ||
|
|
439d56d1d7 | ||
|
|
81f35acf20 | ||
|
|
a755b669c8 | ||
|
|
9fb180956f | ||
|
|
39530a1a3b | ||
|
|
b91d9474d5 | ||
|
|
f0140c0070 | ||
|
|
d23be09cb0 | ||
|
|
4a97ff1d30 | ||
|
|
88fc092aa2 | ||
|
|
d715f7b4c6 | ||
|
|
7cc3c48240 | ||
|
|
26fb0e5e3c | ||
|
|
f4d0835e00 | ||
|
|
ebcf514323 | ||
|
|
3975f3f43c | ||
|
|
be79e6f80c | ||
|
|
61edb40dd1 | ||
|
|
0046ed87d5 | ||
|
|
4f44d119ee | ||
|
|
1e5aabf824 | ||
|
|
7a49ddc045 | ||
|
|
5ced42125c | ||
|
|
3f88b46353 | ||
|
|
34da03d6e3 | ||
|
|
16c0d27130 | ||
|
|
bff2615599 | ||
|
|
c63a659d88 | ||
|
|
cb3fa2d923 | ||
|
|
daea4b5f6c | ||
|
|
8e7ddaebbd | ||
|
|
aaf9f27d56 | ||
|
|
7898eef48d | ||
|
|
71979434d7 | ||
|
|
ad522572f0 | ||
|
|
d9fa4481a2 | ||
|
|
e2c29594f2 | ||
|
|
2ef042d56b | ||
|
|
0fa97f064f | ||
|
|
d7ecc34836 | ||
|
|
168c0588a6 | ||
|
|
4f5ee68f40 | ||
|
|
0c3352935a | ||
|
|
0d02a39b61 | ||
|
|
090cc35bff | ||
|
|
042ee55421 | ||
|
|
ac96e6fc9c | ||
|
|
c6ff9b3d89 | ||
|
|
77af5f6e3c | ||
|
|
95db29bd13 | ||
|
|
7297a2664c | ||
|
|
195e2afbeb | ||
|
|
3dbc5f4147 | ||
|
|
580ce14809 | ||
|
|
d4213e8ee8 | ||
|
|
be168c77f6 | ||
|
|
f9b29acd82 | ||
|
|
4e943bc782 | ||
|
|
b3a818b03a | ||
|
|
982845e877 | ||
|
|
4e35fe19c8 | ||
|
|
45d8e827c1 | ||
|
|
90dd0ab300 | ||
|
|
dee540b83a | ||
|
|
f3b3507c0f | ||
|
|
402710841b | ||
|
|
6ded9d3d37 | ||
|
|
88274fb732 | ||
|
|
36e6cfbe59 | ||
|
|
f05761ac4d | ||
|
|
a33efea2b5 | ||
|
|
b2e6639901 | ||
|
|
db74988190 | ||
|
|
9a43c78e7b | ||
|
|
e915a4221d | ||
|
|
cd1f94f4a0 | ||
|
|
040a6a2815 | ||
|
|
0b9b0a91c8 | ||
|
|
6dace959d2 |
@@ -3,9 +3,8 @@
|
||||
Cannot connect to server:
|
||||
|
||||
1. Stop battery saver if it's active;
|
||||
2. If an upgrade breaks anything, do a manual reset first by pressing Reset at the end of the list;
|
||||
3. Check your config;
|
||||
4. Wipe app data.
|
||||
2. Check your config;
|
||||
3. Wipe app data.
|
||||
|
||||
Crash: [Submit an issue](https://github.com/shadowsocks/shadowsocks-android/issues/new) with logcat attached, or submit a crash report to Google Play. Then, try wiping app data.
|
||||
|
||||
@@ -13,11 +12,6 @@ Crash: [Submit an issue](https://github.com/shadowsocks/shadowsocks-android/issu
|
||||
|
||||
Use [Tasker](http://tasker.dinglisch.net/) integration.
|
||||
|
||||
### How to add QR code from local gallery?
|
||||
|
||||
Scan it with a third-party scanner like [QuickMark Barcode Scanner](https://play.google.com/store/apps/details?id=tw.com.quickmark) and click the `ss` url.
|
||||
|
||||
|
||||
### Why is NAT mode deprecated?
|
||||
|
||||
1. Requiring ROOT permission;
|
||||
@@ -32,9 +26,10 @@ The exclamation mark in the Wi-Fi/cellular icon appears because the system fails
|
||||
|
||||
1. Some ROM has broken VPNService implementation, especially for IPv6;
|
||||
2. Some ROM has aggressive (or called broken) background service killing policy;
|
||||
3. If you have Xposed framework and/or battery saver apps, it's likely that this app wouldn't work well with these either.
|
||||
3. Some ROM like [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) has **very** broken direct boot support;
|
||||
4. If you have Xposed framework and/or battery saver apps, it's likely that this app wouldn't work well with these either.
|
||||
|
||||
* Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772)
|
||||
* Fixes for MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
|
||||
* Fixes for Huawei: [#1091 (comment)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836)
|
||||
* Related to Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
|
||||
* Samsung and/or Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
### Решение проблем
|
||||
|
||||
Не получается подключиться к серверу:
|
||||
|
||||
1. Выключите режим энергосбережения, если он активен;
|
||||
2. Проверьте настройки;
|
||||
3. Сотрите данные приложения.
|
||||
|
||||
Не работает: [Добавьте отчет](https://github.com/shadowsocks/shadowsocks-android/issues/new) прикрепив лог (logcat), или отправьте отзыв в Google Play. Затем попробуйте стереть данные приложения.
|
||||
|
||||
### Как добавить виджет и/или переключать профиль на основании состояния сети?
|
||||
|
||||
Использйте интеграцию с [Tasker](http://tasker.dinglisch.net/).
|
||||
|
||||
### Как добавить QR код из локальной галереи?
|
||||
|
||||
Отсканируйте его при помощи стороннего сканера, например [QuickMark Barcode Scanner](https://play.google.com/store/apps/details?id=tw.com.quickmark) и нажмите на `ss` ссылку.
|
||||
|
||||
|
||||
### Почему режим NAT более не поддерживается?
|
||||
|
||||
1. Требуются ROOT права;
|
||||
2. Нет поддержки IPv6;
|
||||
3. Нет поддержки UDP relay.
|
||||
|
||||
### Как убрать восклицательный знак когда используется режим VPN?
|
||||
|
||||
Восклицательный знак на значке Wi-Fi/Сотовой связи появляется, потому что система не может подключиться к portal-серверу (по-умолчанию к `clients3.google.com`) без VPN подключения. Чтобы убрать его, следуйте инструкциям в [этом топике](https://www.noisyfox.cn/45.html). (на упрощенном китайском)
|
||||
|
||||
### Почему мой ROM не поддерживается?
|
||||
|
||||
1. Некоторые ROM используют неправильную реализацию службы VPN, особенно IPv6;
|
||||
2. Некоторые ROM используют агрессивную (или так называемую испорченную) фоновую службу с ошибочной политикой остановки;
|
||||
3. Некоторые ROM, такие как [Flyme](https://github.com/shadowsocks/shadowsocks-android/issues/1589) имеют **слишком** испорченную поддержку "непосредственной загрузки";
|
||||
4. Если вы используете фреймворк Xposed и/или приложения для энергосбережения, вероятно наше приложение не будет нормально работать с ними.
|
||||
|
||||
* Иправления для MIUI: [#772](https://github.com/shadowsocks/shadowsocks-android/issues/772) [#888](https://github.com/shadowsocks/shadowsocks-android/issues/888)
|
||||
* Иправления для Huawei: [#1091 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1091#issuecomment-276949836)
|
||||
* Вызванные из-за Xposed: [#1414](https://github.com/shadowsocks/shadowsocks-android/issues/1414)
|
||||
* Samsung и/или Brevent: [#1410](https://github.com/shadowsocks/shadowsocks-android/issues/1410)
|
||||
* Не устанавливайте это приложение на SD карту, из-за проблем с разрешениями: [#1124 (комментарий)](https://github.com/shadowsocks/shadowsocks-android/issues/1124#issuecomment-307556453)
|
||||
|
||||
### Как приостановить службу Shadowsocks?
|
||||
|
||||
* Для Android 7.0+: Используйте переключатель в быстрых настройках;
|
||||
* Используйте интеграцию с Tasker;
|
||||
* Добавьте профиль c выбранными приложениями, которые будут работать через Shadowsocks прокси, выключив обходной режим.
|
||||
|
||||
### Почему Shadowsocks потребляет так много энергии на Android 5.0+?
|
||||
|
||||
Так как Shadowsocks работает со всем траффиком устройства, заряд батареи используемый другими приложениями также считается за тот, что использует Shadowsocks. Таким образом использование батареи Shadowsocks'ом равен сумме всей активности устройства в сети. Shadowsocks сам по себе в общем-то является приложением связанного процесса ввода/вывода на современных Android устройствах, которое, ожидаемо, не потребляет значительного количества энергии.
|
||||
|
||||
Так что если вы заметите значительное увеличение потребляемой энергии после того, как вы использовали Shadowsocks, это скорее всего вызванно другими приложениями. Например, сервисы Google Play могут потреблять больше энергии после того, как подключатся к Google, и т.д.
|
||||
|
||||
Больше информации: https://kb.adguard.com/en/android/solving-problems/battery
|
||||
|
||||
### Оно прекрасно работает при Wi-Fi но не может подключиться через сотовую связь?
|
||||
|
||||
Разрешите этому приложению передачу данных в фоновом режиме в настройках приложения.
|
||||
|
||||
Если у вас оперератор Yota, то измените точку доступа APN с `yota.ru` на `client.yota.ru`
|
||||
|
||||
### Почему требуется разрешение камере на устройствах младше Android 6.0?
|
||||
|
||||
Чтобы сканировать QR код через встроенный QR сканер.
|
||||
|
||||
Кстати, обновите уже свою версию Android.
|
||||
|
||||
### Как использовать режим Transproxy (прозрачный прокси)?
|
||||
|
||||
1. Установите [AFWall+](https://github.com/ukanth/afwall);
|
||||
2. Настройте пользовательский скрипт:
|
||||
```sh
|
||||
IP6TABLES=/system/bin/ip6tables
|
||||
IPTABLES=/system/bin/iptables
|
||||
ULIMIT=/system/bin/ulimit
|
||||
SHADOWSOCKS_UID=`dumpsys package com.github.shadowsocks | grep userId | cut -d= -f2 - | cut -d' ' -f1 -`
|
||||
PORT_DNS=5450
|
||||
PORT_TRANSPROXY=8200
|
||||
$ULIMIT -n 4096
|
||||
$IP6TABLES -F
|
||||
$IP6TABLES -A INPUT -j DROP
|
||||
$IP6TABLES -A OUTPUT -j DROP
|
||||
$IPTABLES -t nat -F OUTPUT
|
||||
$IPTABLES -t nat -A OUTPUT -o lo -j RETURN
|
||||
$IPTABLES -t nat -A OUTPUT -d 127.0.0.1 -j RETURN
|
||||
$IPTABLES -t nat -A OUTPUT -m owner --uid-owner $SHADOWSOCKS_UID -j RETURN
|
||||
$IPTABLES -t nat -A OUTPUT -p tcp --dport 53 -j DNAT --to-destination 127.0.0.1:$PORT_DNS
|
||||
$IPTABLES -t nat -A OUTPUT -p udp --dport 53 -j DNAT --to-destination 127.0.0.1:$PORT_DNS
|
||||
$IPTABLES -t nat -A OUTPUT -p tcp -j DNAT --to-destination 127.0.0.1:$PORT_TRANSPROXY
|
||||
$IPTABLES -t nat -A OUTPUT -p udp -j DNAT --to-destination 127.0.0.1:$PORT_TRANSPROXY
|
||||
```
|
||||
3. Настройте пользовательский скрипт отключения:
|
||||
```sh
|
||||
IP6TABLES=/system/bin/ip6tables
|
||||
IPTABLES=/system/bin/iptables
|
||||
$IPTABLES -t nat -F OUTPUT
|
||||
$IP6TABLES -F
|
||||
```
|
||||
4. Убедитесь, что траффик для Shadowsocks разрешен;
|
||||
5. Запустите службу прозрачного прокси (transproxy) в Shadowsocks и включите брандмауэр.
|
||||
@@ -24,3 +24,6 @@ core/src/overture/src/gopkg.in
|
||||
# work in progress
|
||||
tv/
|
||||
things/
|
||||
|
||||
# release apks
|
||||
*.apk
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
## Shadowsocks for Android
|
||||
|
||||
[](https://travis-ci.org/shadowsocks/shadowsocks-android)
|
||||
[](https://android-arsenal.com/api?level=21)
|
||||
[](https://github.com/shadowsocks/shadowsocks-android/releases)
|
||||
[](https://www.codacy.com/app/shadowsocks/shadowsocks-android?utm_source=github.com&utm_medium=referral&utm_content=shadowsocks/shadowsocks-android&utm_campaign=Badge_Grade)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
|
||||
A [shadowsocks](http://shadowsocks.org) client for Android, written in Kotlin.
|
||||
<a href="https://play.google.com/store/apps/details?id=com.github.shadowsocks"><img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" height="48"></a>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
|
||||
buildscript {
|
||||
ext {
|
||||
kotlinVersion = '1.2.10'
|
||||
minSdkVersion = 19
|
||||
kotlinVersion = '1.2.21'
|
||||
minSdkVersion = 21
|
||||
sdkVersion = 27
|
||||
buildToolsVersion = '27.0.3'
|
||||
supportLibraryVersion = '27.0.2'
|
||||
@@ -21,7 +23,8 @@ buildscript {
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.1'
|
||||
classpath 'com.google.gms:google-services:3.1.1'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
|
||||
classpath 'com.google.gms:google-services:3.2.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,19 +36,28 @@ android {
|
||||
}
|
||||
|
||||
task goBuild(type: Exec) {
|
||||
executable "sh"
|
||||
args "-c", "src/overture/make.bash " + minSdkVersion
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
executable "cmd.exe"
|
||||
args "/c", "src\\overture\\make.bat " + minSdkVersion
|
||||
} else {
|
||||
executable "sh"
|
||||
args "-c", "src/overture/make.bash " + minSdkVersion
|
||||
}
|
||||
}
|
||||
|
||||
task goClean(type: Exec) {
|
||||
executable "sh"
|
||||
args "-c", "src/overture/clean.bash"
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
executable "cmd.exe"
|
||||
args "/c", "src\\overture\\clean.bat"
|
||||
} else {
|
||||
executable "sh"
|
||||
args "-c", "src/overture/clean.bash"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.whenTaskAdded { task ->
|
||||
if ((task.name == 'generateJsonModelDebug' ||
|
||||
task.name == 'generateJsonModelRelease') &&
|
||||
!Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
task.name == 'generateJsonModelRelease')) {
|
||||
task.dependsOn(goBuild)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#!/usr/bin/python
|
||||
# -*- encoding: utf8 -*-
|
||||
|
||||
import itertools
|
||||
import math
|
||||
import sys
|
||||
|
||||
import IPy
|
||||
|
||||
@@ -43,8 +43,6 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.system.ErrnoException;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
public class JniHelper {
|
||||
static {
|
||||
System.loadLibrary("jni-helper");
|
||||
@@ -54,8 +52,7 @@ public class JniHelper {
|
||||
public static void sigtermCompat(@NonNull Process process) throws Exception {
|
||||
if (Build.VERSION.SDK_INT >= 24) throw new UnsupportedOperationException("Never call this method in OpenJDK!");
|
||||
int errno = sigterm(process);
|
||||
if (errno != 0) throw Build.VERSION.SDK_INT >= 21
|
||||
? new ErrnoException("kill", errno) : new Exception("kill failed: " + errno);
|
||||
if (errno != 0) throw new ErrnoException("kill", errno);
|
||||
}
|
||||
|
||||
@Deprecated // only implemented for before API 24
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
APP_ABI := armeabi-v7a arm64-v8a x86
|
||||
APP_PLATFORM := android-19
|
||||
APP_PLATFORM := android-21
|
||||
APP_STL := c++_static
|
||||
NDK_TOOLCHAIN_VERSION := clang
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
# executable program
|
||||
#
|
||||
# Modified by @Mygod, based on:
|
||||
# https://android.googlesource.com/platform/ndk/+/f2e98f8c066aed59caf61163d4b87c2b858f9814/build/core/build-shared-library.mk
|
||||
# https://android.googlesource.com/platform/ndk/+/f2e98f8c066aed59caf61163d4b87c2b858f9814/build/core/build-executable.mk
|
||||
# https://android.googlesource.com/platform/ndk/+/a355a4e/build/core/build-shared-library.mk
|
||||
# https://android.googlesource.com/platform/ndk/+/a355a4e/build/core/build-executable.mk
|
||||
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
||||
LOCAL_MAKEFILE := $(local-makefile)
|
||||
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
@ECHO OFF
|
||||
|
||||
SET DIR=%~dp0
|
||||
SET DEPS=%DIR%\.deps
|
||||
|
||||
RD /S /Q %DEPS%
|
||||
RD /S /Q %DIR%\go\bin
|
||||
RD /S /Q %DIR%\bin
|
||||
|
||||
ECHO "Successfully clean overture"
|
||||
@@ -12,7 +12,7 @@ TARGET=$DIR/bin
|
||||
DEPS=$DIR/.deps
|
||||
|
||||
ANDROID_ARM_TOOLCHAIN=$DEPS/android-toolchain-${MIN_API}-arm
|
||||
ANDROID_ARM64_TOOLCHAIN=$DEPS/android-toolchain-21-arm64
|
||||
ANDROID_ARM64_TOOLCHAIN=$DEPS/android-toolchain-${MIN_API}-arm64
|
||||
ANDROID_X86_TOOLCHAIN=$DEPS/android-toolchain-${MIN_API}-x86
|
||||
|
||||
ANDROID_ARM_CC=$ANDROID_ARM_TOOLCHAIN/bin/arm-linux-androideabi-clang
|
||||
@@ -35,7 +35,7 @@ fi
|
||||
if [ ! -f "$ANDROID_ARM64_CC" ]; then
|
||||
echo "Make standalone toolchain for ARM64 arch"
|
||||
$ANDROID_NDK_HOME/build/tools/make_standalone_toolchain.py --arch arm64 \
|
||||
--api 21 --install-dir $ANDROID_ARM64_TOOLCHAIN
|
||||
--api $MIN_API --install-dir $ANDROID_ARM64_TOOLCHAIN
|
||||
fi
|
||||
|
||||
if [ ! -f "$ANDROID_X86_CC" ]; then
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
@ECHO OFF
|
||||
SETLOCAL
|
||||
|
||||
IF NOT DEFINED ANDROID_NDK_HOME (
|
||||
SET ANDROID_NDK_HOME=%ANDROID_HOME%\ndk-bundle
|
||||
)
|
||||
|
||||
SET DIR=%~dp0
|
||||
SET MIN_API=%1%
|
||||
SET TARGET=%DIR%\bin
|
||||
SET DEPS=%DIR%\.deps
|
||||
|
||||
SET ANDROID_ARM_TOOLCHAIN=%DEPS%\android-toolchain-%MIN_API%-arm
|
||||
SET ANDROID_ARM64_TOOLCHAIN=%DEPS%\android-toolchain-%MIN_API%-arm64
|
||||
SET ANDROID_X86_TOOLCHAIN=%DEPS%\android-toolchain-%MIN_API%-x86
|
||||
|
||||
SET ANDROID_ARM_CC=%ANDROID_ARM_TOOLCHAIN%\bin\arm-linux-androideabi-clang
|
||||
SET ANDROID_ARM_STRIP=%ANDROID_ARM_TOOLCHAIN%\bin\arm-linux-androideabi-strip
|
||||
|
||||
SET ANDROID_ARM64_CC=%ANDROID_ARM64_TOOLCHAIN%\bin\aarch64-linux-android-clang
|
||||
SET ANDROID_ARM64_STRIP=%ANDROID_ARM64_TOOLCHAIN%\bin\aarch64-linux-android-strip
|
||||
|
||||
SET ANDROID_X86_CC=%ANDROID_X86_TOOLCHAIN%\bin\i686-linux-android-clang
|
||||
SET ANDROID_X86_STRIP=%ANDROID_X86_TOOLCHAIN%\bin\i686-linux-android-strip
|
||||
|
||||
MKDIR %DEPS%>nul 2>nul
|
||||
MKDIR %TARGET%\armeabi-v7a>nul 2>nul
|
||||
MKDIR %TARGET%\x86>nul 2>nul
|
||||
MKDIR %TARGET%\arm64-v8a>nul 2>nul
|
||||
|
||||
IF NOT EXIST %ANDROID_ARM_CC% (
|
||||
ECHO "Make standalone toolchain for ARM arch"
|
||||
%ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch arm ^
|
||||
--api %MIN_API% --install-dir %ANDROID_ARM_TOOLCHAIN%
|
||||
)
|
||||
|
||||
IF NOT EXIST %ANDROID_ARM64_CC% (
|
||||
ECHO "Make standalone toolchain for ARM64 arch"
|
||||
%ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch arm64 ^
|
||||
--api %MIN_API% --install-dir %ANDROID_ARM64_TOOLCHAIN%
|
||||
)
|
||||
|
||||
IF NOT EXIST %ANDROID_X86_CC% (
|
||||
ECHO "Make standalone toolchain for X86 arch"
|
||||
%ANDROID_NDK_HOME%\build\tools\make_standalone_toolchain.py --arch x86 ^
|
||||
--api %MIN_API% --install-dir %ANDROID_X86_TOOLCHAIN%
|
||||
)
|
||||
|
||||
IF NOT EXIST %DIR%\go\bin\go.exe (
|
||||
ECHO "Build the custom go"
|
||||
|
||||
PUSHD %DIR%\go\src
|
||||
CALL make.bat
|
||||
POPD
|
||||
)
|
||||
|
||||
SET GOROOT=%DIR%\go
|
||||
SET GOPATH=%DIR%
|
||||
SET PATH=%GOROOT%\bin;%GOPATH%\bin;%PATH%
|
||||
|
||||
SET BUILD=1
|
||||
IF EXIST "%TARGET%\armeabi-v7a\liboverture.so" (
|
||||
IF EXIST "%TARGET%\arm64-v8a\liboverture.so" (
|
||||
IF EXIST "%TARGET%\x86\liboverture.so" (
|
||||
SET BUILD=0
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
IF %BUILD% == 1 (
|
||||
ECHO "Get dependences for overture"
|
||||
go.exe get -u github.com\tools\godep
|
||||
|
||||
PUSHD %GOPATH%\src\github.com\shadowsocks\overture\main
|
||||
godep.exe restore
|
||||
|
||||
ECHO "Cross compile overture for arm"
|
||||
IF NOT EXIST "%TARGET%\armeabi-v7a\liboverture.so" (
|
||||
SETLOCAL
|
||||
SET CGO_ENABLED=1
|
||||
SET CC=%ANDROID_ARM_CC%
|
||||
SET GOOS=android
|
||||
SET GOARCH=arm
|
||||
SET GOARM=7
|
||||
go.exe build -ldflags="-s -w"
|
||||
%ANDROID_ARM_STRIP% main
|
||||
MOVE main %TARGET%\armeabi-v7a\liboverture.so>nul 2>nul
|
||||
ENDLOCAL
|
||||
)
|
||||
|
||||
ECHO "Cross compile overture for arm64"
|
||||
IF NOT EXIST "%TARGET%\arm64-v8a\liboverture.so" (
|
||||
SETLOCAL
|
||||
SET CGO_ENABLED=1
|
||||
SET CC=%ANDROID_ARM64_CC%
|
||||
SET GOOS=android
|
||||
SET GOARCH=arm64
|
||||
go.exe build -ldflags="-s -w"
|
||||
%ANDROID_ARM64_STRIP% main
|
||||
MOVE main %TARGET%\arm64-v8a\liboverture.so>nul 2>nul
|
||||
ENDLOCAL
|
||||
)
|
||||
|
||||
ECHO "Cross compile overture for x86"
|
||||
IF NOT EXIST "%TARGET%\x86\liboverture.so" (
|
||||
SETLOCAL
|
||||
SET CGO_ENABLED=1
|
||||
SET CC=%ANDROID_X86_CC%
|
||||
SET GOOS=android
|
||||
SET GOARCH=386
|
||||
go.exe build -ldflags="-s -w"
|
||||
%ANDROID_X86_STRIP% main
|
||||
MOVE main %TARGET%\x86\liboverture.so>nul 2>nul
|
||||
ENDLOCAL
|
||||
)
|
||||
|
||||
POPD
|
||||
)
|
||||
|
||||
ECHO "Successfully build overture"
|
||||
ENDLOCAL
|
||||
@@ -0,0 +1,286 @@
|
||||
# https://github.com/arturbosch/detekt/blob/801994bd60cc759b181649356cea045b8125301b/detekt-cli/src/main/resources/default-detekt-config.yml
|
||||
|
||||
comments:
|
||||
active: false
|
||||
|
||||
complexity:
|
||||
active: true
|
||||
ComplexCondition:
|
||||
active: true
|
||||
threshold: 3
|
||||
ComplexInterface:
|
||||
active: true
|
||||
threshold: 10
|
||||
includeStaticDeclarations: false
|
||||
ComplexMethod:
|
||||
active: true
|
||||
threshold: 10
|
||||
LabeledExpression:
|
||||
active: true
|
||||
LargeClass:
|
||||
active: true
|
||||
threshold: 150
|
||||
LongMethod:
|
||||
active: true
|
||||
threshold: 20
|
||||
LongParameterList:
|
||||
active: true
|
||||
threshold: 5
|
||||
ignoreDefaultParameters: true
|
||||
MethodOverloading:
|
||||
active: false
|
||||
NestedBlockDepth:
|
||||
active: true
|
||||
threshold: 3
|
||||
StringLiteralDuplication:
|
||||
active: true
|
||||
threshold: 2
|
||||
ignoreAnnotation: true
|
||||
excludeStringsWithLessThan5Characters: true
|
||||
ignoreStringsRegex: '$^'
|
||||
TooManyFunctions:
|
||||
active: true
|
||||
thresholdInFiles: 10
|
||||
thresholdInClasses: 10
|
||||
thresholdInInterfaces: 10
|
||||
thresholdInObjects: 10
|
||||
thresholdInEnums: 10
|
||||
|
||||
empty-blocks:
|
||||
active: true
|
||||
EmptyCatchBlock:
|
||||
active: false
|
||||
EmptyClassBlock:
|
||||
active: true
|
||||
EmptyDefaultConstructor:
|
||||
active: true
|
||||
EmptyDoWhileBlock:
|
||||
active: true
|
||||
EmptyElseBlock:
|
||||
active: true
|
||||
EmptyFinallyBlock:
|
||||
active: true
|
||||
EmptyForBlock:
|
||||
active: true
|
||||
EmptyFunctionBlock:
|
||||
active: true
|
||||
EmptyIfBlock:
|
||||
active: true
|
||||
EmptyInitBlock:
|
||||
active: true
|
||||
EmptyKtFile:
|
||||
active: true
|
||||
EmptySecondaryConstructor:
|
||||
active: true
|
||||
EmptyWhenBlock:
|
||||
active: true
|
||||
EmptyWhileBlock:
|
||||
active: true
|
||||
|
||||
exceptions:
|
||||
active: true
|
||||
ExceptionRaisedInUnexpectedLocation:
|
||||
active: true
|
||||
methodNames: 'toString,hashCode,equals,finalize'
|
||||
InstanceOfCheckForException:
|
||||
active: true
|
||||
NotImplementedDeclaration:
|
||||
active: true
|
||||
PrintStackTrace:
|
||||
active: false
|
||||
RethrowCaughtException:
|
||||
active: true
|
||||
ReturnFromFinally:
|
||||
active: true
|
||||
SwallowedException:
|
||||
active: true
|
||||
ThrowingExceptionFromFinally:
|
||||
active: true
|
||||
ThrowingExceptionInMain:
|
||||
active: true
|
||||
ThrowingExceptionsWithoutMessageOrCause:
|
||||
active: true
|
||||
exceptions: 'IllegalArgumentException,IllegalStateException,IOException'
|
||||
ThrowingNewInstanceOfSameException:
|
||||
active: true
|
||||
TooGenericExceptionCaught:
|
||||
active: true
|
||||
exceptions:
|
||||
- ArrayIndexOutOfBoundsException
|
||||
- Error
|
||||
- Exception
|
||||
- IllegalMonitorStateException
|
||||
- NullPointerException
|
||||
- IndexOutOfBoundsException
|
||||
- RuntimeException
|
||||
- Throwable
|
||||
TooGenericExceptionThrown:
|
||||
active: true
|
||||
exceptions:
|
||||
- Error
|
||||
- Exception
|
||||
- NullPointerException
|
||||
- Throwable
|
||||
- RuntimeException
|
||||
|
||||
naming:
|
||||
active: true
|
||||
ClassNaming:
|
||||
active: true
|
||||
classPattern: '[A-Z$][a-zA-Z0-9$]*'
|
||||
EnumNaming:
|
||||
active: true
|
||||
enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*'
|
||||
ForbiddenClassName:
|
||||
active: true
|
||||
forbiddenName: ''
|
||||
FunctionMaxLength:
|
||||
active: true
|
||||
maximumFunctionNameLength: 30
|
||||
FunctionMinLength:
|
||||
active: true
|
||||
minimumFunctionNameLength: 3
|
||||
FunctionNaming:
|
||||
active: true
|
||||
functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$'
|
||||
MatchingDeclarationName:
|
||||
active: true
|
||||
MemberNameEqualsClassName:
|
||||
active: false
|
||||
ObjectPropertyNaming:
|
||||
active: true
|
||||
propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
|
||||
PackageNaming:
|
||||
active: true
|
||||
packagePattern: '^[a-z]+(\.[a-z][a-z0-9]*)*$'
|
||||
TopLevelPropertyNaming:
|
||||
active: true
|
||||
constantPattern: '[A-Z][_A-Z0-9]*'
|
||||
propertyPattern: '[a-z][A-Za-z\d]*'
|
||||
privatePropertyPattern: '(_)?[a-z][A-Za-z0-9]*'
|
||||
VariableMaxLength:
|
||||
active: true
|
||||
maximumVariableNameLength: 64
|
||||
VariableMinLength:
|
||||
active: false
|
||||
VariableNaming:
|
||||
active: true
|
||||
variablePattern: '[a-z][A-Za-z0-9]*'
|
||||
privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
|
||||
|
||||
performance:
|
||||
active: true
|
||||
ForEachOnRange:
|
||||
active: true
|
||||
SpreadOperator:
|
||||
active: true
|
||||
UnnecessaryTemporaryInstantiation:
|
||||
active: true
|
||||
|
||||
potential-bugs:
|
||||
active: true
|
||||
DuplicateCaseInWhenExpression:
|
||||
active: true
|
||||
EqualsAlwaysReturnsTrueOrFalse:
|
||||
active: true
|
||||
EqualsWithHashCodeExist:
|
||||
active: true
|
||||
ExplicitGarbageCollectionCall:
|
||||
active: true
|
||||
InvalidRange:
|
||||
active: true
|
||||
IteratorHasNextCallsNextMethod:
|
||||
active: true
|
||||
IteratorNotThrowingNoSuchElementException:
|
||||
active: true
|
||||
LateinitUsage:
|
||||
active: false
|
||||
UnconditionalJumpStatementInLoop:
|
||||
active: true
|
||||
UnreachableCode:
|
||||
active: true
|
||||
UnsafeCallOnNullableType:
|
||||
active: false
|
||||
UnsafeCast:
|
||||
active: false
|
||||
UselessPostfixExpression:
|
||||
active: true
|
||||
WrongEqualsTypeParameter:
|
||||
active: true
|
||||
|
||||
style:
|
||||
active: true
|
||||
CollapsibleIfStatements:
|
||||
active: true
|
||||
DataClassContainsFunctions:
|
||||
active: true
|
||||
conversionFunctionPrefix: 'to'
|
||||
EqualsNullCall:
|
||||
active: true
|
||||
ExpressionBodySyntax:
|
||||
active: true
|
||||
ForbiddenComment:
|
||||
active: true
|
||||
values: 'TODO:,FIXME:,STOPSHIP:'
|
||||
ForbiddenImport:
|
||||
active: true
|
||||
imports: ''
|
||||
FunctionOnlyReturningConstant:
|
||||
active: true
|
||||
ignoreOverridableFunction: true
|
||||
excludedFunctions: 'describeContents'
|
||||
LoopWithTooManyJumpStatements:
|
||||
active: true
|
||||
maxJumpCount: 1
|
||||
MagicNumber:
|
||||
active: false
|
||||
MaxLineLength:
|
||||
active: true
|
||||
maxLineLength: 120
|
||||
excludePackageStatements: false
|
||||
excludeImportStatements: false
|
||||
ModifierOrder:
|
||||
active: true
|
||||
NestedClassesVisibility:
|
||||
active: true
|
||||
NewLineAtEndOfFile:
|
||||
active: true
|
||||
OptionalAbstractKeyword:
|
||||
active: true
|
||||
OptionalReturnKeyword:
|
||||
active: true
|
||||
OptionalUnit:
|
||||
active: true
|
||||
OptionalWhenBraces:
|
||||
active: true
|
||||
ProtectedMemberInFinalClass:
|
||||
active: true
|
||||
RedundantVisibilityModifierRule:
|
||||
active: true
|
||||
ReturnCount:
|
||||
active: false
|
||||
SafeCast:
|
||||
active: true
|
||||
SerialVersionUIDInSerializableClass:
|
||||
active: true
|
||||
SpacingBetweenPackageAndImports:
|
||||
active: true
|
||||
ThrowsCount:
|
||||
active: true
|
||||
max: 2
|
||||
UnnecessaryAbstractClass:
|
||||
active: true
|
||||
UnnecessaryInheritance:
|
||||
active: true
|
||||
UnnecessaryParentheses:
|
||||
active: true
|
||||
UntilInsteadOfRangeTo:
|
||||
active: true
|
||||
UnusedImports:
|
||||
active: true
|
||||
UseDataClass:
|
||||
active: false
|
||||
UtilityClassWithPublicConstructor:
|
||||
active: true
|
||||
WildcardImport:
|
||||
active: false
|
||||
@@ -1,6 +1,5 @@
|
||||
#Fri Dec 01 17:03:17 PST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
@@ -6,42 +6,6 @@
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
@@ -60,6 +24,46 @@ cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -85,7 +89,7 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
@@ -150,11 +154,19 @@ if $cygwin ; then
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
@@ -46,10 +46,9 @@ echo location of your Java installation.
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
@@ -60,11 +59,6 @@ set _SKIP=2
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import com.android.build.OutputFile
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@@ -7,7 +8,7 @@ apply plugin: 'kotlin-android'
|
||||
def getCurrentFlavor() {
|
||||
String task = getGradle().getStartParameter().getTaskRequests().toString()
|
||||
Matcher matcher = Pattern.compile("(assemble|generate)\\w*(Release|Debug)").matcher(task)
|
||||
if (matcher.find()) return matcher.group(1).toLowerCase() else {
|
||||
if (matcher.find()) return matcher.group(2).toLowerCase() else {
|
||||
println "Warning: No match found for $task"
|
||||
return "debug"
|
||||
}
|
||||
@@ -20,15 +21,15 @@ android {
|
||||
applicationId "com.github.shadowsocks"
|
||||
minSdkVersion rootProject.minSdkVersion
|
||||
targetSdkVersion rootProject.sdkVersion
|
||||
versionCode 203
|
||||
versionName "4.4.3"
|
||||
versionCode 4050000
|
||||
versionName "4.5.0"
|
||||
testApplicationId "com.github.shadowsocks.test"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary true
|
||||
resConfigs "fa", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
|
||||
resConfigs "fa", "fr", "ja", "ko", "ru", "zh-rCN", "zh-rTW"
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
multiDexEnabled true
|
||||
pseudoLocalesEnabled true
|
||||
}
|
||||
release {
|
||||
@@ -37,6 +38,15 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
lintOptions.checkReleaseBuilds false
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'armeabi-v7a', 'arm64-v8a', 'x86'
|
||||
universalApk true
|
||||
}
|
||||
}
|
||||
sourceSets.main.jniLibs.srcDirs +=
|
||||
new File(project(':core').buildDir, "intermediates/bundles/${getCurrentFlavor()}/jni")
|
||||
sourceSets.main.jniLibs.srcDirs += new File(project(':core').projectDir, "src/overture/bin")
|
||||
@@ -48,25 +58,21 @@ dependencies {
|
||||
implementation "com.android.support:design:$supportLibraryVersion"
|
||||
implementation "com.android.support:gridlayout-v7:$supportLibraryVersion"
|
||||
implementation 'com.futuremind.recyclerfastscroll:fastscroll:0.2.5'
|
||||
implementation 'com.evernote:android-job:1.2.1'
|
||||
implementation 'com.evernote:android-job:1.2.4'
|
||||
implementation "com.google.android.gms:play-services-ads:$playServicesVersion"
|
||||
implementation "com.google.android.gms:play-services-analytics:$playServicesVersion"
|
||||
implementation "com.google.android.gms:play-services-gcm:$playServicesVersion"
|
||||
implementation "com.google.android.gms:play-services-vision:$playServicesVersion"
|
||||
implementation "com.google.firebase:firebase-config:$playServicesVersion"
|
||||
implementation 'com.j256.ormlite:ormlite-android:5.0'
|
||||
implementation 'com.mikepenz:crossfader:1.5.1'
|
||||
implementation 'com.mikepenz:fastadapter:3.0.4'
|
||||
implementation 'com.mikepenz:iconics-core:3.0.0'
|
||||
implementation 'com.mikepenz:materialdrawer:6.0.2'
|
||||
implementation 'com.mikepenz:materialize:1.1.2'
|
||||
implementation 'com.mikepenz:materialdrawer:6.0.6'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.9.1'
|
||||
implementation "com.takisoft.fix:preference-v7-simplemenu:$takisoftFixVersion"
|
||||
implementation 'com.twofortyfouram:android-plugin-api-for-locale:1.0.2'
|
||||
implementation 'dnsjava:dnsjava:2.1.8'
|
||||
implementation 'eu.chainfire:libsuperuser:1.0.0.201704021214'
|
||||
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
|
||||
implementation 'net.glxn.qrgen:android:2.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
|
||||
implementation 'xyz.belvi.mobilevision:barcodescanner:2.0.3'
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "com.android.support.test:runner:$androidTestVersion"
|
||||
androidTestImplementation "com.android.support.test.espresso:espresso-core:$androidEspressoVersion"
|
||||
@@ -76,3 +82,11 @@ repositories {
|
||||
}
|
||||
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
|
||||
ext.abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, x86: 3]
|
||||
if (getCurrentFlavor() == 'release') android.applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def offset = project.ext.abiCodes.get(output.getFilter(OutputFile.ABI))
|
||||
if (offset != null) output.versionCodeOverride = variant.versionCode + offset
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".VpnRequestActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:theme="@style/Theme.Translucent"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:launchMode="singleTask"/>
|
||||
@@ -169,17 +169,16 @@
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
<service android:name="com.evernote.android.job.v14.PlatformAlarmService"
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
tools:node="remove"/>
|
||||
<service android:name="com.evernote.android.job.v14.PlatformAlarmServiceExact"
|
||||
tools:node="remove"/>
|
||||
<service android:name="com.evernote.android.job.gcm.PlatformGcmService"
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
tools:node="remove"/>
|
||||
<service android:name="com.evernote.android.job.JobRescheduleService"
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
<receiver android:name="com.evernote.android.job.v14.PlatformAlarmReceiver"
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
tools:node="remove"/>
|
||||
<receiver android:name="com.evernote.android.job.JobBootReceiver"
|
||||
android:process=":bg"
|
||||
android:directBootAware="true"/>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
[proxy_all]
|
||||
|
||||
[bypass_list]
|
||||
0.0.0.0/8
|
||||
10.0.0.0/8
|
||||
100.64.0.0/10
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
|
||||
[proxy_list]
|
||||
# Telegram IPs$
|
||||
91.108.4.0/22
|
||||
91.108.56.0/23
|
||||
91.108.56.0/22
|
||||
91.108.4.0/22
|
||||
149.154.172.0/22
|
||||
149.154.168.0/22
|
||||
149.154.164.0/22
|
||||
109.239.140.0/24
|
||||
149.154.160.0/20
|
||||
(^|\.)4tern\.com$
|
||||
(^|\.)adorama\.com$
|
||||
(^|\.)akiba-web\.com$
|
||||
|
||||
@@ -32,14 +32,15 @@ import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.LocaleList
|
||||
import android.os.Looper
|
||||
import android.support.annotation.RequiresApi
|
||||
import android.support.v4.os.UserManagerCompat
|
||||
import android.support.v7.app.AppCompatDelegate
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.evernote.android.job.JobConstants
|
||||
import com.evernote.android.job.JobManager
|
||||
import com.evernote.android.job.JobManagerCreateException
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.acl.AclSyncJob
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
@@ -59,20 +60,11 @@ import com.j256.ormlite.logger.LocalLog
|
||||
import com.takisoft.fix.support.v7.preference.PreferenceFragmentCompat
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
|
||||
class App : Application() {
|
||||
companion object {
|
||||
lateinit var app: App
|
||||
private const val TAG = "ShadowsocksApplication"
|
||||
|
||||
// The ones in Locale doesn't have script included
|
||||
private val SIMPLIFIED_CHINESE by lazy {
|
||||
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hans-CN") else Locale.SIMPLIFIED_CHINESE
|
||||
}
|
||||
private val TRADITIONAL_CHINESE by lazy {
|
||||
if (Build.VERSION.SDK_INT >= 21) Locale.forLanguageTag("zh-Hant-TW") else Locale.TRADITIONAL_CHINESE
|
||||
}
|
||||
}
|
||||
|
||||
val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
@@ -112,59 +104,11 @@ class App : Application() {
|
||||
t.printStackTrace()
|
||||
}
|
||||
|
||||
private fun checkChineseLocale(config: Configuration) {
|
||||
fun check(locale: Locale): Locale? {
|
||||
if (locale.language != "zh") return null
|
||||
when (locale.country) { "CN", "TW" -> return null }
|
||||
if (Build.VERSION.SDK_INT >= 21) when (locale.script) {
|
||||
"Hans" -> return SIMPLIFIED_CHINESE
|
||||
"Hant" -> return TRADITIONAL_CHINESE
|
||||
else -> Log.w(TAG, "Unknown zh locale script: ${locale.script}. Falling back to trying countries...")
|
||||
}
|
||||
when (locale.country) {
|
||||
"SG" -> return SIMPLIFIED_CHINESE
|
||||
"HK", "MO" -> return TRADITIONAL_CHINESE
|
||||
}
|
||||
Log.w(TAG, "Unknown zh locale: %s. Falling back to zh-Hans-CN..."
|
||||
.format(Locale.ENGLISH, if (Build.VERSION.SDK_INT >= 21) locale.toLanguageTag() else locale))
|
||||
return SIMPLIFIED_CHINESE
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 24) @RequiresApi(24) {
|
||||
val localeList = config.locales
|
||||
var changed = false
|
||||
val newList = Array<Locale>(localeList.size(), { i ->
|
||||
val locale = localeList[i]
|
||||
val newLocale = check(locale)
|
||||
if (newLocale == null) locale else {
|
||||
changed = true
|
||||
newLocale
|
||||
}
|
||||
})
|
||||
if (changed) {
|
||||
val newConfig = Configuration(config)
|
||||
newConfig.locales = LocaleList(*(newList.distinct().toTypedArray()))
|
||||
val res = resources
|
||||
res.updateConfiguration(newConfig, res.displayMetrics)
|
||||
}
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
val newLocale = check(config.locale)
|
||||
if (newLocale != null) {
|
||||
val newConfig = Configuration(config)
|
||||
@Suppress("DEPRECATION")
|
||||
newConfig.locale = newLocale
|
||||
val res = resources
|
||||
res.updateConfiguration(newConfig, res.displayMetrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
app = this
|
||||
if (!BuildConfig.DEBUG) System.setProperty(LocalLog.LOCAL_LOG_LEVEL_PROPERTY, "ERROR")
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
checkChineseLocale(resources.configuration)
|
||||
PreferenceFragmentCompat.registerPreferenceFragment(IconListPreference::class.java,
|
||||
BottomSheetPreferenceDialogFragment::class.java)
|
||||
|
||||
@@ -184,11 +128,16 @@ class App : Application() {
|
||||
remoteConfig.fetch().addOnCompleteListener {
|
||||
if (it.isSuccessful) remoteConfig.activateFetched() else Log.e(TAG, "Failed to fetch config")
|
||||
}
|
||||
JobManager.create(deviceContext).addJobCreator(AclSyncJob)
|
||||
try {
|
||||
JobManager.create(deviceContext).addJobCreator(AclSyncJob)
|
||||
} catch (e: JobManagerCreateException) {
|
||||
Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show()
|
||||
app.track(e)
|
||||
}
|
||||
|
||||
// handle data restored
|
||||
if (DataStore.directBootAware && UserManagerCompat.isUserUnlocked(this)) DirectBoot.update()
|
||||
TcpFastOpen.enabled(DataStore.publicStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
|
||||
TcpFastOpen.enabledAsync(DataStore.publicStore.getBoolean(Key.tfo, TcpFastOpen.sendEnabled))
|
||||
if (DataStore.publicStore.getLong(Key.assetUpdateTime, -1) != info.lastUpdateTime) {
|
||||
val assetManager = assets
|
||||
for (dir in arrayOf("acl", "overture"))
|
||||
@@ -203,6 +152,15 @@ class App : Application() {
|
||||
DataStore.publicStore.putLong(Key.assetUpdateTime, info.lastUpdateTime)
|
||||
}
|
||||
|
||||
updateNotificationChannels()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
updateNotificationChannels()
|
||||
}
|
||||
|
||||
private fun updateNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= 26) @RequiresApi(26) {
|
||||
val nm = getSystemService(NotificationManager::class.java)
|
||||
nm.createNotificationChannels(listOf(
|
||||
@@ -216,18 +174,16 @@ class App : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
checkChineseLocale(newConfig)
|
||||
}
|
||||
|
||||
fun listenForPackageChanges(callback: () -> Unit): BroadcastReceiver {
|
||||
fun listenForPackageChanges(onetime: Boolean = true, callback: () -> Unit): BroadcastReceiver {
|
||||
val filter = IntentFilter(Intent.ACTION_PACKAGE_ADDED)
|
||||
filter.addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||
filter.addDataScheme("package")
|
||||
val result = broadcastReceiver { _, intent ->
|
||||
if (intent.action != Intent.ACTION_PACKAGE_REMOVED ||
|
||||
!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) callback()
|
||||
val result = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) return
|
||||
callback()
|
||||
if (onetime) app.unregisterReceiver(this)
|
||||
}
|
||||
}
|
||||
app.registerReceiver(result, filter)
|
||||
return result
|
||||
|
||||
@@ -34,6 +34,7 @@ import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v4.app.TaskStackBuilder
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.DefaultItemAnimator
|
||||
@@ -43,7 +44,6 @@ import android.support.v7.widget.Toolbar
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.Switch
|
||||
import android.widget.Toast
|
||||
import com.futuremind.recyclerviewfastscroll.FastScroller
|
||||
import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
@@ -63,11 +63,14 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
private var cachedApps: List<PackageInfo>? = null
|
||||
private fun getApps(pm: PackageManager): List<ProxiedApp> {
|
||||
if (receiver == null) receiver = app.listenForPackageChanges {
|
||||
synchronized(AppManager) { cachedApps = null }
|
||||
AppManager.instance?.reloadApps()
|
||||
}
|
||||
return synchronized(AppManager) {
|
||||
if (receiver == null) receiver = app.listenForPackageChanges {
|
||||
synchronized(AppManager) {
|
||||
receiver = null
|
||||
cachedApps = null
|
||||
}
|
||||
AppManager.instance?.reloadApps()
|
||||
}
|
||||
// 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 &&
|
||||
@@ -231,14 +234,14 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
|
||||
ProfileManager.updateProfile(it)
|
||||
}
|
||||
if (DataStore.directBootAware) DirectBoot.update()
|
||||
Toast.makeText(this, R.string.action_apply_all, Toast.LENGTH_SHORT).show()
|
||||
} else Toast.makeText(this, R.string.action_export_err, Toast.LENGTH_SHORT).show()
|
||||
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()
|
||||
return true
|
||||
}
|
||||
R.id.action_export -> {
|
||||
clipboard.primaryClip = ClipData.newPlainText(Key.individual,
|
||||
"${DataStore.bypass}\n${DataStore.individual}")
|
||||
Toast.makeText(this, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
|
||||
Snackbar.make(appListView, R.string.action_export_msg, Snackbar.LENGTH_LONG).show()
|
||||
return true
|
||||
}
|
||||
R.id.action_import -> {
|
||||
@@ -251,23 +254,21 @@ class AppManager : AppCompatActivity(), Toolbar.OnMenuItemClickListener {
|
||||
bypassSwitch.isChecked = enabled.toBoolean()
|
||||
DataStore.individual = apps
|
||||
DataStore.dirty = true
|
||||
Toast.makeText(this, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
|
||||
Snackbar.make(appListView, R.string.action_import_msg, Snackbar.LENGTH_LONG).show()
|
||||
initProxiedApps(apps)
|
||||
reloadApps()
|
||||
return true
|
||||
} catch (_: IllegalArgumentException) { }
|
||||
}
|
||||
Toast.makeText(this, R.string.action_import_err, Toast.LENGTH_SHORT).show()
|
||||
Snackbar.make(appListView, R.string.action_import_err, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
return if (keyCode == KeyEvent.KEYCODE_MENU)
|
||||
if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu() else toolbar.showOverflowMenu()
|
||||
else super.onKeyUp(keyCode, event)
|
||||
}
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent?) = if (keyCode == KeyEvent.KEYCODE_MENU)
|
||||
if (toolbar.isOverflowMenuShowing) toolbar.hideOverflowMenu() else toolbar.showOverflowMenu()
|
||||
else super.onKeyUp(keyCode, event)
|
||||
|
||||
override fun onDestroy() {
|
||||
instance = null
|
||||
|
||||
@@ -26,7 +26,6 @@ import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v14.preference.SwitchPreference
|
||||
import android.support.v7.preference.Preference
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.net.VpnService
|
||||
@@ -56,13 +58,11 @@ import com.github.shadowsocks.utils.Key
|
||||
import com.github.shadowsocks.utils.responseLength
|
||||
import com.github.shadowsocks.utils.thread
|
||||
import com.github.shadowsocks.widget.ServiceButton
|
||||
import com.mikepenz.crossfader.Crossfader
|
||||
import com.mikepenz.crossfader.view.CrossFadeSlidingPaneLayout
|
||||
import com.mikepenz.materialdrawer.Drawer
|
||||
import com.mikepenz.materialdrawer.DrawerBuilder
|
||||
import com.mikepenz.materialdrawer.interfaces.ICrossfader
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
@@ -81,12 +81,14 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
private const val DRAWER_FAQ = 4L
|
||||
private const val DRAWER_CUSTOM_RULES = 5L
|
||||
|
||||
fun pendingIntent(context: Context) = PendingIntent.getActivity(context, 0,
|
||||
Intent(context, MainActivity::class.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0)
|
||||
|
||||
var stateListener: ((Int) -> Unit)? = null
|
||||
}
|
||||
|
||||
// UI
|
||||
private lateinit var fab: ServiceButton
|
||||
internal var crossfader: Crossfader<CrossFadeSlidingPaneLayout>? = null
|
||||
internal lateinit var drawer: Drawer
|
||||
private var previousSelectedDrawer: Long = 0 // it's actually lateinit
|
||||
|
||||
@@ -168,9 +170,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
url.openConnection(Proxy(Proxy.Type.SOCKS,
|
||||
InetSocketAddress("127.0.0.1", DataStore.portProxy))))
|
||||
as HttpURLConnection
|
||||
conn.setRequestProperty("Connection", "close")
|
||||
conn.instanceFollowRedirects = false
|
||||
conn.connectTimeout = 10000
|
||||
conn.readTimeout = 10000
|
||||
conn.useCaches = false
|
||||
val (success, result) = try {
|
||||
val start = SystemClock.elapsedRealtime()
|
||||
@@ -178,8 +179,8 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
val elapsed = SystemClock.elapsedRealtime() - start
|
||||
if (code == 204 || code == 200 && conn.responseLength == 0L)
|
||||
Pair(true, getString(R.string.connection_test_available, elapsed))
|
||||
else throw Exception(getString(R.string.connection_test_error_status_code, code))
|
||||
} catch (e: Exception) {
|
||||
else throw IOException(getString(R.string.connection_test_error_status_code, code))
|
||||
} catch (e: IOException) {
|
||||
Pair(false, getString(R.string.connection_test_error, e.message))
|
||||
} finally {
|
||||
conn.disconnect()
|
||||
@@ -196,6 +197,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
override fun onServiceConnected(service: IShadowsocksService) = changeState(service.state)
|
||||
override fun onServiceDisconnected() = changeState(BaseService.IDLE)
|
||||
override fun binderDied() {
|
||||
super.binderDied()
|
||||
app.handler.post {
|
||||
connection.disconnect()
|
||||
Executable.killAll()
|
||||
@@ -213,7 +215,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.layout_main)
|
||||
val drawerBuilder = DrawerBuilder()
|
||||
drawer = DrawerBuilder()
|
||||
.withActivity(this)
|
||||
.withTranslucentStatusBar(true)
|
||||
.withHeader(R.layout.layout_header)
|
||||
@@ -250,28 +252,7 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
.withOnDrawerItemClickListener(this)
|
||||
.withActionBarDrawerToggle(true)
|
||||
.withSavedInstance(savedInstanceState)
|
||||
val miniDrawerWidth = resources.getDimension(R.dimen.material_mini_drawer_item)
|
||||
if (resources.displayMetrics.widthPixels >=
|
||||
resources.getDimension(R.dimen.profile_item_max_width) + miniDrawerWidth) {
|
||||
drawer = drawerBuilder.withGenerateMiniDrawer(true).buildView()
|
||||
val crossfader = Crossfader<CrossFadeSlidingPaneLayout>()
|
||||
this.crossfader = crossfader
|
||||
crossfader
|
||||
.withContent(findViewById(android.R.id.content))
|
||||
.withFirst(drawer.slider, resources.getDimensionPixelSize(R.dimen.material_drawer_width))
|
||||
.withSecond(drawer.miniDrawer.build(this), miniDrawerWidth.toInt())
|
||||
.withSavedInstance(savedInstanceState)
|
||||
.build()
|
||||
if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL)
|
||||
crossfader.crossFadeSlidingPaneLayout.setShadowDrawableRight(
|
||||
AppCompatResources.getDrawable(this, R.drawable.material_drawer_shadow_right))
|
||||
else crossfader.crossFadeSlidingPaneLayout.setShadowDrawableLeft(
|
||||
AppCompatResources.getDrawable(this, R.drawable.material_drawer_shadow_left))
|
||||
drawer.miniDrawer.withCrossFader(object : ICrossfader { // a wrapper is needed
|
||||
override fun isCrossfaded(): Boolean = crossfader.isCrossFaded
|
||||
override fun crossfade() = crossfader.crossFade()
|
||||
})
|
||||
} else drawer = drawerBuilder.build()
|
||||
.build()
|
||||
|
||||
if (savedInstanceState == null) displayFragment(ProfilesFragment())
|
||||
previousSelectedDrawer = drawer.currentSelection
|
||||
@@ -395,7 +376,6 @@ class MainActivity : AppCompatActivity(), ShadowsocksConnection.Interface, Drawe
|
||||
override fun onSaveInstanceState(outState: Bundle?) {
|
||||
super.onSaveInstanceState(outState)
|
||||
drawer.saveInstanceState(outState)
|
||||
crossfader?.saveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
@@ -60,6 +61,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
private lateinit var plugin: IconListPreference
|
||||
private lateinit var pluginConfigure: EditTextPreference
|
||||
private lateinit var pluginConfiguration: PluginConfiguration
|
||||
private lateinit var receiver: BroadcastReceiver
|
||||
|
||||
override fun onCreatePreferencesFix(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
preferenceManager.preferenceDataStore = DataStore.privateStore
|
||||
@@ -80,13 +82,11 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
val serviceMode = DataStore.serviceMode
|
||||
findPreference(Key.remoteDns).isEnabled = serviceMode != Key.modeProxy
|
||||
isProxyApps = findPreference(Key.proxyApps) as SwitchPreference
|
||||
if (Build.VERSION.SDK_INT < 21) isProxyApps.parent!!.removePreference(isProxyApps) else {
|
||||
isProxyApps.isEnabled = serviceMode == Key.modeVpn
|
||||
isProxyApps.setOnPreferenceClickListener {
|
||||
startActivity(Intent(activity, AppManager::class.java))
|
||||
isProxyApps.isChecked = true
|
||||
false
|
||||
}
|
||||
isProxyApps.isEnabled = serviceMode == Key.modeVpn
|
||||
isProxyApps.setOnPreferenceClickListener {
|
||||
startActivity(Intent(activity, AppManager::class.java))
|
||||
isProxyApps.isChecked = true
|
||||
false
|
||||
}
|
||||
findPreference(Key.udpdns).isEnabled = serviceMode != Key.modeProxy
|
||||
plugin = findPreference(Key.plugin) as IconListPreference
|
||||
@@ -104,7 +104,7 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
}
|
||||
pluginConfigure.onPreferenceChangeListener = this
|
||||
initPlugins()
|
||||
app.listenForPackageChanges { initPlugins() }
|
||||
receiver = app.listenForPackageChanges(false) { initPlugins() }
|
||||
DataStore.privateStore.registerChangeListener(this)
|
||||
}
|
||||
|
||||
@@ -181,31 +181,30 @@ class ProfileConfigFragment : PreferenceFragmentCompatDividers(), Toolbar.OnMenu
|
||||
} else super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_delete -> {
|
||||
val activity = activity!!
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.delete_confirm_prompt)
|
||||
.setPositiveButton(R.string.yes, { _, _ ->
|
||||
ProfileManager.delProfile(profile.id)
|
||||
activity.finish()
|
||||
})
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.create()
|
||||
.show()
|
||||
true
|
||||
}
|
||||
R.id.action_apply -> {
|
||||
saveAndExit()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
override fun onMenuItemClick(item: MenuItem) = when (item.itemId) {
|
||||
R.id.action_delete -> {
|
||||
val activity = activity!!
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.delete_confirm_prompt)
|
||||
.setPositiveButton(R.string.yes, { _, _ ->
|
||||
ProfileManager.delProfile(profile.id)
|
||||
activity.finish()
|
||||
})
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.create()
|
||||
.show()
|
||||
true
|
||||
}
|
||||
R.id.action_apply -> {
|
||||
saveAndExit()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
DataStore.privateStore.unregisterChangeListener(this)
|
||||
app.unregisterReceiver(receiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.bg.TrafficMonitor
|
||||
@@ -370,12 +369,11 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
val profiles = Profile.findAll(clipboard.primaryClip.getItemAt(0).text).toList()
|
||||
if (profiles.isNotEmpty()) {
|
||||
profiles.forEach { ProfileManager.createProfile(it) }
|
||||
Toast.makeText(activity, R.string.action_import_msg, Toast.LENGTH_SHORT).show()
|
||||
Snackbar.make(activity!!.findViewById(R.id.snackbar), R.string.action_import_msg,
|
||||
Snackbar.LENGTH_LONG).show()
|
||||
return true
|
||||
}
|
||||
} catch (exc: Exception) {
|
||||
app.track(exc)
|
||||
}
|
||||
} catch (_: IndexOutOfBoundsException) { }
|
||||
Snackbar.make(activity!!.findViewById(R.id.snackbar), R.string.action_import_err, Snackbar.LENGTH_LONG)
|
||||
.show()
|
||||
true
|
||||
@@ -386,10 +384,10 @@ class ProfilesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
}
|
||||
R.id.action_export -> {
|
||||
val profiles = ProfileManager.getAllProfiles()
|
||||
if (profiles != null) {
|
||||
Snackbar.make(activity!!.findViewById(R.id.snackbar), if (profiles != null) {
|
||||
clipboard.primaryClip = ClipData.newPlainText(null, profiles.joinToString("\n"))
|
||||
Toast.makeText(activity, R.string.action_export_msg, Toast.LENGTH_SHORT).show()
|
||||
} else Toast.makeText(activity, R.string.action_export_err, Toast.LENGTH_SHORT).show()
|
||||
R.string.action_export_msg
|
||||
} else R.string.action_export_err, Snackbar.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
|
||||
@@ -20,29 +20,40 @@
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ShortcutManager
|
||||
import android.graphics.BitmapFactory
|
||||
import android.hardware.camera2.CameraAccessException
|
||||
import android.hardware.camera2.CameraManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.ActivityCompat
|
||||
import android.support.v4.app.TaskStackBuilder
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.utils.resolveResourceId
|
||||
import com.google.zxing.Result
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
||||
import com.google.android.gms.samples.vision.barcodereader.BarcodeCapture
|
||||
import com.google.android.gms.samples.vision.barcodereader.BarcodeGraphic
|
||||
import com.google.android.gms.vision.Frame
|
||||
import com.google.android.gms.vision.barcode.Barcode
|
||||
import com.google.android.gms.vision.barcode.BarcodeDetector
|
||||
import xyz.belvi.mobilevisionbarcodescanner.BarcodeRetriever
|
||||
|
||||
class ScannerActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
|
||||
class ScannerActivity : AppCompatActivity(), Toolbar.OnMenuItemClickListener, BarcodeRetriever {
|
||||
companion object {
|
||||
private const val MY_PERMISSIONS_REQUEST_CAMERA = 1
|
||||
private const val TAG = "ScannerActivity"
|
||||
private const val REQUEST_IMPORT = 2
|
||||
private const val REQUEST_IMPORT_OR_FINISH = 3
|
||||
}
|
||||
|
||||
private lateinit var scannerView: ZXingScannerView
|
||||
|
||||
private fun navigateUp() {
|
||||
val intent = parentActivityIntent
|
||||
if (shouldUpRecreateTask(intent) || isTaskRoot)
|
||||
@@ -52,45 +63,79 @@ class ScannerActivity : AppCompatActivity(), ZXingScannerView.ResultHandler {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan")
|
||||
if (try {
|
||||
(getSystemService(Context.CAMERA_SERVICE) as CameraManager).cameraIdList.isEmpty()
|
||||
} catch (_: CameraAccessException) {
|
||||
true
|
||||
}) {
|
||||
startImport()
|
||||
return
|
||||
}
|
||||
setContentView(R.layout.layout_scanner)
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
toolbar.title = title
|
||||
toolbar.setNavigationIcon(theme.resolveResourceId(R.attr.homeAsUpIndicator))
|
||||
toolbar.setNavigationOnClickListener { navigateUp() }
|
||||
scannerView = findViewById(R.id.scanner)
|
||||
if (Build.VERSION.SDK_INT >= 25) getSystemService(ShortcutManager::class.java).reportShortcutUsed("scan")
|
||||
toolbar.inflateMenu(R.menu.scanner_menu)
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
(supportFragmentManager.findFragmentById(R.id.barcode) as BarcodeCapture).setRetrieval(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val permissionCheck = ContextCompat.checkSelfPermission(this,
|
||||
android.Manifest.permission.CAMERA)
|
||||
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
|
||||
scannerView.setResultHandler(this) // Register ourselves as a handler for scan results.
|
||||
scannerView.startCamera() // Start camera on resume
|
||||
} else ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CAMERA),
|
||||
MY_PERMISSIONS_REQUEST_CAMERA)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
if (requestCode == MY_PERMISSIONS_REQUEST_CAMERA)
|
||||
if (grantResults.getOrNull(0) == PackageManager.PERMISSION_GRANTED) {
|
||||
scannerView.setResultHandler(this)
|
||||
scannerView.startCamera()
|
||||
} else {
|
||||
Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
}
|
||||
else super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
scannerView.stopCamera() // Stop camera on pause
|
||||
}
|
||||
|
||||
override fun handleResult(rawResult: Result?) {
|
||||
Profile.findAll(rawResult?.text).forEach { ProfileManager.createProfile(it) }
|
||||
override fun onRetrieved(barcode: Barcode) = runOnUiThread {
|
||||
Profile.findAll(barcode.rawValue).forEach { ProfileManager.createProfile(it) }
|
||||
navigateUp()
|
||||
}
|
||||
override fun onRetrievedMultiple(closetToClick: Barcode?, barcode: MutableList<BarcodeGraphic>?) = check(false)
|
||||
override fun onBitmapScanned(sparseArray: SparseArray<Barcode>?) { }
|
||||
override fun onRetrievedFailed(reason: String?) {
|
||||
Log.w(TAG, reason)
|
||||
}
|
||||
override fun onPermissionRequestDenied() {
|
||||
Toast.makeText(this, R.string.add_profile_scanner_permission_required, Toast.LENGTH_SHORT).show()
|
||||
startImport()
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem) = when (item.itemId) {
|
||||
R.id.action_import -> {
|
||||
startImport(true)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun startImport(manual: Boolean = false) = startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
.setType("image/*")
|
||||
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true), if (manual) REQUEST_IMPORT else REQUEST_IMPORT_OR_FINISH)
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
REQUEST_IMPORT, REQUEST_IMPORT_OR_FINISH -> if (resultCode == Activity.RESULT_OK) {
|
||||
var success = false
|
||||
val detector = BarcodeDetector.Builder(this)
|
||||
.setBarcodeFormats(Barcode.QR_CODE)
|
||||
.build()
|
||||
if (detector.isOperational) {
|
||||
var list = listOfNotNull(data?.data)
|
||||
val clipData = data?.clipData
|
||||
if (clipData != null) list += (0 until clipData.itemCount).map { clipData.getItemAt(it).uri }
|
||||
val resolver = contentResolver
|
||||
for (uri in list) try {
|
||||
val barcodes = detector.detect(Frame.Builder()
|
||||
.setBitmap(BitmapFactory.decodeStream(resolver.openInputStream(uri))).build())
|
||||
for (i in 0 until barcodes.size()) Profile.findAll(barcodes.valueAt(i).rawValue).forEach {
|
||||
ProfileManager.createProfile(it)
|
||||
success = true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
app.track(e)
|
||||
}
|
||||
} else Log.w(TAG, "Google vision isn't operational.")
|
||||
Toast.makeText(this, if (success) R.string.action_import_msg else R.string.action_import_err,
|
||||
Toast.LENGTH_SHORT).show()
|
||||
navigateUp()
|
||||
} else if (requestCode == REQUEST_IMPORT_OR_FINISH) navigateUp()
|
||||
else -> super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
|
||||
* Different from Android framework, this method will be called even when you call `detachService`.
|
||||
*/
|
||||
fun onServiceDisconnected() { }
|
||||
override fun binderDied() { }
|
||||
override fun binderDied() {
|
||||
connection.service = null
|
||||
}
|
||||
}
|
||||
|
||||
private var connectionActive = false
|
||||
@@ -109,6 +111,8 @@ class ShadowsocksConnection(private val instance: Interface) : ServiceConnection
|
||||
connectionActive = false
|
||||
if (instance.listenForDeath) binder?.unlinkToDeath(instance, 0)
|
||||
binder = null
|
||||
if (instance.serviceCallback != null)
|
||||
service?.stopListeningForBandwidth(instance.serviceCallback)
|
||||
service = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ open class ToolbarFragment : Fragment() {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
toolbar = view.findViewById(R.id.toolbar)
|
||||
val activity = activity as MainActivity
|
||||
if (activity.crossfader == null) activity.drawer.setToolbar(activity, toolbar, true)
|
||||
activity.drawer.setToolbar(activity, toolbar, true)
|
||||
}
|
||||
|
||||
open fun onTrafficUpdated(profileId: Int, txRate: Long, rxRate: Long, txTotal: Long, rxTotal: Long) { }
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
|
||||
package com.github.shadowsocks
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.KeyguardManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
@@ -28,13 +27,14 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.VpnService
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.aidl.IShadowsocksService
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.utils.broadcastReceiver
|
||||
|
||||
class VpnRequestActivity : Activity(), ShadowsocksConnection.Interface {
|
||||
class VpnRequestActivity : AppCompatActivity(), ShadowsocksConnection.Interface {
|
||||
companion object {
|
||||
private const val TAG = "VpnRequestActivity"
|
||||
private const val REQUEST_CONNECT = 1
|
||||
@@ -56,11 +56,9 @@ class VpnRequestActivity : Activity(), ShadowsocksConnection.Interface {
|
||||
}
|
||||
|
||||
override fun onServiceConnected(service: IShadowsocksService) {
|
||||
app.handler.postDelayed({
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent == null) onActivityResult(REQUEST_CONNECT, RESULT_OK, null)
|
||||
else startActivityForResult(intent, REQUEST_CONNECT)
|
||||
}, 1000)
|
||||
val intent = VpnService.prepare(this)
|
||||
if (intent == null) onActivityResult(REQUEST_CONNECT, RESULT_OK, null)
|
||||
else startActivityForResult(intent, REQUEST_CONNECT)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
|
||||
@@ -23,22 +23,21 @@ package com.github.shadowsocks.acl
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.design.widget.TextInputLayout
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v7.app.AlertDialog
|
||||
import android.support.v7.widget.DefaultItemAnimator
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.support.v7.widget.Toolbar
|
||||
import android.support.v7.widget.helper.ItemTouchHelper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import com.futuremind.recyclerviewfastscroll.FastScroller
|
||||
import com.futuremind.recyclerviewfastscroll.SectionTitleProvider
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
@@ -48,12 +47,14 @@ import com.github.shadowsocks.ToolbarFragment
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import com.github.shadowsocks.utils.Subnet
|
||||
import com.github.shadowsocks.utils.asIterable
|
||||
import com.github.shadowsocks.utils.resolveResourceId
|
||||
import com.github.shadowsocks.widget.UndoSnackbarManager
|
||||
import java.net.IDN
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
|
||||
class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener, ActionMode.Callback {
|
||||
companion object {
|
||||
private const val TEMPLATE_REGEX_DOMAIN = "(^|\\.)%s$"
|
||||
|
||||
@@ -70,6 +71,92 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
Domain,
|
||||
Url;
|
||||
}
|
||||
private inner class AclRuleDialog(item: Any = "") : TextWatcher, AdapterView.OnItemSelectedListener {
|
||||
val builder: AlertDialog.Builder
|
||||
val templateSelector: Spinner
|
||||
val editText: EditText
|
||||
private val inputLayout: TextInputLayout
|
||||
private lateinit var dialog: AlertDialog
|
||||
private lateinit var positive: Button
|
||||
|
||||
init {
|
||||
val activity = activity!!
|
||||
val view = activity.layoutInflater.inflate(R.layout.dialog_acl_rule, null)
|
||||
templateSelector = view.findViewById(R.id.template_selector)
|
||||
editText = view.findViewById(R.id.content)
|
||||
inputLayout = view.findViewById(R.id.content_layout)
|
||||
when (item) {
|
||||
is String -> {
|
||||
val match = PATTERN_DOMAIN.find(item)
|
||||
if (match != null) {
|
||||
templateSelector.setSelection(Template.Domain.ordinal)
|
||||
editText.setText(IDN.toUnicode(match.value.replace("\\.", "."),
|
||||
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES))
|
||||
} else {
|
||||
templateSelector.setSelection(Template.Generic.ordinal)
|
||||
editText.setText(item)
|
||||
}
|
||||
}
|
||||
is URL -> {
|
||||
templateSelector.setSelection(Template.Url.ordinal)
|
||||
editText.setText(item.toString())
|
||||
}
|
||||
else -> {
|
||||
templateSelector.setSelection(Template.Generic.ordinal)
|
||||
editText.setText(item.toString())
|
||||
}
|
||||
}
|
||||
templateSelector.onItemSelectedListener = this
|
||||
editText.addTextChangedListener(this)
|
||||
builder = AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.edit_rule)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setView(view)
|
||||
}
|
||||
|
||||
fun show() {
|
||||
dialog = builder.create()
|
||||
dialog.show()
|
||||
positive = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
validate()
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
|
||||
override fun afterTextChanged(s: Editable) = validate(value = s)
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = check(false)
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = validate(position)
|
||||
|
||||
private fun validate(template: Int = templateSelector.selectedItemPosition, value: Editable = editText.text) {
|
||||
val error = when (Template.values()[template]) {
|
||||
Template.Generic -> if (value.isEmpty()) "" else null
|
||||
Template.Domain -> try {
|
||||
IDN.toASCII(value.toString(), IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES)
|
||||
null
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.cause?.message ?: e.message
|
||||
}
|
||||
Template.Url -> try {
|
||||
URL(value.toString())
|
||||
null
|
||||
} catch (e: MalformedURLException) {
|
||||
e.message
|
||||
}
|
||||
}
|
||||
inputLayout.error = error
|
||||
positive.isEnabled = error == null
|
||||
}
|
||||
|
||||
fun add(): Int? {
|
||||
val text = editText.text.toString()
|
||||
return when (Template.values()[templateSelector.selectedItemPosition]) {
|
||||
Template.Generic -> adapter.addToProxy(text)
|
||||
Template.Domain -> adapter.addHostname(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text,
|
||||
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\.")))
|
||||
Template.Url -> adapter.addURL(URL(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class AclRuleViewHolder(view: View) : RecyclerView.ViewHolder(view),
|
||||
View.OnClickListener, View.OnLongClickListener {
|
||||
@@ -103,19 +190,18 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
if (selectedItems.isNotEmpty()) onLongClick(v) else {
|
||||
val (templateSelector, editText, dialog) = createAclRuleDialog(item)
|
||||
dialog
|
||||
val dialog = AclRuleDialog(item)
|
||||
dialog.builder
|
||||
.setNeutralButton(R.string.delete, { _, _ ->
|
||||
adapter.remove(item)
|
||||
undoManager.remove(Pair(-1, item))
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok, { _, _ ->
|
||||
adapter.remove(item)
|
||||
val index = adapter.addFromTemplate(templateSelector.selectedItemPosition,
|
||||
editText.text.toString()) ?: adapter.add(item)
|
||||
val index = dialog.add() ?: adapter.add(item)
|
||||
if (index != null) list.post { list.scrollToPosition(index) }
|
||||
})
|
||||
.create().show()
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
override fun onLongClick(p0: View?): Boolean {
|
||||
@@ -142,16 +228,14 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
override fun getItemCount(): Int = acl.subnets.size() + acl.hostnames.size() + acl.urls.size()
|
||||
override fun getSectionTitle(i: Int): String {
|
||||
val j = i - acl.subnets.size()
|
||||
return try {
|
||||
(if (j < 0) acl.subnets[i].address.hostAddress.substring(0, 1) else {
|
||||
val k = j - acl.hostnames.size()
|
||||
if (k < 0) {
|
||||
val hostname = acl.hostnames[j]
|
||||
// don't convert IDN yet
|
||||
PATTERN_DOMAIN.find(hostname)?.value?.replace("\\.", ".") ?: hostname
|
||||
} else acl.urls[k].host
|
||||
}).substring(0, 1)
|
||||
} catch (_: IndexOutOfBoundsException) { " " }
|
||||
return (if (j < 0) acl.subnets[i].address.hostAddress.substring(0, 1) else {
|
||||
val k = j - acl.hostnames.size()
|
||||
if (k < 0) {
|
||||
val hostname = acl.hostnames[j]
|
||||
// don't convert IDN yet
|
||||
PATTERN_DOMAIN.find(hostname)?.value?.replace("\\.", ".") ?: hostname
|
||||
} else acl.urls[k].host
|
||||
}).firstOrNull()?.toString() ?: " "
|
||||
}
|
||||
|
||||
private fun apply() {
|
||||
@@ -207,20 +291,6 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
.forEach { if (result == null) result = it }
|
||||
return result
|
||||
}
|
||||
fun addFromTemplate(templateValue: Int, text: String): Int? {
|
||||
val template = Template.values()[templateValue]
|
||||
return when (template) {
|
||||
Template.Generic -> addToProxy(text)
|
||||
Template.Domain -> try {
|
||||
addHostname(TEMPLATE_REGEX_DOMAIN.format(Locale.ENGLISH, IDN.toASCII(text,
|
||||
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES).replace(".", "\\.")))
|
||||
} catch (exc: IllegalArgumentException) {
|
||||
Toast.makeText(activity, exc.message, Toast.LENGTH_SHORT).show()
|
||||
null
|
||||
}
|
||||
Template.Url -> addURL(URL(text))
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(i: Int) {
|
||||
val j = i - acl.subnets.size()
|
||||
@@ -288,44 +358,12 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
private val selectedItems = HashSet<Any>()
|
||||
private val adapter by lazy { AclRulesAdapter() }
|
||||
private lateinit var list: RecyclerView
|
||||
private var selectionItem: MenuItem? = null
|
||||
private var mode: ActionMode? = null
|
||||
private lateinit var undoManager: UndoSnackbarManager<Any>
|
||||
private val clipboard by lazy { activity!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager }
|
||||
|
||||
private fun onSelectedItemsUpdated() {
|
||||
val selectionItem = selectionItem
|
||||
if (selectionItem != null) selectionItem.isVisible = selectedItems.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun createAclRuleDialog(item: Any = ""): Triple<Spinner, EditText, AlertDialog.Builder> {
|
||||
val view = activity!!.layoutInflater.inflate(R.layout.dialog_acl_rule, null)
|
||||
val templateSelector = view.findViewById<Spinner>(R.id.template_selector)
|
||||
val editText = view.findViewById<EditText>(R.id.content)
|
||||
when (item) {
|
||||
is String -> {
|
||||
val match = PATTERN_DOMAIN.find(item)
|
||||
if (match != null) {
|
||||
templateSelector.setSelection(Template.Domain.ordinal)
|
||||
editText.setText(IDN.toUnicode(match.value.replace("\\.", "."),
|
||||
IDN.ALLOW_UNASSIGNED or IDN.USE_STD3_ASCII_RULES))
|
||||
} else {
|
||||
templateSelector.setSelection(Template.Generic.ordinal)
|
||||
editText.setText(item)
|
||||
}
|
||||
}
|
||||
is URL -> {
|
||||
templateSelector.setSelection(Template.Url.ordinal)
|
||||
editText.setText(item.toString())
|
||||
}
|
||||
else -> {
|
||||
templateSelector.setSelection(Template.Generic.ordinal)
|
||||
editText.setText(item.toString())
|
||||
}
|
||||
}
|
||||
return Triple(templateSelector, editText, AlertDialog.Builder(activity!!)
|
||||
.setTitle(R.string.edit_rule)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setView(view))
|
||||
if (selectedItems.isEmpty()) mode?.finish() else if (mode == null) mode = toolbar.startActionMode(this)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
|
||||
@@ -345,9 +383,6 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
toolbar.setTitle(R.string.custom_rules)
|
||||
toolbar.inflateMenu(R.menu.custom_rules_menu)
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
val selectionItem = toolbar.menu.findItem(R.id.selection)
|
||||
selectionItem.isVisible = selectedItems.isNotEmpty()
|
||||
this.selectionItem = selectionItem
|
||||
list = view.findViewById(R.id.list)
|
||||
list.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
||||
list.itemAnimator = DefaultItemAnimator()
|
||||
@@ -365,10 +400,9 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return if (selectedItems.isNotEmpty()) {
|
||||
selectedItems.clear()
|
||||
onSelectedItemsUpdated()
|
||||
adapter.notifyDataSetChanged()
|
||||
val mode = mode
|
||||
return if (mode != null) {
|
||||
mode.finish()
|
||||
true
|
||||
} else super.onBackPressed()
|
||||
}
|
||||
@@ -395,29 +429,10 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.action_select_all -> {
|
||||
adapter.selectAll()
|
||||
true
|
||||
}
|
||||
R.id.action_cut -> {
|
||||
copySelected()
|
||||
adapter.removeSelected()
|
||||
true
|
||||
}
|
||||
R.id.action_copy -> {
|
||||
copySelected()
|
||||
true
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
adapter.removeSelected()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_manual_settings -> {
|
||||
val (templateSelector, editText, dialog) = createAclRuleDialog()
|
||||
dialog.setPositiveButton(android.R.string.ok, { _, _ ->
|
||||
adapter.addFromTemplate(templateSelector.selectedItemPosition, editText.text.toString())
|
||||
}).create().show()
|
||||
val dialog = AclRuleDialog()
|
||||
dialog.builder.setPositiveButton(android.R.string.ok, { _, _ -> dialog.add() })
|
||||
dialog.show()
|
||||
true
|
||||
}
|
||||
R.id.action_import -> {
|
||||
@@ -442,6 +457,53 @@ class CustomRulesFragment : ToolbarFragment(), Toolbar.OnMenuItemClickListener {
|
||||
|
||||
override fun onDetach() {
|
||||
undoManager.flush()
|
||||
mode?.finish()
|
||||
super.onDetach()
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val activity = activity!!
|
||||
val window = activity.window
|
||||
// In the end material_grey_100 is used for background, see AppCompatDrawableManager (very complicated)
|
||||
if (Build.VERSION.SDK_INT >= 23) {
|
||||
window.statusBarColor = ContextCompat.getColor(activity, R.color.material_grey_300)
|
||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||
} else window.statusBarColor = ContextCompat.getColor(activity, R.color.material_grey_600)
|
||||
activity.menuInflater.inflate(R.menu.custom_rules_selection, menu)
|
||||
toolbar.touchscreenBlocksFocus = true
|
||||
return true
|
||||
}
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = false
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = when (item.itemId) {
|
||||
R.id.action_select_all -> {
|
||||
adapter.selectAll()
|
||||
true
|
||||
}
|
||||
R.id.action_cut -> {
|
||||
copySelected()
|
||||
adapter.removeSelected()
|
||||
true
|
||||
}
|
||||
R.id.action_copy -> {
|
||||
copySelected()
|
||||
true
|
||||
}
|
||||
R.id.action_delete -> {
|
||||
adapter.removeSelected()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
val activity = activity!!
|
||||
val window = activity.window
|
||||
window.statusBarColor = ContextCompat.getColor(activity,
|
||||
activity.theme.resolveResourceId(android.R.attr.statusBarColor))
|
||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
|
||||
toolbar.touchscreenBlocksFocus = false
|
||||
selectedItems.clear()
|
||||
onSelectedItemsUpdated()
|
||||
adapter.notifyDataSetChanged()
|
||||
this.mode = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ import android.os.RemoteCallbackList
|
||||
import android.support.v4.os.UserManagerCompat
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
@@ -88,17 +87,14 @@ object BaseService {
|
||||
val closeReceiver = broadcastReceiver { _, intent ->
|
||||
when (intent.action) {
|
||||
Action.RELOAD -> service.forceLoad()
|
||||
else -> {
|
||||
Toast.makeText(service as Context, R.string.stopping, Toast.LENGTH_SHORT).show()
|
||||
service.stopRunner(true)
|
||||
}
|
||||
else -> service.stopRunner(true)
|
||||
}
|
||||
}
|
||||
var closeReceiverRegistered = false
|
||||
|
||||
val binder = object : IShadowsocksService.Stub() {
|
||||
override fun getState(): Int = this@Data.state
|
||||
override fun getProfileName(): String = profile?.formattedName ?: "Idle"
|
||||
override fun getProfileName(): String = profile?.name ?: "Idle"
|
||||
|
||||
override fun registerCallback(cb: IShadowsocksServiceCallback) {
|
||||
callbacks.register(cb)
|
||||
@@ -149,18 +145,18 @@ object BaseService {
|
||||
}
|
||||
|
||||
internal fun updateTrafficTotal(tx: Long, rx: Long) {
|
||||
val profile = profile ?: return
|
||||
val p = ProfileManager.getProfile(profile.id) ?: return // profile may have host, etc. modified
|
||||
p.tx += tx
|
||||
p.rx += rx
|
||||
ProfileManager.updateProfile(p)
|
||||
// this.profile may have host, etc. modified and thus a re-fetch is necessary (possible race condition)
|
||||
val profile = ProfileManager.getProfile((profile ?: return).id) ?: return
|
||||
profile.tx += tx
|
||||
profile.rx += rx
|
||||
ProfileManager.updateProfile(profile)
|
||||
app.handler.post {
|
||||
if (bandwidthListeners.isNotEmpty()) {
|
||||
val n = callbacks.beginBroadcast()
|
||||
for (i in 0 until n) {
|
||||
try {
|
||||
val item = callbacks.getBroadcastItem(i)
|
||||
if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(p.id)
|
||||
if (bandwidthListeners.contains(item.asBinder())) item.trafficPersisted(profile.id)
|
||||
} catch (_: Exception) { } // ignore
|
||||
}
|
||||
callbacks.finishBroadcast()
|
||||
@@ -185,9 +181,9 @@ object BaseService {
|
||||
.put("plugin_opts", plugin.toString())
|
||||
}
|
||||
// sensitive Shadowsocks config is stored in
|
||||
val file = File((if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) {
|
||||
val file = File(if (UserManagerCompat.isUserUnlocked(app)) app.filesDir else @TargetApi(24) {
|
||||
app.deviceContext.noBackupFilesDir // only API 24+ will be in locked state
|
||||
}), CONFIG_FILE)
|
||||
}, CONFIG_FILE)
|
||||
shadowsocksConfigFile = file
|
||||
file.writeText(config.toString())
|
||||
return file
|
||||
@@ -221,9 +217,9 @@ object BaseService {
|
||||
false
|
||||
} else true
|
||||
fun forceLoad() {
|
||||
val p = app.currentProfile
|
||||
val profile = app.currentProfile
|
||||
?: return stopRunner(true, (this as Context).getString(R.string.profile_empty))
|
||||
if (!checkProfile(p)) return
|
||||
if (!checkProfile(profile)) return
|
||||
val s = data.state
|
||||
when (s) {
|
||||
STOPPED -> startRunner()
|
||||
@@ -322,6 +318,7 @@ object BaseService {
|
||||
stopRunner(true, getString(R.string.profile_empty))
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
profile.name = profile.formattedName // save name for later queries
|
||||
data.profile = profile
|
||||
|
||||
TrafficMonitor.reset()
|
||||
|
||||
@@ -20,56 +20,23 @@
|
||||
|
||||
package com.github.shadowsocks.bg
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.BuildConfig
|
||||
import okhttp3.Dns as Okdns
|
||||
import org.xbill.DNS.*
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.UnknownHostException
|
||||
import java.util.*
|
||||
import java.io.IOException
|
||||
import java.net.*
|
||||
|
||||
object Dns {
|
||||
private const val TAG = "Dns"
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getDnsResolver(context: Context): String {
|
||||
val dnsResolvers = getDnsResolvers(context)
|
||||
if (dnsResolvers.isEmpty()) throw Exception("Couldn't find an active DNS resolver")
|
||||
val dnsResolver = dnsResolvers.iterator().next().toString()
|
||||
if (dnsResolver.startsWith("/")) return dnsResolver.substring(1)
|
||||
return dnsResolver
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun getDnsResolvers(context: Context): Collection<InetAddress> {
|
||||
val addresses = ArrayList<InetAddress>()
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val classLinkProperties = Class.forName("android.net.LinkProperties")
|
||||
val getActiveLinkPropertiesMethod = ConnectivityManager::class.java.getMethod("getActiveLinkProperties")
|
||||
val linkProperties = getActiveLinkPropertiesMethod.invoke(connectivityManager)
|
||||
if (linkProperties != null) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
val dnses = classLinkProperties.getMethod("getDnses").invoke(linkProperties) as Collection<*>
|
||||
dnses.mapTo(addresses) { it as InetAddress }
|
||||
} else addresses.addAll((linkProperties as LinkProperties).dnsServers)
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
private val hasIPv6Support get() = try {
|
||||
val result = NetworkInterface.getNetworkInterfaces().asSequence().flatMap { it.inetAddresses.asSequence() }
|
||||
.count { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress } > 0
|
||||
.any { it is Inet6Address && !it.isLoopbackAddress && !it.isLinkLocalAddress }
|
||||
if (result && BuildConfig.DEBUG) Log.d(TAG, "IPv6 address detected")
|
||||
result
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "Failed to get interfaces' addresses.")
|
||||
} catch (ex: SocketException) {
|
||||
Log.e(TAG, "Failed to get interfaces' addresses.", ex)
|
||||
app.track(ex)
|
||||
false
|
||||
}
|
||||
@@ -90,7 +57,7 @@ object Dns {
|
||||
Type.AAAA -> return (r as AAAARecord).address.hostAddress
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
} catch (_: IOException) { }
|
||||
return null
|
||||
}
|
||||
private fun resolve(host: String): String? = try {
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.JniHelper
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
object Executable {
|
||||
const val REDSOCKS = "libredsocks.so"
|
||||
@@ -37,7 +38,11 @@ object Executable {
|
||||
|
||||
fun killAll() {
|
||||
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }) {
|
||||
val exe = File(File(process, "cmdline").readText().split(Character.MIN_VALUE, limit = 2).first())
|
||||
val exe = File(try {
|
||||
File(process, "cmdline").readText()
|
||||
} catch (ignore: FileNotFoundException) {
|
||||
continue
|
||||
}.split(Character.MIN_VALUE, limit = 2).first())
|
||||
if (exe.parent == app.applicationInfo.nativeLibraryDir && EXECUTABLES.contains(exe.name)) {
|
||||
val errno = JniHelper.sigkill(process.name.toInt())
|
||||
if (errno != 0) Log.w("kill", "SIGKILL ${exe.absolutePath} (${process.name}) failed with $errno")
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
|
||||
package com.github.shadowsocks.bg
|
||||
|
||||
import android.content.Context
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
@@ -47,14 +46,14 @@ object LocalDnsService {
|
||||
val data = data
|
||||
val profile = data.profile!!
|
||||
|
||||
fun makeDns(name: String, address: String, edns: Boolean = true): JSONObject {
|
||||
fun makeDns(name: String, address: String, timeout: Int, edns: Boolean = true): JSONObject {
|
||||
val dns = JSONObject()
|
||||
.put("Name", name)
|
||||
.put("Address", (when (address.parseNumericAddress()) {
|
||||
.put("Address", when (address.parseNumericAddress()) {
|
||||
is Inet6Address -> "[$address]"
|
||||
else -> address
|
||||
}) + ":53")
|
||||
.put("Timeout", 6)
|
||||
})
|
||||
.put("Timeout", timeout)
|
||||
.put("EDNSClientSubnet", JSONObject().put("Policy", "disable"))
|
||||
if (edns) dns
|
||||
.put("Protocol", "tcp")
|
||||
@@ -70,21 +69,17 @@ object LocalDnsService {
|
||||
.put("RedirectIPv6Record", true)
|
||||
.put("DomainBase64Decode", false)
|
||||
.put("HostsFile", "hosts")
|
||||
.put("MinimumTTL", 3600)
|
||||
.put("MinimumTTL", 120)
|
||||
.put("CacheSize", 4096)
|
||||
val remoteDns = JSONArray(profile.remoteDns.split(",")
|
||||
.mapIndexed { i, dns -> makeDns("UserDef-" + i, dns.trim()) })
|
||||
.mapIndexed { i, dns -> makeDns("UserDef-" + i,
|
||||
dns.trim() + ":53", 9) })
|
||||
val localDns = JSONArray(arrayOf(
|
||||
makeDns("Primary-1", "119.29.29.29", false),
|
||||
makeDns("Primary-2", "114.114.114.114", false)
|
||||
makeDns("Primary-1", "119.29.29.29:53", 3, false),
|
||||
makeDns("Primary-2", "114.114.114.114:53", 3, false),
|
||||
makeDns("Primary-3", "208.67.222.222:443", 3, false)
|
||||
))
|
||||
|
||||
this as Context
|
||||
try {
|
||||
val localLinkDns = Dns.getDnsResolver(this)
|
||||
localDns.put(makeDns("Primary-3", localLinkDns, false))
|
||||
} catch (_: Exception) { } // ignore
|
||||
|
||||
when (profile.route) {
|
||||
Acl.BYPASS_CHN, Acl.BYPASS_LAN_CHN, Acl.GFWLIST, Acl.CUSTOM_RULES -> config
|
||||
.put("PrimaryDNS", localDns)
|
||||
|
||||
@@ -41,7 +41,7 @@ abstract class LocalSocketListener(protected val tag: String) : Thread() {
|
||||
* Inherited class do not need to close input/output streams as they will be closed automatically.
|
||||
*/
|
||||
protected abstract fun accept(socket: LocalSocket)
|
||||
override final fun run() {
|
||||
final override fun run() {
|
||||
socketFile.delete() // It's a must-have to close and reuse previous local socket.
|
||||
LocalSocket().use { localSocket ->
|
||||
val serverSocket = try {
|
||||
|
||||
@@ -23,7 +23,6 @@ package com.github.shadowsocks.bg
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.github.shadowsocks.database.Profile
|
||||
|
||||
/**
|
||||
* Shadowsocks service at its minimum.
|
||||
|
||||
@@ -40,8 +40,7 @@ import java.util.*
|
||||
|
||||
/**
|
||||
* Android < 8 VPN: always invisible because of VPN notification/icon
|
||||
* Android 4.x other: always visible
|
||||
* Android 5-7 other: only invisible in (possibly unsecure) lockscreen
|
||||
* Android < 8 other: only invisible in (possibly unsecure) lockscreen
|
||||
* Android 8+: always visible due to system limitations
|
||||
* (user can choose to hide the notification in secure lockscreen or anywhere)
|
||||
*/
|
||||
@@ -74,8 +73,7 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
|
||||
.setColor(ContextCompat.getColor(service, R.color.material_primary_500))
|
||||
.setTicker(service.getString(R.string.forward_success))
|
||||
.setContentTitle(profileName)
|
||||
.setContentIntent(PendingIntent.getActivity(service, 0, Intent(service, MainActivity::class.java)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT), 0))
|
||||
.setContentIntent(MainActivity.pendingIntent(service))
|
||||
.setSmallIcon(R.drawable.ic_service_active)
|
||||
private val style = NotificationCompat.BigTextStyle(builder)
|
||||
private var isVisible = true
|
||||
@@ -85,24 +83,22 @@ class ServiceNotification(private val service: BaseService.Interface, profileNam
|
||||
if (Build.VERSION.SDK_INT < 24) builder.addAction(R.drawable.ic_navigation_close,
|
||||
service.getString(R.string.stop), PendingIntent.getBroadcast(service, 0, Intent(Action.CLOSE), 0))
|
||||
val power = service.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
update(if (if (Build.VERSION.SDK_INT >= 20) power.isInteractive else @Suppress("DEPRECATION") power.isScreenOn)
|
||||
Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
|
||||
update(if (power.isInteractive) Intent.ACTION_SCREEN_ON else Intent.ACTION_SCREEN_OFF, true)
|
||||
val screenFilter = IntentFilter()
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_ON)
|
||||
screenFilter.addAction(Intent.ACTION_SCREEN_OFF)
|
||||
if (visible && Build.VERSION.SDK_INT in 21 until 26) screenFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
if (visible && Build.VERSION.SDK_INT < 26) screenFilter.addAction(Intent.ACTION_USER_PRESENT)
|
||||
service.registerReceiver(lockReceiver, screenFilter)
|
||||
}
|
||||
|
||||
private fun update(action: String, forceShow: Boolean = false) {
|
||||
if (forceShow || service.data.state == BaseService.CONNECTED) when (action) {
|
||||
Intent.ACTION_SCREEN_OFF -> {
|
||||
setVisible(visible && Build.VERSION.SDK_INT < 21, forceShow)
|
||||
setVisible(false, forceShow)
|
||||
unregisterCallback() // unregister callback to save battery
|
||||
}
|
||||
Intent.ACTION_SCREEN_ON -> {
|
||||
setVisible(visible && (Build.VERSION.SDK_INT < 21 || !keyGuard.inKeyguardRestrictedInputMode()),
|
||||
forceShow)
|
||||
setVisible(visible && !keyGuard.inKeyguardRestrictedInputMode(), forceShow)
|
||||
service.data.binder.registerCallback(callback)
|
||||
service.data.binder.startListeningForBandwidth(callback)
|
||||
callbackRegistered = true
|
||||
|
||||
@@ -30,11 +30,12 @@ import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.ShadowsocksConnection
|
||||
import com.github.shadowsocks.aidl.IShadowsocksService
|
||||
import com.github.shadowsocks.aidl.IShadowsocksServiceCallback
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import android.service.quicksettings.TileService as BaseTileService
|
||||
|
||||
@RequiresApi(24)
|
||||
class TileService : BaseTileService(), ShadowsocksConnection.Interface {
|
||||
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_idle).setTint(0x79ffffff) }
|
||||
private val iconIdle by lazy { Icon.createWithResource(this, R.drawable.ic_service_idle) }
|
||||
private val iconBusy by lazy { Icon.createWithResource(this, R.drawable.ic_service_busy) }
|
||||
private val iconConnected by lazy { Icon.createWithResource(this, R.drawable.ic_service_active) }
|
||||
private val keyguard by lazy { getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager }
|
||||
@@ -81,7 +82,7 @@ class TileService : BaseTileService(), ShadowsocksConnection.Interface {
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
if (isLocked) unlockAndRun(this::toggle) else toggle()
|
||||
if (isLocked && !DataStore.directBootAware) unlockAndRun(this::toggle) else toggle()
|
||||
}
|
||||
|
||||
private fun toggle() {
|
||||
|
||||
@@ -44,7 +44,7 @@ object TrafficMonitor {
|
||||
private val units = arrayOf("KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "NB", "DB", "CB")
|
||||
private val numberFormat = DecimalFormat("@@@")
|
||||
fun formatTraffic(size: Long): String {
|
||||
var n: Double = size.toDouble()
|
||||
var n = size.toDouble()
|
||||
var i = -1
|
||||
while (n >= 999.5) {
|
||||
n /= 1024
|
||||
|
||||
@@ -37,8 +37,7 @@ class TrafficMonitorThread : LocalSocketListener("TrafficMonitorThread") {
|
||||
if (socket.inputStream.read(buffer) != 16) throw IOException("Unexpected traffic stat length")
|
||||
val stat = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN)
|
||||
TrafficMonitor.update(stat.getLong(0), stat.getLong(8))
|
||||
socket.outputStream.write(0)
|
||||
} catch (e: Exception) {
|
||||
} catch (e: IOException) {
|
||||
Log.e(tag, "Error when recv traffic stat", e)
|
||||
app.track(e)
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.LocalSocket
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.util.Log
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.JniHelper
|
||||
import com.github.shadowsocks.MainActivity
|
||||
import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.VpnRequestActivity
|
||||
import com.github.shadowsocks.acl.Acl
|
||||
@@ -38,6 +38,7 @@ import com.github.shadowsocks.utils.Subnet
|
||||
import com.github.shadowsocks.utils.parseNumericAddress
|
||||
import java.io.File
|
||||
import java.io.FileDescriptor
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Method
|
||||
import java.util.*
|
||||
import android.net.VpnService as BaseVpnService
|
||||
@@ -55,18 +56,24 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
override val socketFile: File = File(app.deviceContext.filesDir, "protect_path")
|
||||
|
||||
override fun accept(socket: LocalSocket) {
|
||||
var success = false
|
||||
try {
|
||||
socket.inputStream.read()
|
||||
val fds = socket.ancillaryFileDescriptors
|
||||
if (fds.isEmpty()) return
|
||||
val fd = getInt.invoke(fds.first()) as Int
|
||||
val ret = protect(fd)
|
||||
success = protect(fd)
|
||||
JniHelper.close(fd) // Trick to close file decriptor
|
||||
socket.outputStream.write(if (ret) 0 else 1)
|
||||
} catch (e: Exception) {
|
||||
Log.e(tag, "Error when protect socket", e)
|
||||
app.track(e)
|
||||
}
|
||||
try {
|
||||
socket.outputStream.write(if (success) 0 else 1)
|
||||
} catch (e: IOException) {
|
||||
Log.e(tag, "Error when returning result in protect", e)
|
||||
app.track(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
class NullConnectionException : NullPointerException()
|
||||
@@ -118,7 +125,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
super.startNativeProcesses()
|
||||
|
||||
val fd = startVpn()
|
||||
if (!sendFd(fd)) throw Exception("sendFd failed")
|
||||
if (!sendFd(fd)) throw IOException("sendFd failed")
|
||||
}
|
||||
|
||||
override fun buildAdditionalArguments(cmd: ArrayList<String>): ArrayList<String> {
|
||||
@@ -129,6 +136,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
private fun startVpn(): Int {
|
||||
val profile = data.profile!!
|
||||
val builder = Builder()
|
||||
.setConfigureIntent(MainActivity.pendingIntent(this))
|
||||
.setSession(profile.formattedName)
|
||||
.setMtu(VPN_MTU)
|
||||
.addAddress(PRIVATE_VLAN.format(Locale.ENGLISH, "1"), 24)
|
||||
@@ -140,7 +148,7 @@ class VpnService : BaseVpnService(), LocalDnsService.Interface {
|
||||
builder.addRoute("::", 0)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21 && profile.proxyApps) {
|
||||
if (profile.proxyApps) {
|
||||
val me = packageName
|
||||
profile.individual.split('\n')
|
||||
.filter { it != me }
|
||||
|
||||
@@ -72,10 +72,10 @@ object PrivateDatabase : OrmLiteSqliteOpenHelper(app, Key.DB_PROFILE, null, 25)
|
||||
profileDao.executeRawNoArgs("ALTER TABLE `profile` RENAME TO `tmp`;")
|
||||
TableUtils.createTable(connectionSource, Profile::class.java)
|
||||
profileDao.executeRawNoArgs(
|
||||
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route, proxyApps, bypass," +
|
||||
" udpdns, ipv6, individual) " +
|
||||
"SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global, bypass, udpdns, ipv6," +
|
||||
" individual FROM `tmp`;")
|
||||
"INSERT INTO `profile`(id, name, host, localPort, remotePort, password, method, route," +
|
||||
" proxyApps, bypass, udpdns, ipv6, individual) " +
|
||||
"SELECT id, name, host, localPort, remotePort, password, method, route, 1 - global," +
|
||||
" bypass, udpdns, ipv6, individual FROM `tmp`;")
|
||||
profileDao.executeRawNoArgs("DROP TABLE `tmp`;")
|
||||
profileDao.executeRawNoArgs("COMMIT;")
|
||||
} else if (oldVersion < 13) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import java.util.*
|
||||
class Profile : Serializable {
|
||||
companion object {
|
||||
private const val TAG = "ShadowParser"
|
||||
private const val serialVersionUID = 0L
|
||||
private val pattern = """(?i)ss://[-a-zA-Z0-9+&@#/%?=~_|!:,.;\[\]]*[-a-zA-Z0-9+&@#/%=~_|\[\]]""".toRegex()
|
||||
private val userInfoPattern = "^(.+?):(.*)$".toRegex()
|
||||
private val legacyPattern = "^(.+?):(.*)@(.+?):(\\d+?)$".toRegex()
|
||||
|
||||
@@ -25,79 +25,69 @@ import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.ProfilesFragment
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import com.github.shadowsocks.utils.DirectBoot
|
||||
import java.sql.SQLException
|
||||
|
||||
/**
|
||||
* SQLExceptions are not caught (and therefore will cause crash) for insert/update transactions
|
||||
* to ensure we are in a consistent state.
|
||||
*/
|
||||
object ProfileManager {
|
||||
private const val TAG = "ProfileManager"
|
||||
|
||||
@Throws(SQLException::class)
|
||||
fun createProfile(p: Profile? = null): Profile {
|
||||
val profile = p ?: Profile()
|
||||
profile.id = 0
|
||||
try {
|
||||
val oldProfile = app.currentProfile
|
||||
if (oldProfile != null) {
|
||||
// Copy Feature Settings from old profile
|
||||
profile.route = oldProfile.route
|
||||
profile.ipv6 = oldProfile.ipv6
|
||||
profile.proxyApps = oldProfile.proxyApps
|
||||
profile.bypass = oldProfile.bypass
|
||||
profile.individual = oldProfile.individual
|
||||
profile.udpdns = oldProfile.udpdns
|
||||
}
|
||||
val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder()
|
||||
.selectRaw("MAX(userOrder)").prepareStatementString()).firstResult
|
||||
if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1
|
||||
PrivateDatabase.profileDao.createOrUpdate(profile)
|
||||
ProfilesFragment.instance?.profilesAdapter?.add(profile)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "addProfile", ex)
|
||||
app.track(ex)
|
||||
val oldProfile = app.currentProfile
|
||||
if (oldProfile != null) {
|
||||
// Copy Feature Settings from old profile
|
||||
profile.route = oldProfile.route
|
||||
profile.ipv6 = oldProfile.ipv6
|
||||
profile.proxyApps = oldProfile.proxyApps
|
||||
profile.bypass = oldProfile.bypass
|
||||
profile.individual = oldProfile.individual
|
||||
profile.udpdns = oldProfile.udpdns
|
||||
}
|
||||
val last = PrivateDatabase.profileDao.queryRaw(PrivateDatabase.profileDao.queryBuilder()
|
||||
.selectRaw("MAX(userOrder)").prepareStatementString()).firstResult
|
||||
if (last != null && last.size == 1 && last[0] != null) profile.userOrder = last[0].toLong() + 1
|
||||
PrivateDatabase.profileDao.createOrUpdate(profile)
|
||||
ProfilesFragment.instance?.profilesAdapter?.add(profile)
|
||||
return profile
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: It's caller's responsibility to update DirectBoot profile if necessary.
|
||||
*/
|
||||
fun updateProfile(profile: Profile): Boolean = try {
|
||||
PrivateDatabase.profileDao.update(profile)
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "updateProfile", ex)
|
||||
app.track(ex)
|
||||
false
|
||||
}
|
||||
@Throws(SQLException::class)
|
||||
fun updateProfile(profile: Profile) = PrivateDatabase.profileDao.update(profile)
|
||||
|
||||
fun getProfile(id: Int): Profile? = try {
|
||||
PrivateDatabase.profileDao.queryForId(id)
|
||||
} catch (ex: Exception) {
|
||||
} catch (ex: SQLException) {
|
||||
Log.e(TAG, "getProfile", ex)
|
||||
app.track(ex)
|
||||
null
|
||||
}
|
||||
|
||||
fun delProfile(id: Int): Boolean = try {
|
||||
@Throws(SQLException::class)
|
||||
fun delProfile(id: Int) {
|
||||
PrivateDatabase.profileDao.deleteById(id)
|
||||
ProfilesFragment.instance?.profilesAdapter?.removeId(id)
|
||||
if (id == DataStore.profileId && DataStore.directBootAware) DirectBoot.clean()
|
||||
true
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "delProfile", ex)
|
||||
app.track(ex)
|
||||
false
|
||||
}
|
||||
|
||||
fun getFirstProfile(): Profile? = try {
|
||||
val result = PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare())
|
||||
if (result.size == 1) result[0] else null
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "getAllProfiles", ex)
|
||||
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().limit(1L).prepare()).singleOrNull()
|
||||
} catch (ex: SQLException) {
|
||||
Log.e(TAG, "getFirstProfile", ex)
|
||||
app.track(ex)
|
||||
null
|
||||
}
|
||||
|
||||
fun getAllProfiles(): List<Profile>? = try {
|
||||
PrivateDatabase.profileDao.query(PrivateDatabase.profileDao.queryBuilder().orderBy("userOrder", true).prepare())
|
||||
} catch (ex: Exception) {
|
||||
} catch (ex: SQLException) {
|
||||
Log.e(TAG, "getAllProfiles", ex)
|
||||
app.track(ex)
|
||||
null
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
package com.github.shadowsocks.plugin
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.ContentResolver
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
@@ -84,11 +85,16 @@ object PluginManager {
|
||||
""", Base64.DEFAULT))
|
||||
}
|
||||
|
||||
private val receiver by lazy { app.listenForPackageChanges { synchronized(this) { cachedPlugins = null } } }
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
private var cachedPlugins: Map<String, Plugin>? = null
|
||||
fun fetchPlugins(): Map<String, Plugin> {
|
||||
receiver
|
||||
return synchronized(this) {
|
||||
if (receiver == null) receiver = app.listenForPackageChanges {
|
||||
synchronized(this) {
|
||||
receiver = null
|
||||
cachedPlugins = null
|
||||
}
|
||||
}
|
||||
if (cachedPlugins == null) {
|
||||
val pm = app.packageManager
|
||||
cachedPlugins = (pm.queryIntentContentProviders(Intent(PluginContract.ACTION_NATIVE_PLUGIN),
|
||||
|
||||
@@ -29,6 +29,7 @@ import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.support.design.widget.BottomSheetDialog
|
||||
import android.support.v7.preference.PreferenceDialogFragmentCompat
|
||||
import android.support.v7.widget.DefaultItemAnimator
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
@@ -107,10 +108,12 @@ class BottomSheetPreferenceDialogFragment : PreferenceDialogFragmentCompat() {
|
||||
recycler.setPadding(0, padding, 0, padding)
|
||||
recycler.setHasFixedSize(true)
|
||||
recycler.layoutManager = LinearLayoutManager(activity)
|
||||
recycler.itemAnimator = DefaultItemAnimator()
|
||||
recycler.adapter = IconListAdapter(dialog)
|
||||
recycler.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
dialog.setContentView(recycler)
|
||||
dialog.findViewById<View>(R.id.touch_outside)!!.isFocusable = false
|
||||
return dialog
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ object DataStore {
|
||||
/**
|
||||
* Setter is defined in MainActivity.onPreferenceDataStoreChanged.
|
||||
*/
|
||||
val directBootAware: Boolean get() = BootReceiver.enabled && (publicStore.getBoolean(Key.directBootAware) ?: false)
|
||||
val directBootAware: Boolean get() = BootReceiver.enabled && publicStore.getBoolean(Key.directBootAware) == true
|
||||
var serviceMode: String
|
||||
get() = publicStore.getString(Key.serviceMode) ?: Key.modeVpn
|
||||
set(value) = publicStore.putString(Key.serviceMode, value)
|
||||
|
||||
@@ -28,11 +28,7 @@ import android.util.AttributeSet
|
||||
class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPreference(context, attrs) {
|
||||
var entryIcons: Array<Drawable?>? = null
|
||||
val selectedEntry: Int get() = entryValues.indexOf(value)
|
||||
val entryIcon: Drawable? get() = try {
|
||||
entryIcons?.get(selectedEntry)
|
||||
} catch (_: ArrayIndexOutOfBoundsException) {
|
||||
null
|
||||
}
|
||||
val entryIcon: Drawable? get() = entryIcons?.getOrNull(selectedEntry)
|
||||
// fun setEntryIcons(@ArrayRes entryIconsResId: Int) {
|
||||
// val array = getContext().getResources().obtainTypedArray(entryIconsResId)
|
||||
// entryIcons = Array(array.length(), { i -> array.getDrawable(i) })
|
||||
@@ -49,7 +45,7 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
|
||||
}
|
||||
|
||||
init {
|
||||
super.setOnPreferenceChangeListener({ preference, newValue ->
|
||||
super.setOnPreferenceChangeListener { preference, newValue ->
|
||||
val listener = listener
|
||||
if (listener == null || listener.onPreferenceChange(preference, newValue)) {
|
||||
value = newValue.toString()
|
||||
@@ -57,7 +53,7 @@ class IconListPreference(context: Context, attrs: AttributeSet? = null) : ListPr
|
||||
if (entryIcons != null) icon = entryIcon
|
||||
true
|
||||
} else false
|
||||
})
|
||||
}
|
||||
// val a = context.obtainStyledAttributes(attrs, R.styleable.IconListPreference)
|
||||
// val entryIconsResId: Int = a.getResourceId(R.styleable.IconListPreference_entryIcons, -1)
|
||||
// if (entryIconsResId != -1) entryIcons = entryIconsResId
|
||||
|
||||
@@ -22,7 +22,6 @@ package com.github.shadowsocks.tasker
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.DefaultItemAnimator
|
||||
@@ -71,13 +70,12 @@ class ConfigActivity : AppCompatActivity() {
|
||||
|
||||
inner class ProfilesAdapter : RecyclerView.Adapter<ProfileViewHolder>() {
|
||||
internal val profiles = ProfileManager.getAllProfiles()?.toMutableList() ?: mutableListOf()
|
||||
private val name = "select_dialog_singlechoice_" + (if (Build.VERSION.SDK_INT >= 21) "material" else "holo")
|
||||
|
||||
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) =
|
||||
if (position == 0) holder.bindDefault() else holder.bind(profiles[position - 1])
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder = ProfileViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(Resources.getSystem()
|
||||
.getIdentifier(name, "layout", "android"), parent, false))
|
||||
.getIdentifier("select_dialog_singlechoice_material", "layout", "android"), parent, false))
|
||||
override fun getItemCount(): Int = 1 + profiles.size
|
||||
}
|
||||
|
||||
@@ -87,12 +85,12 @@ class ConfigActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
try {
|
||||
taskerOption = Settings.fromIntent(intent!!)
|
||||
} catch (_: Exception) {
|
||||
val intent = intent
|
||||
if (intent == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
taskerOption = Settings.fromIntent(intent)
|
||||
setContentView(R.layout.layout_tasker)
|
||||
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
|
||||
@@ -44,8 +44,8 @@ class Settings(bundle: Bundle?) {
|
||||
if (profileId >= 0) bundle.putInt(KEY_PROFILE_ID, profileId)
|
||||
val profile = ProfileManager.getProfile(profileId)
|
||||
return Intent().putExtra(ApiIntent.EXTRA_BUNDLE, bundle).putExtra(ApiIntent.EXTRA_STRING_BLURB,
|
||||
if (profile != null)
|
||||
context.getString(if (switchOn) R.string.start_service else R.string.stop_service, profile.formattedName)
|
||||
if (profile != null) context.getString(if (switchOn) R.string.start_service else R.string.stop_service,
|
||||
profile.formattedName)
|
||||
else context.getString(if (switchOn) R.string.start_service_default else R.string.stop))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
|
||||
package com.github.shadowsocks.utils
|
||||
|
||||
import java.util.ArrayList
|
||||
import java.util.Arrays
|
||||
import java.util.StringTokenizer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Commandline objects help handling command lines specifying processes to
|
||||
@@ -86,9 +84,7 @@ object Commandline {
|
||||
* @return empty string for null or no command, else every argument split
|
||||
* by spaces and quoted by quoting rules.
|
||||
*/
|
||||
fun toString(args: Array<String>): String {
|
||||
return toString(Arrays.asList(*args)) // thanks to Java, arrays aren't iterable
|
||||
}
|
||||
fun toString(args: Array<String>) = toString(args.asIterable()) // thanks to Java, arrays aren't iterable
|
||||
|
||||
/**
|
||||
* Crack a command line.
|
||||
|
||||
@@ -7,7 +7,7 @@ import com.github.shadowsocks.database.Profile
|
||||
import com.github.shadowsocks.database.ProfileManager
|
||||
import com.github.shadowsocks.preference.DataStore
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.ObjectInputStream
|
||||
import java.io.ObjectOutputStream
|
||||
|
||||
@@ -17,7 +17,7 @@ object DirectBoot {
|
||||
|
||||
fun getDeviceProfile(): Profile? = try {
|
||||
ObjectInputStream(file.inputStream()).use { it.readObject() as Profile }
|
||||
} catch (_: FileNotFoundException) { null }
|
||||
} catch (_: IOException) { null }
|
||||
|
||||
fun clean() {
|
||||
file.delete()
|
||||
|
||||
@@ -52,4 +52,5 @@ object TcpFastOpen {
|
||||
"else",
|
||||
" echo Failed.",
|
||||
"fi"), null, true)?.joinToString("\n")
|
||||
fun enabledAsync(value: Boolean) = thread { enabled(value) }.join(1000)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.support.annotation.AttrRes
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v4.app.FragmentManager
|
||||
import android.support.v7.util.SortedList
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import android.util.TypedValue
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.JniHelper
|
||||
@@ -22,11 +21,6 @@ private val fieldChildFragmentManager by lazy {
|
||||
field
|
||||
}
|
||||
|
||||
fun isAccessibilityEnabled(context: Context): Boolean {
|
||||
val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
||||
return am.isEnabled()
|
||||
}
|
||||
|
||||
fun String.isNumericAddress() = JniHelper.parseNumericAddress(this) != null
|
||||
fun String.parseNumericAddress(): InetAddress? {
|
||||
val addr = JniHelper.parseNumericAddress(this)
|
||||
@@ -34,8 +28,8 @@ fun String.parseNumericAddress(): InetAddress? {
|
||||
}
|
||||
|
||||
fun parsePort(str: String?, default: Int, min: Int = 1025): Int {
|
||||
val x = str?.toIntOrNull() ?: default
|
||||
return if (x < min || x > 65535) default else x
|
||||
val value = str?.toIntOrNull() ?: default
|
||||
return if (value < min || value > 65535) default else value
|
||||
}
|
||||
|
||||
fun broadcastReceiver(callback: (Context, Intent) -> Unit): BroadcastReceiver = object : BroadcastReceiver() {
|
||||
@@ -78,6 +72,6 @@ private class SortedListIterable<out T>(private val list: SortedList<T>) : Itera
|
||||
private class SortedListIterator<out T>(private val list: SortedList<T>) : Iterator<T> {
|
||||
private var count = 0
|
||||
override fun hasNext() = count < list.size()
|
||||
override fun next(): T = list[count++]
|
||||
override fun next(): T = if (hasNext()) list[count++] else throw NoSuchElementException()
|
||||
}
|
||||
fun <T> SortedList<T>.asIterable(): Iterable<T> = SortedListIterable(this)
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Copyright (C) 2017 by Max Lv <max.c.lv@gmail.com> *
|
||||
* Copyright (C) 2017 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/>. *
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
package com.github.shadowsocks.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.support.design.widget.CoordinatorLayout
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import com.github.shadowsocks.R
|
||||
|
||||
class BottomMarginBehavior : CoordinatorLayout.Behavior<View> {
|
||||
constructor() : super()
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
|
||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean =
|
||||
dependency.id == R.id.stat // sorry I'm too lazy to write an attribute
|
||||
|
||||
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
|
||||
(child.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = (parent.height - dependency.y).toInt()
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,6 @@ import android.support.graphics.drawable.AnimatedVectorDrawableCompat
|
||||
import android.support.v7.widget.TooltipCompat
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import com.github.shadowsocks.App.Companion.app
|
||||
import com.github.shadowsocks.R
|
||||
import com.github.shadowsocks.bg.BaseService
|
||||
import java.util.*
|
||||
@@ -84,9 +83,7 @@ class ServiceButton @JvmOverloads constructor(context: Context, attrs: Attribute
|
||||
TooltipCompat.setTooltipText(this, context.getString(R.string.connect))
|
||||
}
|
||||
refreshDrawableState()
|
||||
isEnabled = false
|
||||
if (state == BaseService.CONNECTED || state == BaseService.STOPPED) app.handler.postDelayed(
|
||||
{ isEnabled = state == BaseService.CONNECTED || state == BaseService.STOPPED }, 1000)
|
||||
isEnabled = state == BaseService.CONNECTED || state == BaseService.STOPPED
|
||||
}
|
||||
|
||||
private fun counters(a: AnimatedVectorDrawableCompat, b: AnimatedVectorDrawableCompat): Boolean =
|
||||
|
||||
@@ -20,32 +20,27 @@
|
||||
|
||||
package com.github.shadowsocks.widget
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.support.design.widget.CoordinatorLayout
|
||||
import android.support.design.widget.SnackbarAnimation
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.design.widget.SnackbarAnimation
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
|
||||
import com.github.shadowsocks.utils.isAccessibilityEnabled
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
|
||||
/**
|
||||
* Full credits go to: https://stackoverflow.com/a/35904421/2245107
|
||||
*/
|
||||
class MoveUpwardBehavior : CoordinatorLayout.Behavior<View> {
|
||||
constructor() : super()
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
class ShrinkUpwardBehavior(context: Context, attrs: AttributeSet) : CoordinatorLayout.Behavior<View>(context, attrs) {
|
||||
private val accessibility = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
||||
|
||||
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean =
|
||||
dependency is Snackbar.SnackbarLayout
|
||||
dependency is Snackbar.SnackbarLayout
|
||||
|
||||
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
|
||||
child.translationY = Math.min(0f, dependency.translationY - dependency.height)
|
||||
child.layoutParams.height = dependency.y.toInt()
|
||||
child.requestLayout()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -53,24 +48,18 @@ class MoveUpwardBehavior : CoordinatorLayout.Behavior<View> {
|
||||
* Based on BaseTransientBottomBar.animateViewOut (support lib 27.0.2).
|
||||
*/
|
||||
override fun onDependentViewRemoved(parent: CoordinatorLayout, child: View, dependency: View) {
|
||||
if (!isAccessibilityEnabled(parent.getContext())) {
|
||||
if (accessibility.isEnabled) child.layoutParams.height = parent.height else {
|
||||
val animator = ValueAnimator()
|
||||
val start = child.translationY
|
||||
animator.setFloatValues(start, 0f)
|
||||
val start = child.height
|
||||
animator.setIntValues(start, parent.height)
|
||||
animator.interpolator = SnackbarAnimation.FAST_OUT_SLOW_IN_INTERPOLATOR
|
||||
animator.duration = SnackbarAnimation.ANIMATION_DURATION
|
||||
animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener {
|
||||
private var previousValue = start
|
||||
|
||||
override fun onAnimationUpdate(animator: ValueAnimator) {
|
||||
val currentValue = animator.animatedValue as Float
|
||||
child.translationY = currentValue
|
||||
previousValue = currentValue
|
||||
}
|
||||
})
|
||||
@Suppress("NAME_SHADOWING")
|
||||
animator.addUpdateListener { animator ->
|
||||
child.layoutParams.height = animator.animatedValue as Int
|
||||
child.requestLayout()
|
||||
}
|
||||
animator.start()
|
||||
} else {
|
||||
child.translationY = 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 198 B |
|
Before Width: | Height: | Size: 400 B |
|
Before Width: | Height: | Size: 156 B |
|
Before Width: | Height: | Size: 298 B |
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/material_primary_300" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
<item android:state_selected="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/material_green_700" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
<!-- android:start not available until API 23 -->
|
||||
<item android:left="8dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/white" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="?android:attr/selectableItemBackground"/>
|
||||
</layer-list>
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight">
|
||||
<item>
|
||||
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
<item android:state_selected="true">
|
||||
<color android:color="@color/material_primary_100"/>
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
</ripple>
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight">
|
||||
<item>
|
||||
<color android:color="@color/material_primary_300"/>
|
||||
</item>
|
||||
</ripple>
|
||||
|
Before Width: | Height: | Size: 228 B |
|
Before Width: | Height: | Size: 510 B |
|
Before Width: | Height: | Size: 284 B |
|
Before Width: | Height: | Size: 748 B |
|
Before Width: | Height: | Size: 364 B |
@@ -15,18 +15,11 @@
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
<item android:left="@dimen/profile_padding">
|
||||
<!-- android:start not available until API 23 -->
|
||||
<item android:left="8dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/white" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<selector>
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#12000000" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
<item android:drawable="?android:attr/selectableItemBackground"/>
|
||||
</layer-list>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:enterFadeDuration="@android:integer/config_mediumAnimTime"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
<item android:state_selected="true">
|
||||
<color android:color="@color/material_primary_100"/>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<selector android:enterFadeDuration="@android:integer/config_mediumAnimTime"
|
||||
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||
<item android:state_selected="true">
|
||||
<color android:color="@color/material_primary_100"/>
|
||||
</item>
|
||||
</selector>
|
||||
</item>
|
||||
</selector>
|
||||
<item android:drawable="?android:attr/selectableItemBackground"/>
|
||||
</layer-list>
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/material_primary_300" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<selector>
|
||||
<item android:state_pressed="true">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#12000000" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
<color android:color="@color/material_primary_300"/>
|
||||
</item>
|
||||
<item android:drawable="?android:attr/selectableItemBackground"/>
|
||||
</layer-list>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
|
||||
<path
|
||||
android:fillColor="#f5f5f5"
|
||||
android:pathData="M24,24m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0Z" />
|
||||
<path
|
||||
android:fillColor="#546e7a"
|
||||
android:pathData="M24,21.8 C25.7673,21.8,27.2,23.2327,27.2,25 C27.2,26.7673,25.7673,28.2,24,28.2
|
||||
C22.2327,28.2,20.8,26.7673,20.8,25 C20.8,23.2327,22.2327,21.8,24,21.8 Z" />
|
||||
<path
|
||||
android:fillColor="#546e7a"
|
||||
android:pathData="M21,15 L19.17,17 L16,17 A2,2,0,0,0,14,19 L14,31 A2,2,0,0,0,16,33 L32,33
|
||||
A2,2,0,0,0,34,31 L34,19 A2,2,0,0,0,32,17 L28.83,17 L27,15 Z M24,30
|
||||
A5,5,0,1,1,29,25 A5,5,0,0,1,24,30 Z" />
|
||||
</vector>
|
||||
@@ -2,9 +2,7 @@
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:drawable="@drawable/ic_service_busy"
|
||||
tools:ignore="NewApi">
|
||||
android:drawable="@drawable/ic_service_busy">
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:drawable="@drawable/ic_service_idle"
|
||||
tools:ignore="NewApi">
|
||||
android:drawable="@drawable/ic_service_idle">
|
||||
<target android:name="strike_thru_path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:drawable="@drawable/ic_service_idle"
|
||||
tools:ignore="NewApi">
|
||||
android:drawable="@drawable/ic_service_idle">
|
||||
<target android:name="strike_thru_path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<animated-vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:drawable="@drawable/ic_service_busy"
|
||||
tools:ignore="NewApi">
|
||||
android:drawable="@drawable/ic_service_busy">
|
||||
<target android:name="path">
|
||||
<aapt:attr name="android:animation">
|
||||
<objectAnimator
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
@@ -8,8 +9,15 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:labelFor="@+id/content"
|
||||
android:entries="@array/acl_rule_templates"/>
|
||||
<EditText android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textNoSuggestions|textMultiLine"/>
|
||||
<android.support.design.widget.TextInputLayout
|
||||
android:id="@+id/content_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:errorEnabled="true">
|
||||
<android.support.design.widget.TextInputEditText
|
||||
android:id="@+id/content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textNoSuggestions|textMultiLine"/>
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
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:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:duplicateParentState="false">
|
||||
<include layout="@layout/toolbar_light_dark"/>
|
||||
@@ -41,28 +41,36 @@
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="18sp"/>
|
||||
</LinearLayout>
|
||||
<FrameLayout android:layout_width="fill_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
<ProgressBar android:id="@+id/loading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:id="@+id/list"
|
||||
tools:listitem="@layout/layout_apps_item"/>
|
||||
<com.futuremind.recyclerviewfastscroll.FastScroller
|
||||
android:id="@+id/fastscroller"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:fastscroll__handleColor="@color/material_primary_500"
|
||||
app:fastscroll__bubbleColor="@color/material_primary_500"
|
||||
app:fastscroll__bubbleTextAppearance="@style/StyledScrollerTextAppearance"/>
|
||||
</FrameLayout>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
android:id="@+id/snackbar"
|
||||
android:background="@color/material_primary_700"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
<FrameLayout android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:attr/windowBackground"
|
||||
app:layout_behavior="com.github.shadowsocks.widget.ShrinkUpwardBehavior">
|
||||
<ProgressBar android:id="@+id/loading"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center" />
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:id="@+id/list"
|
||||
tools:listitem="@layout/layout_apps_item"/>
|
||||
<com.futuremind.recyclerviewfastscroll.FastScroller
|
||||
android:id="@+id/fastscroller"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:fastscroll__handleColor="@color/material_primary_500"
|
||||
app:fastscroll__bubbleColor="@color/material_primary_500"
|
||||
app:fastscroll__bubbleTextAppearance="@style/StyledScrollerTextAppearance"/>
|
||||
</FrameLayout>
|
||||
</android.support.design.widget.CoordinatorLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -9,140 +9,146 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_holder"
|
||||
android:background="?android:attr/windowBackground"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="com.github.shadowsocks.widget.ShrinkUpwardBehavior">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/stat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/background_stat"
|
||||
android:elevation="4dp"
|
||||
android:focusable="true"
|
||||
android:nextFocusRight="@+id/fab"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="16dip"
|
||||
android:paddingEnd="16dip"
|
||||
android:paddingStart="16dip"
|
||||
android:paddingTop="16dip"
|
||||
android:layout_gravity="bottom"
|
||||
app:layout_behavior="com.github.shadowsocks.widget.MoveUpwardBehavior">
|
||||
<FrameLayout
|
||||
android:id="@+id/fragment_holder"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:layout_above="@+id/stat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:paddingEnd="72dip"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="16sp"
|
||||
tools:ignore="RtlSymmetry"/>
|
||||
|
||||
<com.github.shadowsocks.widget.BoundedGridLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/stat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:bounded_width="288dp"
|
||||
app:columnCount="4">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.AppCompat.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="end"
|
||||
android:text="@string/sent"
|
||||
app:layout_column="0"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tx"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="fill_horizontal"
|
||||
android:ellipsize="marquee"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="1"
|
||||
app:layout_columnWeight="1"/>
|
||||
android:background="@drawable/background_stat"
|
||||
android:elevation="4dp"
|
||||
android:focusable="true"
|
||||
android:nextFocusRight="@+id/fab"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="16dip"
|
||||
android:paddingEnd="16dip"
|
||||
android:paddingStart="16dip"
|
||||
android:paddingTop="16dip"
|
||||
android:layout_alignParentBottom="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:text="▲"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="2"/>
|
||||
android:paddingEnd="72dip"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="16sp"
|
||||
tools:ignore="RtlSymmetry"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txRate"
|
||||
android:layout_width="0dp"
|
||||
<com.github.shadowsocks.widget.BoundedGridLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="fill_horizontal"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="end"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="3"
|
||||
app:layout_columnWeight="1"/>
|
||||
app:bounded_width="288dp"
|
||||
app:columnCount="4">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.AppCompat.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="end"
|
||||
android:text="@string/received"
|
||||
app:layout_column="0"
|
||||
app:layout_row="1"/>
|
||||
<TextView
|
||||
style="@style/TextAppearance.AppCompat.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="end"
|
||||
android:text="@string/sent"
|
||||
app:layout_column="0"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rx"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="fill_horizontal"
|
||||
android:ellipsize="marquee"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="1"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_row="1"/>
|
||||
<TextView
|
||||
android:id="@+id/tx"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="fill_horizontal"
|
||||
android:ellipsize="marquee"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="1"
|
||||
app:layout_columnWeight="1"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:text="▼"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="2"
|
||||
app:layout_row="1"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:text="▲"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="2"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rxRate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="fill_horizontal"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="end"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="3"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_row="1"/>
|
||||
</com.github.shadowsocks.widget.BoundedGridLayout>
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/txRate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="fill_horizontal"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="end"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="3"
|
||||
app:layout_columnWeight="1"/>
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.AppCompat.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="end"
|
||||
android:text="@string/received"
|
||||
app:layout_column="0"
|
||||
app:layout_row="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rx"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="fill_horizontal"
|
||||
android:ellipsize="marquee"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="1"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_row="1"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:text="▼"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="2"
|
||||
app:layout_row="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/rxRate"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="fill_horizontal"
|
||||
android:ellipsize="marquee"
|
||||
android:gravity="end"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="14sp"
|
||||
app:layout_column="3"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_row="1"/>
|
||||
</com.github.shadowsocks.widget.BoundedGridLayout>
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- workaround for 0 elevation in disabled state -->
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="@dimen/fab_margin_top"
|
||||
android:layout_marginTop="-28dp"
|
||||
android:elevation="6dp"
|
||||
app:layout_anchor="@id/stat"
|
||||
app:layout_anchorGravity="top|end">
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
<include layout="@layout/toolbar_light_dark" />
|
||||
<me.dm7.barcodescanner.zxing.ZXingScannerView android:id="@+id/scanner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
<fragment
|
||||
android:id="@+id/barcode"
|
||||
android:name="com.google.android.gms.samples.vision.barcodereader.BarcodeCapture"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
app:gvb_auto_focus="true"
|
||||
app:gvb_code_format="qr_code"/>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<menu xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/action_apply_all"
|
||||
android:title="@string/action_apply_all"
|
||||
android:alphabeticShortcut="a"
|
||||
|
||||
@@ -1,41 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/selection"
|
||||
android:alphabeticShortcut="s"
|
||||
android:icon="?attr/actionModeSelectAllDrawable"
|
||||
android:numericShortcut="0"
|
||||
android:title="@string/action_selection"
|
||||
android:visible="false"
|
||||
app:showAsAction="always">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/action_select_all"
|
||||
android:alphabeticShortcut="a"
|
||||
android:numericShortcut="1"
|
||||
android:title="@android:string/selectAll"
|
||||
app:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_cut"
|
||||
android:alphabeticShortcut="t"
|
||||
android:numericShortcut="2"
|
||||
android:title="@android:string/cut"
|
||||
app:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_copy"
|
||||
android:alphabeticShortcut="c"
|
||||
android:numericShortcut="3"
|
||||
android:title="@android:string/copy"
|
||||
app:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:alphabeticShortcut="d"
|
||||
android:numericShortcut="4"
|
||||
android:title="@string/delete"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
</item>
|
||||
<item android:title="@string/action_add_rule"
|
||||
android:icon="@drawable/ic_av_playlist_add"
|
||||
android:alphabeticShortcut="a"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="AppCompatResource">
|
||||
<item
|
||||
android:id="@+id/action_select_all"
|
||||
android:alphabeticShortcut="a"
|
||||
android:icon="?attr/actionModeSelectAllDrawable"
|
||||
android:numericShortcut="1"
|
||||
android:title="@android:string/selectAll"
|
||||
android:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_cut"
|
||||
android:alphabeticShortcut="t"
|
||||
android:icon="?attr/actionModeCutDrawable"
|
||||
android:numericShortcut="2"
|
||||
android:title="@android:string/cut"
|
||||
android:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_copy"
|
||||
android:alphabeticShortcut="c"
|
||||
android:icon="?attr/actionModeCopyDrawable"
|
||||
android:numericShortcut="3"
|
||||
android:title="@android:string/copy"
|
||||
android:showAsAction="always"/>
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:alphabeticShortcut="d"
|
||||
android:icon="@drawable/ic_action_delete"
|
||||
android:numericShortcut="4"
|
||||
android:title="@string/delete"
|
||||
android:showAsAction="always"/>
|
||||
</menu>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item android:id="@+id/action_import"
|
||||
android:title="@string/action_import_file"
|
||||
android:icon="@drawable/ic_image_photo"
|
||||
android:alphabeticShortcut="p"
|
||||
android:numericShortcut="1"
|
||||
app:showAsAction="always"/>
|
||||
</menu>
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<!-- misc -->
|
||||
<string name="profile">"پروفایل"</string>
|
||||
<string name="connection_test_testing">"در حال آزمایش..."</string>
|
||||
<string name="connection_test_testing">"در حال آزمایش…"</string>
|
||||
|
||||
<!-- proxy category -->
|
||||
<string name="profile_name">"نام پروفایل"</string>
|
||||
@@ -20,7 +20,7 @@
|
||||
<string name="invalid_server">"آدرس سرور نامعتبر است"</string>
|
||||
<string name="service_failed">"اتصال به سرور ناموفق بود!"</string>
|
||||
<string name="stop">"غیرفعالکردن"</string>
|
||||
<string name="stopping">"در حال قطعکردن اتصال..."</string>
|
||||
<string name="stopping">"در حال قطعکردن اتصال…"</string>
|
||||
<string name="profile_invalid_input">"هیچ پروفایل معتبری یافت نشد!"</string>
|
||||
|
||||
<!-- alert category -->
|
||||
@@ -33,7 +33,6 @@
|
||||
<string name="profiles">"پروفایلها"</string>
|
||||
<string name="settings">"تنظیمات"</string>
|
||||
<string name="faq">"سوالات مداول"</string>
|
||||
<string name="recovery">"ریست"</string>
|
||||
<string name="about">"درباره ما"</string>
|
||||
<string name="edit">"ویرایش"</string>
|
||||
<string name="share">"به اشتراک گذاری"</string>
|
||||
@@ -52,9 +51,8 @@
|
||||
<!-- status -->
|
||||
<string name="sent">"فرستادهشده:"</string>
|
||||
<string name="received">"دریافتشده:"</string>
|
||||
<string name="connecting">"در حال اتصال..."</string>
|
||||
<string name="connecting">"در حال اتصال…"</string>
|
||||
<string name="vpn_connected">"وصل شد. برای بررسی اتصال ضربه (Tap) بزنید."</string>
|
||||
<string name="nat_connected">"اتصال برقرار است."</string>
|
||||
<string name="not_connected">"اتصال برقرار نیست!"</string>
|
||||
<string name="route_entry_all">"همه"</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- misc -->
|
||||
<string name="profile">"Profil"</string>
|
||||
<string name="connection_test_error">"Impossible de détecter la connexion Internet :%s"</string>
|
||||
<string name="connection_test_fail">"Internet Indisponible"</string>
|
||||
<string name="connection_test_error_status_code">"Code de l'Erreur : #%d"</string>
|
||||
<string name="proxy">"Serveur"</string>
|
||||
<string name="sitekey">"Mot de passe"</string>
|
||||
<string name="service_failed">"Échec de la connexion au serveur distant"</string>
|
||||
|
||||
<!-- alert category -->
|
||||
<string name="close">"Fermer"</string>
|
||||
<string name="profile_empty">"Veuillez sélectionner un profil"</string>
|
||||
|
||||
<!-- menu category -->
|
||||
<string name="profiles">"Profils"</string>
|
||||
<string name="settings">"Paramètres"</string>
|
||||
<string name="about">"À propos"</string>
|
||||
<string name="share">"Partager"</string>
|
||||
<string name="add_profile">"Ajouter un Profil"</string>
|
||||
<string name="action_apply_all">"Appliquer les Paramètres à tous les Profils"</string>
|
||||
<string name="action_export">"Exporter vers le presse-papiers"</string>
|
||||
<string name="action_import">"Importer depuis le presse-papiers"</string>
|
||||
<string name="delete">"Supprimer"</string>
|
||||
<string name="delete_confirm_prompt">"Êtes-vous sûr de vouloir supprimer ce profil?"</string>
|
||||
<string name="share_qr_nfc">"QR Code/NFC"</string>
|
||||
<string name="add_profile_methods_scan_qr_code">"Scanner le QR Code"</string>
|
||||
<plurals name="removed">
|
||||
<item quantity="one">"Supprimé"</item>
|
||||
<item quantity="other">"%d éléments supprimés"</item>
|
||||
</plurals>
|
||||
<string name="undo">"Annuler"</string>
|
||||
</resources>
|
||||